From f677f1ce3af8f9765030a4f29b727450c0721641 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Mon, 18 Sep 2023 23:27:02 +0200 Subject: [PATCH 001/345] 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 | 3 + 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/pkg/gnolang/values.go | 2 +- gnovm/stdlibs/unicode/utf16/export_test.gno | 11 + 102 files changed, 7847 insertions(+), 38 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 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 - {{ end }} - + + {{ template "html_head" . }} + gno.land + + +
+ +
+ This is the gno.land (test) {{ if .Data.Config.CaptchaSite }} + + {{ end }} + - + // Reset the captcha + grecaptcha.reset(); + }); + }; + -
-
-
- {{ if .Data.Config.CaptchaSite }} -
- {{ end }} -
-
-
- -
- {{ template "footer" }} -
- {{ template "js" }} - +
+
+
+ {{ if .Data.Config.CaptchaSite }} +
+ {{ end }} +
+ +
+
+
+ + + {{ template "footer" }} + + {{ template "js" }} + {{- end -}} diff --git a/gno.land/pkg/gnoweb/views/funcs.html b/gno.land/pkg/gnoweb/views/funcs.html index 626d01d8448..5a3a086e155 100644 --- a/gno.land/pkg/gnoweb/views/funcs.html +++ b/gno.land/pkg/gnoweb/views/funcs.html @@ -1,220 +1,337 @@ {{- define "header_buttons" -}}
- +
-{{- end -}} - -{{- define "html_head" -}} +{{- end -}} {{- define "html_head" -}} -{{if .Data.Description}}{{end}} +{{if .Data.Description}}{{end}} - - - - + + + + - + - + -{{- end -}} - -{{- define "logo" -}} +{{- end -}} {{- define "logo" -}} -{{- end -}} - -{{- define "footer" -}} -
- {{ template "logo" }} -
-{{- end -}} - -{{- define "js" -}} +{{- end -}} {{- define "footer" -}} +
{{ template "logo" }}
+{{- end -}} {{- define "js" -}} -{{ template "analytics" .}} -{{- end -}} - -{{- define "analytics" -}} -{{- if .Data.Config.WithAnalytics -}} +{{ template "analytics" .}} {{- end -}} {{- define "analytics" -}} {{- if +.Data.Config.WithAnalytics -}} - -{{- end -}} -{{- end -}} - -{{- define "subscribe" -}} + +{{- end -}} {{- end -}} {{- define "subscribe" -}}
-
- -
-
- - - -
-
- - -
- -
-
+
+ +
+
+ + + +
+
+ + +
+ +
+
{{- end -}} diff --git a/gno.land/pkg/gnoweb/views/generic.html b/gno.land/pkg/gnoweb/views/generic.html index 5bcd14c3a46..a917ad34c03 100644 --- a/gno.land/pkg/gnoweb/views/generic.html +++ b/gno.land/pkg/gnoweb/views/generic.html @@ -1,21 +1,24 @@ {{- define "app" -}} - + - - Gno.land - {{ .Data.Title }} - {{ template "html_head" . }} - - -
- -
-
+    
+        gno.land - {{ .Data.Title }}
+        {{ template "html_head" . }}
+    
+    
+        
+ +
+
           {{- .Data.MainContent -}}
-        
-
- {{ template "footer" }} -
- {{ template "js" .}} - +
+
+ {{ template "footer" }} +
+ {{ template "js" .}} + {{- end -}} diff --git a/gno.land/pkg/gnoweb/views/package_dir.html b/gno.land/pkg/gnoweb/views/package_dir.html index 793ebd40b84..ed7cd9a8347 100644 --- a/gno.land/pkg/gnoweb/views/package_dir.html +++ b/gno.land/pkg/gnoweb/views/package_dir.html @@ -1,33 +1,37 @@ {{- define "app" -}} - + - - {{ template "html_head" . }} - Gno.land - {{.Data.DirPath}} - - -
- - -
{{ template "dir_contents" . }}
- {{ template "footer" }} -
- {{ template "js" . }} - + + {{ template "html_head" . }} + gno.land - {{.Data.DirPath}} + + +
+ + +
+ {{ template "dir_contents" . }} +
+ {{ template "footer" }} +
+ {{ template "js" . }} + -{{- end -}} - -{{- define "dir_contents" -}} +{{- end -}} {{- define "dir_contents" -}}
- {{ $dirPath := .Data.DirPath }} -
    - {{ range .Data.Files }} -
  • - {{ . }} -
  • - {{ end }} -
+ {{ $dirPath := .Data.DirPath }} +
    + {{ range .Data.Files }} +
  • + {{ . }} +
  • + {{ end }} +
{{- end -}} diff --git a/gno.land/pkg/gnoweb/views/package_file.html b/gno.land/pkg/gnoweb/views/package_file.html index 43e7820b29f..32d8af9e174 100644 --- a/gno.land/pkg/gnoweb/views/package_file.html +++ b/gno.land/pkg/gnoweb/views/package_file.html @@ -1,23 +1,28 @@ {{- define "app" -}} - + - - {{ template "html_head" . }} - Gno.land - {{.Data.DirPath}}/{{.Data.FileName}} - - -
- -
- {{ .Data.DirPath }}/{{ .Data.FileName }} -
-
- {{ .Data.FileContents }} -
+ + {{ template "html_head" . }} + gno.land - {{.Data.DirPath}}/{{.Data.FileName}} + + +
+ +
+ + {{ .Data.DirPath }}/{{ + .Data.FileName }} + +
+
+ {{ .Data.FileContents }} +
- {{ template "footer" }} -
- {{ template "js" .}} - + {{ template "footer" }} +
+ {{ template "js" .}} + {{- end -}} diff --git a/gno.land/pkg/gnoweb/views/realm_help.html b/gno.land/pkg/gnoweb/views/realm_help.html index 7bde8fef7fa..f33bb50e9e6 100644 --- a/gno.land/pkg/gnoweb/views/realm_help.html +++ b/gno.land/pkg/gnoweb/views/realm_help.html @@ -1,89 +1,100 @@ {{- define "app" -}} - + - - {{ template "html_head" . }} - Gno.land - {{.Data.DirPath}} - - -
-
- - + + {{ template "html_head" . }} + gno.land - {{.Data.DirPath}} + + +
+
+ +
+ + {{ .Data.DirPath }}$help + +
-
-
- These are the realm's exposed functions ("public smart contracts").
-
- My address: (see `gnokey list`)
-
-
- {{ template "func_specs" . }} -
+
+
+ These are the realm's exposed functions ("public smart + contracts").
+
+ My address: + (see + `gnokey list`)
+
+
+ {{ template "func_specs" . }} +
- {{ template "footer" }} -
- {{ template "js" . }} - - + {{ template "footer" }} +
+ {{ template "js" . }} + + -{{- end -}} - -{{- define "func_specs" -}} +{{- end -}} {{- define "func_specs" -}}
- {{ $funcName := .Data.FuncName }} {{ $found := false }} {{ if eq $funcName "" }} {{ range .Data.FunctionSignatures }} {{ template "func_spec" . }} {{ end }} {{ else }} {{ range - .Data.FunctionSignatures }} {{ if eq .FuncName $funcName }} {{ $found = true }} {{ template "func_spec" . }} {{ end }} {{ end }} {{ if not $found }} {{ $funcName }} not found. {{ end }} {{ end }} + {{ $funcName := .Data.FuncName }} {{ $found := false }} {{ if eq $funcName + "" }} {{ range .Data.FunctionSignatures }} {{ template "func_spec" . }} {{ + end }} {{ else }} {{ range .Data.FunctionSignatures }} {{ if eq .FuncName + $funcName }} {{ $found = true }} {{ template "func_spec" . }} {{ end }} {{ + end }} {{ if not $found }} {{ $funcName }} not found. {{ end }} {{ end }}
-{{- end -}} - -{{- define "func_spec" -}} +{{- end -}} {{- define "func_spec" -}}
- - - - - - - - - - - - - - - - - -
contract{{ .FuncName }}(...)
params - - {{ range .Params }}{{ template "func_param" . }}{{ end }} -
-
results - - {{ range .Results }}{{ template "func_result" . }}{{ end }} -
-
command -
-
+ + + + + + + + + + + + + + + + + +
contract{{ .FuncName }}(...)
params + + {{ range .Params }}{{ template "func_param" . }}{{ end }} +
+
results + + {{ range .Results }}{{ template "func_result" . }}{{ end }} +
+
command +
+
-{{- end -}} - -{{- define "func_param" -}} +{{- end -}} {{- define "func_param" -}} - {{ .Name }} - - - - {{ .Type }} + {{ .Name }} + + + + {{ .Type }} -{{- end -}} - -{{- define "func_result" -}} +{{- end -}} {{- define "func_result" -}} - {{ .Name }} - {{ .Type }} + {{ .Name }} + {{ .Type }} {{ end }} diff --git a/gno.land/pkg/gnoweb/views/realm_render.html b/gno.land/pkg/gnoweb/views/realm_render.html index 1b5842cba1f..978b6afed46 100644 --- a/gno.land/pkg/gnoweb/views/realm_render.html +++ b/gno.land/pkg/gnoweb/views/realm_render.html @@ -1,35 +1,40 @@ {{- define "app" -}} - + - - {{ template "html_head" . }} - Gno.land - {{.Data.RealmName}} - - -
- -
- /r/{{ .Data.RealmName }} - {{- if .Data.Query -}}:{{- end -}} {{- range $index, $link := .Data.PathLinks -}} {{- if (gt $index 0) }}/{{ end -}} - {{ $link.Text }} - {{- end -}} - - - {{ if .Data.HasReadme }} - [readme] - {{ end }} - [source] - [help] - -
+ + {{ template "html_head" . }} + gno.land - {{.Data.RealmName}} + + +
+ +
+ /r/{{ .Data.RealmName }} + {{- if .Data.Query -}}:{{- end -}} {{- range $index, $link + := .Data.PathLinks -}} {{- if (gt $index 0) }}/{{ end -}} + {{ $link.Text }} + {{- end -}} + + + {{ if .Data.HasReadme }} + [readme] + {{ end }} + [source] + [help] + +
-
-
{{ .Data.Contents }}
-
- {{ template "footer" }} -
- {{ template "js" .}} - +
+
{{ .Data.Contents }}
+
+ {{ template "footer" }} +
+ {{ template "js" .}} + {{- end -}} diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index dfb1e9f114c..9410eede29e 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -29,7 +29,7 @@ type PackageGetter func(pkgPath string, store Store) (*PackageNode, *PackageValu type NativeStore func(pkgName string, name Name) func(m *Machine) // Store is the central interface that specifies the communications between the -// GnoVM and the underlying data store; currently, generally the Gno.land +// GnoVM and the underlying data store; currently, generally the gno.land // blockchain, or the file system. type Store interface { // STABLE From 81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:06:30 +0100 Subject: [PATCH 215/345] feat: add p/avl/pager (#2584) - add `p/demo/avl/pager` - update `r/demo/users` Hey reviewers, in addition to what you wanted to review, I'm specifically curious if you have any better API/usage ideas. Example: https://github.com/gnolang/gno/pull/2584/files#diff-8d5cbbe072737a7f288f74adcaaace11cacc3d31264e6a001515fcae824394e2R33 Related with #447, #599, #868 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Antonio Navarro Perez Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- examples/gno.land/p/demo/avl/pager/gno.mod | 8 + examples/gno.land/p/demo/avl/pager/pager.gno | 213 ++++++++++++++++++ .../gno.land/p/demo/avl/pager/pager_test.gno | 191 ++++++++++++++++ .../gno.land/p/demo/avl/pager/z_filetest.gno | 101 +++++++++ examples/gno.land/r/demo/users/gno.mod | 1 + examples/gno.land/r/demo/users/users.gno | 30 ++- .../gno.land/r/demo/users/z_5_filetest.gno | 6 + 7 files changed, 543 insertions(+), 7 deletions(-) create mode 100644 examples/gno.land/p/demo/avl/pager/gno.mod create mode 100644 examples/gno.land/p/demo/avl/pager/pager.gno create mode 100644 examples/gno.land/p/demo/avl/pager/pager_test.gno create mode 100644 examples/gno.land/p/demo/avl/pager/z_filetest.gno diff --git a/examples/gno.land/p/demo/avl/pager/gno.mod b/examples/gno.land/p/demo/avl/pager/gno.mod new file mode 100644 index 00000000000..59c961d73f2 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/gno.mod @@ -0,0 +1,8 @@ +module gno.land/p/demo/avl/pager + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno new file mode 100644 index 00000000000..60bb44d97b6 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -0,0 +1,213 @@ +package pager + +import ( + "math" + "net/url" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +// Pager is a struct that holds the AVL tree and pagination parameters. +type Pager struct { + Tree *avl.Tree + PageQueryParam string + SizeQueryParam string + DefaultPageSize int +} + +// Page represents a single page of results. +type Page struct { + Items []Item + PageNumber int + PageSize int + TotalItems int + TotalPages int + HasPrev bool + HasNext bool + Pager *Pager // Reference to the parent Pager +} + +// Item represents a key-value pair in the AVL tree. +type Item struct { + Key string + Value interface{} +} + +// NewPager creates a new Pager with default values. +func NewPager(tree *avl.Tree, defaultPageSize int) *Pager { + return &Pager{ + Tree: tree, + PageQueryParam: "page", + SizeQueryParam: "size", + DefaultPageSize: defaultPageSize, + } +} + +// GetPage retrieves a page of results from the AVL tree. +func (p *Pager) GetPage(pageNumber int) *Page { + return p.GetPageWithSize(pageNumber, p.DefaultPageSize) +} + +func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { + totalItems := p.Tree.Size() + totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize))) + + page := &Page{ + TotalItems: totalItems, + TotalPages: totalPages, + PageSize: pageSize, + Pager: p, + } + + // pages without content + if pageSize < 1 { + return page + } + + // page number provided is not available + if pageNumber < 1 { + page.HasNext = totalPages > 0 + return page + } + + // page number provided is outside the range of total pages + if pageNumber > totalPages { + page.PageNumber = pageNumber + page.HasPrev = pageNumber > 0 + return page + } + + startIndex := (pageNumber - 1) * pageSize + endIndex := startIndex + pageSize + if endIndex > totalItems { + endIndex = totalItems + } + + items := []Item{} + p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + + page.Items = items + page.PageNumber = pageNumber + page.HasPrev = pageNumber > 1 + page.HasNext = pageNumber < totalPages + return page +} + +func (p *Pager) MustGetPageByPath(rawURL string) *Page { + page, err := p.GetPageByPath(rawURL) + if err != nil { + panic("invalid path") + } + return page +} + +// GetPageByPath retrieves a page of results based on the query parameters in the URL path. +func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { + pageNumber, pageSize, err := p.ParseQuery(rawURL) + if err != nil { + return nil, err + } + return p.GetPageWithSize(pageNumber, pageSize), nil +} + +// UI generates the Markdown UI for the page selector. +func (p *Page) Selector() string { + pageNumber := p.PageNumber + pageNumber = max(pageNumber, 1) + + if p.TotalPages <= 1 { + return "" + } + + md := "" + + if p.HasPrev { + // Always show the first page link + md += ufmt.Sprintf("[%d](?%s=%d) | ", 1, p.Pager.PageQueryParam, 1) + + // Before + if p.PageNumber > 4 { + md += "… | " + } + + if p.PageNumber > 3 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2) + } + + if p.PageNumber > 2 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1) + } + } + + if p.PageNumber > 0 && p.PageNumber <= p.TotalPages { + // Current page + md += ufmt.Sprintf("**%d**", p.PageNumber) + } else { + md += ufmt.Sprintf("_%d_", p.PageNumber) + } + + if p.HasNext { + md += " | " + + if p.PageNumber < p.TotalPages-1 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1) + } + + if p.PageNumber < p.TotalPages-2 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2) + } + + if p.PageNumber < p.TotalPages-3 { + md += "… | " + } + + // Always show the last page link + md += ufmt.Sprintf("[%d](?%s=%d)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages) + } + + return md +} + +// ParseQuery parses the URL to extract the page number and page size. +func (p *Pager) ParseQuery(rawURL string) (int, int, error) { + u, err := url.Parse(rawURL) + if err != nil { + return 1, p.DefaultPageSize, err + } + + query := u.Query() + pageNumber := 1 + pageSize := p.DefaultPageSize + + if p.PageQueryParam != "" { + if pageStr := query.Get(p.PageQueryParam); pageStr != "" { + pageNumber, err = strconv.Atoi(pageStr) + if err != nil || pageNumber < 1 { + pageNumber = 1 + } + } + } + + if p.SizeQueryParam != "" { + if sizeStr := query.Get(p.SizeQueryParam); sizeStr != "" { + pageSize, err = strconv.Atoi(sizeStr) + if err != nil || pageSize < 1 { + pageSize = p.DefaultPageSize + } + } + } + + return pageNumber, pageSize, nil +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/examples/gno.land/p/demo/avl/pager/pager_test.gno b/examples/gno.land/p/demo/avl/pager/pager_test.gno new file mode 100644 index 00000000000..da4680db8c7 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/pager_test.gno @@ -0,0 +1,191 @@ +package pager + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +func TestPager_GetPage(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + tree.Set("a", 1) + tree.Set("b", 2) + tree.Set("c", 3) + tree.Set("d", 4) + tree.Set("e", 5) + + // Create a new pager. + pager := NewPager(tree, 10) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, + {3, 2, []Item{{Key: "e", Value: 5}}}, + {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {2, 5, []Item{}}, + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + uassert.Equal(t, len(tt.expected), len(page.Items)) + + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } + } +} + +func TestPager_GetPageByPath(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + for i := 0; i < 50; i++ { + tree.Set(ufmt.Sprintf("key%d", i), i) + } + + // Create a new pager. + pager := NewPager(tree, 10) + + // Define test cases. + tests := []struct { + rawURL string + expectedPage int + expectedSize int + }{ + {"/r/foo:bar/baz?size=10&page=1", 1, 10}, + {"/r/foo:bar/baz?size=10&page=2", 2, 10}, + {"/r/foo:bar/baz?page=3", 3, pager.DefaultPageSize}, + {"/r/foo:bar/baz?size=20", 1, 20}, + {"/r/foo:bar/baz", 1, pager.DefaultPageSize}, + } + + for _, tt := range tests { + page, err := pager.GetPageByPath(tt.rawURL) + urequire.NoError(t, err, ufmt.Sprintf("GetPageByPath(%s) returned error: %v", tt.rawURL, err)) + + uassert.Equal(t, tt.expectedPage, page.PageNumber) + uassert.Equal(t, tt.expectedSize, page.PageSize) + } +} + +func TestPage_Selector(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + tree.Set("a", 1) + tree.Set("b", 2) + tree.Set("c", 3) + tree.Set("d", 4) + tree.Set("e", 5) + + // Create a new pager. + pager := NewPager(tree, 10) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected string + }{ + {1, 2, "**1** | [2](?page=2) | [3](?page=3)"}, + {2, 2, "[1](?page=1) | **2** | [3](?page=3)"}, + {3, 2, "[1](?page=1) | [2](?page=2) | **3**"}, + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + ui := page.Selector() + uassert.Equal(t, tt.expected, ui) + } +} + +func TestPager_UI_WithManyPages(t *testing.T) { + // Create a new AVL tree and populate it with many key-value pairs. + tree := avl.NewTree() + for i := 0; i < 100; i++ { + tree.Set(ufmt.Sprintf("key%d", i), i) + } + + // Create a new pager. + pager := NewPager(tree, 10) + + // Define test cases for a large number of pages. + tests := []struct { + pageNumber int + pageSize int + expected string + }{ + // XXX: -1 + // XXX: 0 + {1, 10, "**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)"}, + {2, 10, "[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)"}, + {3, 10, "[1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | … | [10](?page=10)"}, + {4, 10, "[1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) | … | [10](?page=10)"}, + {5, 10, "[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)"}, + {6, 10, "[1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** | [7](?page=7) | [8](?page=8) | … | [10](?page=10)"}, + {7, 10, "[1](?page=1) | … | [5](?page=5) | [6](?page=6) | **7** | [8](?page=8) | [9](?page=9) | [10](?page=10)"}, + {8, 10, "[1](?page=1) | … | [6](?page=6) | [7](?page=7) | **8** | [9](?page=9) | [10](?page=10)"}, + {9, 10, "[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)"}, + {10, 10, "[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**"}, + // XXX: 11 + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + ui := page.Selector() + uassert.Equal(t, tt.expected, ui) + } +} + +func TestPager_ParseQuery(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + tree.Set("a", 1) + tree.Set("b", 2) + tree.Set("c", 3) + tree.Set("d", 4) + tree.Set("e", 5) + + // Create a new pager. + pager := NewPager(tree, 10) + + // Define test cases. + tests := []struct { + rawURL string + expectedPage int + expectedSize int + expectedError bool + }{ + {"/r/foo:bar/baz?size=2&page=1", 1, 2, false}, + {"/r/foo:bar/baz?size=3&page=2", 2, 3, false}, + {"/r/foo:bar/baz?size=5&page=3", 3, 5, false}, + {"/r/foo:bar/baz?page=2", 2, pager.DefaultPageSize, false}, + {"/r/foo:bar/baz?size=3", 1, 3, false}, + {"/r/foo:bar/baz", 1, pager.DefaultPageSize, false}, + {"/r/foo:bar/baz?size=0&page=0", 1, pager.DefaultPageSize, false}, + } + + for _, tt := range tests { + page, size, err := pager.ParseQuery(tt.rawURL) + if tt.expectedError { + uassert.Error(t, err, ufmt.Sprintf("ParseQuery(%s) expected error but got none", tt.rawURL)) + } else { + urequire.NoError(t, err, ufmt.Sprintf("ParseQuery(%s) returned error: %v", tt.rawURL, err)) + uassert.Equal(t, tt.expectedPage, page, ufmt.Sprintf("ParseQuery(%s) returned page %d, expected %d", tt.rawURL, page, tt.expectedPage)) + uassert.Equal(t, tt.expectedSize, size, ufmt.Sprintf("ParseQuery(%s) returned size %d, expected %d", tt.rawURL, size, tt.expectedSize)) + } + } +} diff --git a/examples/gno.land/p/demo/avl/pager/z_filetest.gno b/examples/gno.land/p/demo/avl/pager/z_filetest.gno new file mode 100644 index 00000000000..91c20115469 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno @@ -0,0 +1,101 @@ +package main + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) + +func main() { + // Create a new AVL tree and populate it with some key-value pairs. + var id seqid.ID + tree := avl.NewTree() + for i := 0; i < 42; i++ { + tree.Set(id.Next().String(), i) + } + + // Create a new pager. + pager := pager.NewPager(tree, 7) + + for pn := -1; pn < 8; pn++ { + page := pager.GetPage(pn) + + println(ufmt.Sprintf("## Page %d of %d", page.PageNumber, page.TotalPages)) + for idx, item := range page.Items { + println(ufmt.Sprintf("- idx=%d key=%s value=%d", idx, item.Key, item.Value)) + } + println(page.Selector()) + println() + } +} + +// Output: +// ## Page 0 of 6 +// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6) +// +// ## Page 0 of 6 +// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6) +// +// ## Page 1 of 6 +// - idx=0 key=0000001 value=0 +// - idx=1 key=0000002 value=1 +// - idx=2 key=0000003 value=2 +// - idx=3 key=0000004 value=3 +// - idx=4 key=0000005 value=4 +// - idx=5 key=0000006 value=5 +// - idx=6 key=0000007 value=6 +// **1** | [2](?page=2) | [3](?page=3) | … | [6](?page=6) +// +// ## Page 2 of 6 +// - idx=0 key=0000008 value=7 +// - idx=1 key=0000009 value=8 +// - idx=2 key=000000a value=9 +// - idx=3 key=000000b value=10 +// - idx=4 key=000000c value=11 +// - idx=5 key=000000d value=12 +// - idx=6 key=000000e value=13 +// [1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [6](?page=6) +// +// ## Page 3 of 6 +// - idx=0 key=000000f value=14 +// - idx=1 key=000000g value=15 +// - idx=2 key=000000h value=16 +// - idx=3 key=000000j value=17 +// - idx=4 key=000000k value=18 +// - idx=5 key=000000m value=19 +// - idx=6 key=000000n value=20 +// [1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | [6](?page=6) +// +// ## Page 4 of 6 +// - idx=0 key=000000p value=21 +// - idx=1 key=000000q value=22 +// - idx=2 key=000000r value=23 +// - idx=3 key=000000s value=24 +// - idx=4 key=000000t value=25 +// - idx=5 key=000000v value=26 +// - idx=6 key=000000w value=27 +// [1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) +// +// ## Page 5 of 6 +// - idx=0 key=000000x value=28 +// - idx=1 key=000000y value=29 +// - idx=2 key=000000z value=30 +// - idx=3 key=0000010 value=31 +// - idx=4 key=0000011 value=32 +// - idx=5 key=0000012 value=33 +// - idx=6 key=0000013 value=34 +// [1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) +// +// ## Page 6 of 6 +// - idx=0 key=0000014 value=35 +// - idx=1 key=0000015 value=36 +// - idx=2 key=0000016 value=37 +// - idx=3 key=0000017 value=38 +// - idx=4 key=0000018 value=39 +// - idx=5 key=0000019 value=40 +// - idx=6 key=000001a value=41 +// [1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** +// +// ## Page 7 of 6 +// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_ diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod index cdef52b6952..f2f88a0f993 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -2,6 +2,7 @@ module gno.land/r/demo/users require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/avl/pager v0.0.0-latest gno.land/p/demo/avlhelpers v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/users v0.0.0-latest diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 4a0b9c1caf7..daad2e7fc18 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -7,6 +7,7 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" "gno.land/p/demo/avlhelpers" "gno.land/p/demo/users" ) @@ -301,9 +302,10 @@ var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`) //---------------------------------------- // Render main page -func Render(path string) string { +func Render(fullPath string) string { + path, _ := splitPathAndQuery(fullPath) if path == "" { - return renderHome() + return renderHome(fullPath) } else if len(path) >= 38 { // 39? 40? if path[:2] != "g1" { return "invalid address " + path @@ -323,12 +325,26 @@ func Render(path string) string { } } -func renderHome() string { +func renderHome(path string) string { doc := "" - name2User.Iterate("", "", func(key string, value interface{}) bool { - user := value.(*users.User) + + page := pager.NewPager(&name2User, 50).MustGetPageByPath(path) + + for _, item := range page.Items { + user := item.Value.(*users.User) doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" - return false - }) + } + doc += "\n" + doc += page.Selector() return doc } + +func splitPathAndQuery(fullPath string) (string, string) { + parts := strings.SplitN(fullPath, "?", 2) + path := parts[0] + queryString := "" + if len(parts) > 1 { + queryString = "?" + parts[1] + } + return path, queryString +} diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno index 31e482b7388..2b3e1b17b5c 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -28,6 +28,8 @@ func main() { users.Register(caller, "satoshi", "my other profile") println(users.Render("")) println("========================================") + println(users.Render("?page=2")) + println("========================================") println(users.Render("gnouser")) println("========================================") println(users.Render("satoshi")) @@ -49,6 +51,10 @@ func main() { // * [test1](/r/demo/users:test1) // * [x](/r/demo/users:x) // +// +// ======================================== +// +// // ======================================== // ## user gnouser // From 9129e4e973220c401be70e179b9eb3acad09b48f Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 7 Nov 2024 15:56:54 +0100 Subject: [PATCH 216/345] fix(gnovm): don't print to stdout by default (#3076) Attempt to fix #3075 cc/ @zivkovicmilos @sw360cab --- contribs/gnodev/pkg/dev/node.go | 2 ++ gno.land/pkg/gnoland/app.go | 5 ++++- gno.land/pkg/gnoland/node_inmemory.go | 3 +++ gno.land/pkg/sdk/vm/keeper.go | 28 ++++++++++++++++----------- gnovm/pkg/gnolang/machine.go | 3 +-- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 54baa2ea774..9b3f838b8a0 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "os" "path/filepath" "strings" "sync" @@ -576,5 +577,6 @@ func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesi PrivValidator: pv, TMConfig: tmc, Genesis: genesis, + VMOutput: os.Stdout, } } diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index ff1b5a88fea..d29ae3fd181 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -3,6 +3,7 @@ package gnoland import ( "fmt" + "io" "log/slog" "path/filepath" "strconv" @@ -36,10 +37,11 @@ type AppOptions struct { DB dbm.DB // required Logger *slog.Logger // required EventSwitch events.EventSwitch // required + VMOutput io.Writer // optional InitChainerConfig // options related to InitChainer } -// DefaultAppOptions provides a "ready" default [AppOptions] for use with +// TestAppOptions provides a "ready" default [AppOptions] for use with // [NewAppWithOptions], using the provided db. func TestAppOptions(db dbm.DB) *AppOptions { return &AppOptions{ @@ -91,6 +93,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { bankKpr := bank.NewBankKeeper(acctKpr) paramsKpr := params.NewParamsKeeper(mainKey, "vm") vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) + vmk.Output = cfg.VMOutput // Set InitChainer icc := cfg.InitChainerConfig diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 9dccbbfac8d..426a8c780c7 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -2,6 +2,7 @@ package gnoland import ( "fmt" + "io" "log/slog" "path/filepath" "time" @@ -23,6 +24,7 @@ type InMemoryNodeConfig struct { Genesis *bft.GenesisDoc TMConfig *tmcfg.Config DB *memdb.MemDB // will be initialized if nil + VMOutput io.Writer // optional // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") InitChainerConfig @@ -107,6 +109,7 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, DB: cfg.DB, EventSwitch: evsw, InitChainerConfig: cfg.InitChainerConfig, + VMOutput: cfg.VMOutput, }) if err != nil { return nil, fmt.Errorf("error initializing new app: %w", err) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 7f5216a4f03..5921e3eb3bb 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -6,8 +6,8 @@ import ( "bytes" "context" "fmt" + "io" "log/slog" - "os" "path/filepath" "regexp" "strings" @@ -57,6 +57,9 @@ var _ VMKeeperI = &VMKeeper{} // VMKeeper holds all package code and store state. type VMKeeper struct { + // Needs to be explicitly set, like in the case of gnodev. + Output io.Writer + baseKey store.StoreKey iavlKey store.StoreKey acck auth.AccountKeeper @@ -108,7 +111,7 @@ func (vm *VMKeeper) Initialize( m2 := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", - Output: os.Stdout, // XXX + Output: vm.Output, Store: vm.gnoStore, }) defer m2.Release() @@ -191,8 +194,7 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { m := gno.NewMachineWithOptions(gno.MachineOptions{ PkgPath: "gno.land/r/stdlibs/" + pkgPath, // PkgPath: pkgPath, XXX why? - Output: os.Stdout, - Store: store, + Store: store, }) defer m.Release() m.RunMemPackage(memPkg, true) @@ -275,7 +277,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", - Output: os.Stdout, // XXX + Output: vm.Output, Store: store, Context: msgCtx, Alloc: store.GetAllocator(), @@ -376,7 +378,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { m2 := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", - Output: os.Stdout, // XXX + Output: vm.Output, Store: gnostore, Alloc: gnostore.GetAllocator(), Context: msgCtx, @@ -477,7 +479,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", - Output: os.Stdout, // XXX + Output: vm.Output, Store: gnostore, Context: msgCtx, Alloc: gnostore.GetAllocator(), @@ -574,10 +576,14 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { } // Parse and run the files, construct *PV. buf := new(bytes.Buffer) + output := io.Writer(buf) + if vm.Output != nil { + output = io.MultiWriter(buf, vm.Output) + } m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", - Output: buf, + Output: output, Store: gnostore, Alloc: gnostore.GetAllocator(), Context: msgCtx, @@ -603,7 +609,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { m2 := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", - Output: buf, + Output: output, Store: gnostore, Alloc: gnostore.GetAllocator(), Context: msgCtx, @@ -735,7 +741,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: pkgPath, - Output: os.Stdout, // XXX + Output: vm.Output, Store: gnostore, Context: msgCtx, Alloc: alloc, @@ -802,7 +808,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: pkgPath, - Output: os.Stdout, // XXX + Output: vm.Output, Store: gnostore, Context: msgCtx, Alloc: alloc, diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 09be71682a5..33bf32730c5 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "os" "reflect" "slices" "strconv" @@ -144,7 +143,7 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { output := opts.Output if output == nil { - output = os.Stdout + output = io.Discard } alloc := opts.Alloc if alloc == nil { From 7ef606ce42b175d74b16ba3dd631fdb8926ab9d7 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:40:17 +0100 Subject: [PATCH 217/345] fix(gnovm): prevent assignment to non-assignable expressions (#2896) closes: #2889
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--- gnovm/pkg/gnolang/preprocess.go | 3 +++ gnovm/pkg/gnolang/type_check.go | 35 +++++++++++++++++++++++++++++++++ gnovm/tests/files/assign29.gno | 8 ++++++++ gnovm/tests/files/assign30.gno | 8 ++++++++ gnovm/tests/files/assign31.gno | 9 +++++++++ gnovm/tests/files/var31.gno | 8 ++++++++ gnovm/tests/files/var32.gno | 8 ++++++++ gnovm/tests/files/var33.gno | 9 +++++++++ 8 files changed, 88 insertions(+) create mode 100644 gnovm/tests/files/assign29.gno create mode 100644 gnovm/tests/files/assign30.gno create mode 100644 gnovm/tests/files/assign31.gno create mode 100644 gnovm/tests/files/var31.gno create mode 100644 gnovm/tests/files/var32.gno create mode 100644 gnovm/tests/files/var33.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a7a1e15bbf3..78e4488b2a0 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -1961,6 +1961,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *AssignStmt: n.AssertCompatible(store, last) + // NOTE: keep DEFINE and ASSIGN in sync. if n.Op == DEFINE { // Rhs consts become default *ConstExprs. @@ -2291,6 +2292,8 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *ValueDecl: + assertValidAssignRhs(store, last, n) + // evaluate value if const expr. if n.Const { // NOTE: may or may not be a *ConstExpr, diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index f86c44e7921..c62d67375ee 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -837,6 +837,7 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) { if x.Op == ASSIGN || x.Op == DEFINE { + assertValidAssignRhs(store, last, x) if len(x.Lhs) > len(x.Rhs) { if len(x.Rhs) != 1 { panic(fmt.Sprintf("assignment mismatch: %d variables but %d values", len(x.Lhs), len(x.Rhs))) @@ -997,6 +998,40 @@ func assertValidAssignLhs(store Store, last BlockNode, lx Expr) { } } +func assertValidAssignRhs(store Store, last BlockNode, n Node) { + var exps []Expr + switch x := n.(type) { + case *ValueDecl: + exps = x.Values + case *AssignStmt: + exps = x.Rhs + default: + panic(fmt.Sprintf("unexpected node type %T", n)) + } + + for _, exp := range exps { + tt := evalStaticTypeOfRaw(store, last, exp) + if tt == nil { + switch x := n.(type) { + case *ValueDecl: + if x.Type != nil { + continue + } + panic("use of untyped nil in variable declaration") + case *AssignStmt: + if x.Op != DEFINE { + continue + } + panic("use of untyped nil in assignment") + } + } + if _, ok := tt.(*TypeType); ok { + tt = evalStaticType(store, last, exp) + panic(fmt.Sprintf("%s (type) is not an expression", tt.String())) + } + } +} + func kindString(xt Type) string { if xt != nil { return xt.Kind().String() diff --git a/gnovm/tests/files/assign29.gno b/gnovm/tests/files/assign29.gno new file mode 100644 index 00000000000..ca605f5ecbe --- /dev/null +++ b/gnovm/tests/files/assign29.gno @@ -0,0 +1,8 @@ +package main + +func main() { + t := struct{} +} + +// Error: +// main/files/assign29.gno:4:2: struct{} (type) is not an expression diff --git a/gnovm/tests/files/assign30.gno b/gnovm/tests/files/assign30.gno new file mode 100644 index 00000000000..ad72f880f27 --- /dev/null +++ b/gnovm/tests/files/assign30.gno @@ -0,0 +1,8 @@ +package main + +func main() { + t := *struct{} +} + +// Error: +// main/files/assign30.gno:4:2: *struct{} (type) is not an expression diff --git a/gnovm/tests/files/assign31.gno b/gnovm/tests/files/assign31.gno new file mode 100644 index 00000000000..8c96c04501e --- /dev/null +++ b/gnovm/tests/files/assign31.gno @@ -0,0 +1,9 @@ +package main + +func main() { + a := nil + println(a) +} + +// Error: +// main/files/assign31.gno:4:2: use of untyped nil in assignment diff --git a/gnovm/tests/files/var31.gno b/gnovm/tests/files/var31.gno new file mode 100644 index 00000000000..813e6ff9e22 --- /dev/null +++ b/gnovm/tests/files/var31.gno @@ -0,0 +1,8 @@ +package main + +func main() { + var t = struct{} +} + +// Error: +// main/files/var31.gno:4:6: struct{} (type) is not an expression diff --git a/gnovm/tests/files/var32.gno b/gnovm/tests/files/var32.gno new file mode 100644 index 00000000000..827c3951f94 --- /dev/null +++ b/gnovm/tests/files/var32.gno @@ -0,0 +1,8 @@ +package main + +func main() { + var t = nil +} + +// Error: +// main/files/var32.gno:4:6: use of untyped nil in variable declaration diff --git a/gnovm/tests/files/var33.gno b/gnovm/tests/files/var33.gno new file mode 100644 index 00000000000..ce883dce47c --- /dev/null +++ b/gnovm/tests/files/var33.gno @@ -0,0 +1,9 @@ +package main + +func main() { + var t *int = nil + println("pass") +} + +// Output: +// pass From d73b6c6d5b594266d2c6e4d48f9b4d431ac2b507 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:06:53 +0100 Subject: [PATCH 218/345] ci: add go caching (#3091) `actions/setup-go` requires `actions/checkout` to read the `go.sum` file. ![CleanShot 2024-11-08 at 11 55 28@2x](https://github.com/user-attachments/assets/9014b6f4-d415-487a-a624-d5879e69d2c2) Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/workflows/build_template.yml | 6 +++--- .github/workflows/lint_template.yml | 6 +++--- .github/workflows/test_template.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_template.yml b/.github/workflows/build_template.yml index 430aa393a73..a2c96f2d37e 100644 --- a/.github/workflows/build_template.yml +++ b/.github/workflows/build_template.yml @@ -12,14 +12,14 @@ jobs: generated: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - - name: Check generated files are up to date working-directory: ${{ inputs.modulepath }} run: | diff --git a/.github/workflows/lint_template.yml b/.github/workflows/lint_template.yml index 65679633240..5b792269c02 100644 --- a/.github/workflows/lint_template.yml +++ b/.github/workflows/lint_template.yml @@ -13,16 +13,16 @@ jobs: lint: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - name: Lint uses: golangci/golangci-lint-action@v6 with: working-directory: ${{ inputs.modulepath }} args: --config=${{ github.workspace }}/.github/golangci.yml - version: v1.59 # sync with misc/devdeps \ No newline at end of file + version: v1.59 # sync with misc/devdeps diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml index 38fa10e096b..97b675e5531 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -18,12 +18,12 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - name: Go test working-directory: ${{ inputs.modulepath }} env: From da79c846c949169188361635ec58439d0f3f6227 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:06:57 +0100 Subject: [PATCH 219/345] test(ci): coverpkg=gno.land/... for txtar tests (#3088) Note: I'm uncertain about what will happen after the merge. Fixes #3085 Addresses #3003 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- .github/workflows/gnoland.yml | 1 + .github/workflows/main_template.yml | 6 +++++- .github/workflows/test_template.yml | 17 ++++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 9451d6da3a1..4817e2db0e3 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -13,5 +13,6 @@ jobs: uses: ./.github/workflows/main_template.yml with: modulepath: "gno.land" + tests-extra-args: "-coverpkg=github.com/gnolang/gno/gno.land/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/main_template.yml b/.github/workflows/main_template.yml index 8efb0277816..5b3437b54a1 100644 --- a/.github/workflows/main_template.yml +++ b/.github/workflows/main_template.yml @@ -4,6 +4,9 @@ on: modulepath: required: true type: string + tests-extra-args: + required: false + type: string secrets: codecov-token: required: true @@ -32,6 +35,7 @@ jobs: modulepath: ${{ inputs.modulepath }} tests-timeout: "30m" go-version: "1.22.x" + tests-extra-args: ${{ inputs.tests-extra-args }} secrets: codecov-token: ${{ secrets.codecov-token }} - \ No newline at end of file + diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml index 97b675e5531..b032718ff62 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -5,11 +5,14 @@ on: required: true type: string tests-timeout: - required: true - type: string + required: true + type: string go-version: - required: true - type: string + required: true + type: string + tests-extra-args: + required: false + type: string secrets: codecov-token: required: true @@ -23,7 +26,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go-version }} + go-version: ${{ inputs.go-version }} - name: Go test working-directory: ${{ inputs.modulepath }} env: @@ -42,7 +45,7 @@ jobs: # confusing and meticulous. There will be some improvements in Go # 1.23 regarding coverage, so we can use this as a workaround until # then. - go test -covermode=atomic -timeout ${{ inputs.tests-timeout }} ./... -test.gocoverdir=$GOCOVERDIR + go test -covermode=atomic -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR # Print results (set +x; echo 'go coverage results:') @@ -70,7 +73,7 @@ jobs: # - name: Install Go # uses: actions/setup-go@v5 # with: - # go-version: ${{ inputs.go-version }} + # go-version: ${{ inputs.go-version }} # - name: Checkout code # uses: actions/checkout@v4 # - name: Go race test From 4f27a57230320b77d577dd1ab3c773db6189908c Mon Sep 17 00:00:00 2001 From: n0izn0iz Date: Fri, 8 Nov 2024 18:09:14 +0100 Subject: [PATCH 220/345] fix(cmd/gno/clean): allow to run `gno clean -modcache` from anywhere + rename and use `gnomod.ModCachePath` + tmp `GNOHOME` in main tests (#3083) - In `gno clean`, run the `-modcache` case before checking for presence of a `gno.mod` to allow to run `gno clean -modcache` from anywhere (like go) - Refactor `gno clean -modcache` to use the `gnomod.GetGnoModPath` helper to get the modcache path - Rename `gnomod.GetGnoModPath` -> `gnomod.ModCachePath` - Improve `gno` cmd tests by using a tmp `GNOHOME` instead of the system one
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: Norman Meier --- gnovm/cmd/gno/clean.go | 27 ++++++++++++++------------- gnovm/cmd/gno/clean_test.go | 11 +++++++++++ gnovm/cmd/gno/env_test.go | 6 +++--- gnovm/cmd/gno/main_test.go | 8 ++++++++ gnovm/cmd/gno/mod.go | 2 +- gnovm/pkg/gnomod/gnomod.go | 10 +++++----- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/gnovm/cmd/gno/clean.go b/gnovm/cmd/gno/clean.go index 19a73c51794..0ca2e940d58 100644 --- a/gnovm/cmd/gno/clean.go +++ b/gnovm/cmd/gno/clean.go @@ -9,7 +9,6 @@ import ( "path/filepath" "strings" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -55,7 +54,7 @@ func (c *cleanCfg) RegisterFlags(fs *flag.FlagSet) { &c.modCache, "modcache", false, - "remove the entire module download cache", + "remove the entire module download cache and exit", ) } @@ -64,6 +63,19 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error { return flag.ErrHelp } + if cfg.modCache { + modCacheDir := gnomod.ModCachePath() + if !cfg.dryRun { + if err := os.RemoveAll(modCacheDir); err != nil { + return err + } + } + if cfg.dryRun || cfg.verbose { + io.Println("rm -rf", modCacheDir) + } + return nil + } + path, err := os.Getwd() if err != nil { return err @@ -81,17 +93,6 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error { return err } - if cfg.modCache { - modCacheDir := filepath.Join(gnoenv.HomeDir(), "pkg", "mod") - if !cfg.dryRun { - if err := os.RemoveAll(modCacheDir); err != nil { - return err - } - } - if cfg.dryRun || cfg.verbose { - io.Println("rm -rf", modCacheDir) - } - } return nil } diff --git a/gnovm/cmd/gno/clean_test.go b/gnovm/cmd/gno/clean_test.go index cfca2655031..401d0c87ddc 100644 --- a/gnovm/cmd/gno/clean_test.go +++ b/gnovm/cmd/gno/clean_test.go @@ -32,6 +32,17 @@ func TestCleanApp(t *testing.T) { testDir: "../../tests/integ/minimalist_gnomod", simulateExternalRepo: true, }, + { + args: []string{"clean", "-modcache"}, + testDir: "../../tests/integ/empty_dir", + simulateExternalRepo: true, + }, + { + args: []string{"clean", "-modcache", "-n"}, + testDir: "../../tests/integ/empty_dir", + simulateExternalRepo: true, + stdoutShouldContain: "rm -rf ", + }, } testMainCaseRun(t, tc) diff --git a/gnovm/cmd/gno/env_test.go b/gnovm/cmd/gno/env_test.go index 8aeb84ab2cc..b15658ed4f5 100644 --- a/gnovm/cmd/gno/env_test.go +++ b/gnovm/cmd/gno/env_test.go @@ -18,13 +18,13 @@ func TestEnvApp(t *testing.T) { {args: []string{"env", "foo"}, stdoutShouldBe: "\n"}, {args: []string{"env", "foo", "bar"}, stdoutShouldBe: "\n\n"}, {args: []string{"env", "GNOROOT"}, stdoutShouldBe: testGnoRootEnv + "\n"}, - {args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n"}, + {args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n", noTmpGnohome: true}, {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOROOT=%q", testGnoRootEnv)}, - {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv)}, + {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv), noTmpGnohome: true}, // json {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOROOT\": %q", testGnoRootEnv)}, - {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv)}, + {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv), noTmpGnohome: true}, { args: []string{"env", "-json", "GNOROOT"}, stdoutShouldBe: fmt.Sprintf("{\n\t\"GNOROOT\": %q\n}\n", testGnoRootEnv), diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go index 069c42db379..76c67f6807b 100644 --- a/gnovm/cmd/gno/main_test.go +++ b/gnovm/cmd/gno/main_test.go @@ -26,6 +26,7 @@ type testMainCase struct { args []string testDir string simulateExternalRepo bool + noTmpGnohome bool // for the following FooContain+FooBe expected couples, if both are empty, // then the test suite will require that the "got" is not empty. @@ -58,6 +59,13 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { mockOut := bytes.NewBufferString("") mockErr := bytes.NewBufferString("") + if !test.noTmpGnohome { + tmpGnoHome, err := os.MkdirTemp(os.TempDir(), "gnotesthome_") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpGnoHome) }) + t.Setenv("GNOHOME", tmpGnoHome) + } + checkOutputs := func(t *testing.T) { t.Helper() diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 03b2bb348a8..67af5631c71 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -177,7 +177,7 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { } // fetch dependencies - if err := gnoMod.FetchDeps(gnomod.GetGnoModPath(), cfg.remote, cfg.verbose); err != nil { + if err := gnoMod.FetchDeps(gnomod.ModCachePath(), cfg.remote, cfg.verbose); err != nil { return fmt.Errorf("fetch: %w", err) } diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index 553bb32f4b5..9384c41c293 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -19,20 +19,20 @@ import ( const queryPathFile = "vm/qfile" -// GetGnoModPath returns the path for gno modules -func GetGnoModPath() string { +// ModCachePath returns the path for gno modules +func ModCachePath() string { return filepath.Join(gnoenv.HomeDir(), "pkg", "mod") } // PackageDir resolves a given module.Version to the path on the filesystem. -// If root is dir, it is defaulted to the value of [GetGnoModPath]. +// If root is dir, it is defaulted to the value of [ModCachePath]. func PackageDir(root string, v module.Version) string { // This is also used internally exactly like filepath.Join; but we'll keep // the calls centralized to make sure we can change the path centrally should // we start including the module version in the path. if root == "" { - root = GetGnoModPath() + root = ModCachePath() } return filepath.Join(root, v.Path) } @@ -89,7 +89,7 @@ func writePackage(remote, basePath, pkgPath string) (requirements []string, err func GnoToGoMod(f File) (*File, error) { // TODO(morgan): good candidate to move to pkg/transpiler. - gnoModPath := GetGnoModPath() + gnoModPath := ModCachePath() if !gnolang.IsStdlib(f.Module.Mod.Path) { f.AddModuleStmt(transpiler.TranspileImportPath(f.Module.Mod.Path)) From 5f85d50e7dc88f6290192bc8f71669b8ccac43dc Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 12 Nov 2024 03:18:23 +0100 Subject: [PATCH 221/345] feat: add p/moul/realmpath (#3094) Lightweight `Render.path` parsing and link generation library with an idiomatic API, closely resembling that of `net/url`. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/realmpath/gno.mod | 6 + .../gno.land/p/moul/realmpath/realmpath.gno | 100 ++++++++++++ .../p/moul/realmpath/realmpath_test.gno | 151 ++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 examples/gno.land/p/moul/realmpath/gno.mod create mode 100644 examples/gno.land/p/moul/realmpath/realmpath.gno create mode 100644 examples/gno.land/p/moul/realmpath/realmpath_test.gno diff --git a/examples/gno.land/p/moul/realmpath/gno.mod b/examples/gno.land/p/moul/realmpath/gno.mod new file mode 100644 index 00000000000..e391b76390f --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/moul/realmpath + +require ( + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest +) diff --git a/examples/gno.land/p/moul/realmpath/realmpath.gno b/examples/gno.land/p/moul/realmpath/realmpath.gno new file mode 100644 index 00000000000..c46c97b4bed --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/realmpath.gno @@ -0,0 +1,100 @@ +// Package realmpath is a lightweight Render.path parsing and link generation +// library with an idiomatic API, closely resembling that of net/url. +// +// This package provides utilities for parsing request paths and query +// parameters, allowing you to extract path segments and manipulate query +// values. +// +// Example usage: +// +// import "gno.land/p/moul/realmpath" +// +// func Render(path string) string { +// // Parsing a sample path with query parameters +// path = "hello/world?foo=bar&baz=foobar" +// req := realmpath.Parse(path) +// +// // Accessing parsed path and query parameters +// println(req.Path) // Output: hello/world +// println(req.PathPart(0)) // Output: hello +// println(req.PathPart(1)) // Output: world +// println(req.Query.Get("foo")) // Output: bar +// println(req.Query.Get("baz")) // Output: foobar +// +// // Rebuilding the URL +// println(req.String()) // Output: /r/current/realm:hello/world?baz=foobar&foo=bar +// } +package realmpath + +import ( + "net/url" + "std" + "strings" +) + +const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) + +// Request represents a parsed request. +type Request struct { + Path string // The path of the request + Query url.Values // The parsed query parameters + Realm string // The realm associated with the request +} + +// Parse takes a raw path string and returns a Request object. +// It splits the path into its components and parses any query parameters. +func Parse(rawPath string) *Request { + // Split the raw path into path and query components + path, query := splitPathAndQuery(rawPath) + + // Parse the query string into url.Values + queryValues, _ := url.ParseQuery(query) + + return &Request{ + Path: path, // Set the path + Query: queryValues, // Set the parsed query values + } +} + +// PathParts returns the segments of the path as a slice of strings. +// It trims leading and trailing slashes and splits the path by slashes. +func (r *Request) PathParts() []string { + return strings.Split(strings.Trim(r.Path, "/"), "/") +} + +// PathPart returns the specified part of the path. +// If the index is out of bounds, it returns an empty string. +func (r *Request) PathPart(index int) string { + parts := r.PathParts() // Get the path segments + if index < 0 || index >= len(parts) { + return "" // Return empty if index is out of bounds + } + return parts[index] // Return the specified path part +} + +// String rebuilds the URL from the path and query values. +// If the Realm is not set, it automatically retrieves the current realm path. +func (r *Request) String() string { + // Automatically set the Realm if it is not already defined + if r.Realm == "" { + r.Realm = std.CurrentRealm().PkgPath() // Get the current realm path + } + + // Rebuild the path using the realm and path parts + relativePkgPath := strings.TrimPrefix(r.Realm, chainDomain) // Trim the chain domain prefix + reconstructedPath := relativePkgPath + ":" + strings.Join(r.PathParts(), "/") + + // Rebuild the query string + queryString := r.Query.Encode() // Encode the query parameters + if queryString != "" { + return reconstructedPath + "?" + queryString // Return the full URL with query + } + return reconstructedPath // Return the path without query parameters +} + +func splitPathAndQuery(rawPath string) (string, string) { + if idx := strings.Index(rawPath, "?"); idx != -1 { + return rawPath[:idx], rawPath[idx+1:] // Split at the first '?' found + } + return rawPath, "" // No query string present +} diff --git a/examples/gno.land/p/moul/realmpath/realmpath_test.gno b/examples/gno.land/p/moul/realmpath/realmpath_test.gno new file mode 100644 index 00000000000..a638b40d3ca --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/realmpath_test.gno @@ -0,0 +1,151 @@ +package realmpath_test + +import ( + "net/url" + "std" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/moul/realmpath" +) + +func TestExample(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + + // initial parsing + path := "hello/world?foo=bar&baz=foobar" + req := realmpath.Parse(path) + urequire.False(t, req == nil, "req should not be nil") + uassert.Equal(t, req.Path, "hello/world") + uassert.Equal(t, req.Query.Get("foo"), "bar") + uassert.Equal(t, req.Query.Get("baz"), "foobar") + uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar") + + // alter query + req.Query.Set("hey", "salut") + uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar&hey=salut") + + // alter path + req.Path = "bye/ciao" + uassert.Equal(t, req.String(), "/r/lorem/ipsum:bye/ciao?baz=foobar&foo=bar&hey=salut") +} + +func TestParse(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + + tests := []struct { + rawPath string + realm string // optional + expectedPath string + expectedQuery url.Values + expectedString string + }{ + { + rawPath: "hello/world?foo=bar&baz=foobar", + expectedPath: "hello/world", + expectedQuery: url.Values{ + "foo": []string{"bar"}, + "baz": []string{"foobar"}, + }, + expectedString: "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar", + }, + { + rawPath: "api/v1/resource?search=test&limit=10", + expectedPath: "api/v1/resource", + expectedQuery: url.Values{ + "search": []string{"test"}, + "limit": []string{"10"}, + }, + expectedString: "/r/lorem/ipsum:api/v1/resource?limit=10&search=test", + }, + { + rawPath: "singlepath", + expectedPath: "singlepath", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:singlepath", + }, + { + rawPath: "path/with/trailing/slash/", + expectedPath: "path/with/trailing/slash/", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:path/with/trailing/slash", + }, + { + rawPath: "emptyquery?", + expectedPath: "emptyquery", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:emptyquery", + }, + { + rawPath: "path/with/special/characters/?key=val%20ue&anotherKey=with%21special%23chars", + expectedPath: "path/with/special/characters/", + expectedQuery: url.Values{ + "key": []string{"val ue"}, + "anotherKey": []string{"with!special#chars"}, + }, + expectedString: "/r/lorem/ipsum:path/with/special/characters?anotherKey=with%21special%23chars&key=val+ue", + }, + { + rawPath: "path/with/empty/key?keyEmpty&=valueEmpty", + expectedPath: "path/with/empty/key", + expectedQuery: url.Values{ + "keyEmpty": []string{""}, + "": []string{"valueEmpty"}, + }, + expectedString: "/r/lorem/ipsum:path/with/empty/key?=valueEmpty&keyEmpty=", + }, + { + rawPath: "path/with/multiple/empty/keys?=empty1&=empty2", + expectedPath: "path/with/multiple/empty/keys", + expectedQuery: url.Values{ + "": []string{"empty1", "empty2"}, + }, + expectedString: "/r/lorem/ipsum:path/with/multiple/empty/keys?=empty1&=empty2", + }, + { + rawPath: "path/with/percent-encoded/%20space?query=hello%20world", + expectedPath: "path/with/percent-encoded/%20space", // XXX: should we decode? + expectedQuery: url.Values{ + "query": []string{"hello world"}, + }, + expectedString: "/r/lorem/ipsum:path/with/percent-encoded/%20space?query=hello+world", + }, + { + rawPath: "path/with/very/long/query?key1=value1&key2=value2&key3=value3&key4=value4&key5=value5&key6=value6", + expectedPath: "path/with/very/long/query", + expectedQuery: url.Values{ + "key1": []string{"value1"}, + "key2": []string{"value2"}, + "key3": []string{"value3"}, + "key4": []string{"value4"}, + "key5": []string{"value5"}, + "key6": []string{"value6"}, + }, + expectedString: "/r/lorem/ipsum:path/with/very/long/query?key1=value1&key2=value2&key3=value3&key4=value4&key5=value5&key6=value6", + }, + { + rawPath: "custom/realm?foo=bar&baz=foobar", + realm: "gno.land/r/foo/bar", + expectedPath: "custom/realm", + expectedQuery: url.Values{ + "foo": []string{"bar"}, + "baz": []string{"foobar"}, + }, + expectedString: "/r/foo/bar:custom/realm?baz=foobar&foo=bar", + }, + } + + for _, tt := range tests { + t.Run(tt.rawPath, func(t *testing.T) { + req := realmpath.Parse(tt.rawPath) + req.Realm = tt.realm // set optional realm + urequire.False(t, req == nil, "req should not be nil") + uassert.Equal(t, req.Path, tt.expectedPath) + urequire.Equal(t, len(req.Query), len(tt.expectedQuery)) + uassert.Equal(t, req.Query.Encode(), tt.expectedQuery.Encode()) + // XXX: uassert.Equal(t, req.Query, tt.expectedQuery) + uassert.Equal(t, req.String(), tt.expectedString) + }) + } +} From 5b64aa9aecff327a715bcb91ef4dcac636bbb1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Tue, 12 Nov 2024 08:24:49 +0100 Subject: [PATCH 222/345] feat: add initial `test5.gno.land` deployment (#3092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR adds the initial `test5.gno.land` deployment params. List of transaction changes as opposed to current `master` examples: https://github.com/gnolang/gno/commit/2e9f5ce8ecc90ee81eb3ae41c06bab30ab926150 - **131 txs in the `genesis.json`** All validator entities added: - @r3v4s ✅ - @D4ryl00 ✅ - @sw360cab ✅ - @mazzy89 ✅ - @n0izn0iz ✅ - @albttx ✅ Release we will use for the initial `test5.gno.land` deployment: https://github.com/gnolang/gno/pkgs/container/gno%2Fgnoland/303668315?tag=chain-test5.0 Closes #3061
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: D4ryl00 Signed-off-by: Norman Meier Co-authored-by: Sergio Maria Matone Co-authored-by: Rémi BARBERO Co-authored-by: Blake <104744707+r3v4s@users.noreply.github.com> Co-authored-by: Salvatore Mazzarino Co-authored-by: n0izn0iz Co-authored-by: albttx --- misc/deployments/test5.gno.land/CHECKLIST.md | 8 + misc/deployments/test5.gno.land/README.md | 47 + misc/deployments/test5.gno.land/config.toml | 246 + misc/deployments/test5.gno.land/genesis.json | 5915 +++++++++++++++++ .../test5.gno.land/genesis_balances.txt | 83 + .../test5.gno.land/genesis_txs.jsonl | 131 + 6 files changed, 6430 insertions(+) create mode 100644 misc/deployments/test5.gno.land/CHECKLIST.md create mode 100644 misc/deployments/test5.gno.land/README.md create mode 100644 misc/deployments/test5.gno.land/config.toml create mode 100644 misc/deployments/test5.gno.land/genesis.json create mode 100644 misc/deployments/test5.gno.land/genesis_balances.txt create mode 100755 misc/deployments/test5.gno.land/genesis_txs.jsonl diff --git a/misc/deployments/test5.gno.land/CHECKLIST.md b/misc/deployments/test5.gno.land/CHECKLIST.md new file mode 100644 index 00000000000..a2706acd1d7 --- /dev/null +++ b/misc/deployments/test5.gno.land/CHECKLIST.md @@ -0,0 +1,8 @@ +# Checklist Test5 + +- [x] Collect all validators keys for genesis +- [x] Collect all balances for genesis +- [x] Generate Final Genesis File +- [X] Collect list of public peer nodes for configurations +- [ ] Generate Release Docker Images of Gnoland for test5 +- [ ] Change DNS entries diff --git a/misc/deployments/test5.gno.land/README.md b/misc/deployments/test5.gno.land/README.md new file mode 100644 index 00000000000..3dcbf79f2ec --- /dev/null +++ b/misc/deployments/test5.gno.land/README.md @@ -0,0 +1,47 @@ +# Overview + +This deployment folder contains minimal information needed to launch a full test5.gno.land validator node. + +## `genesis.json` + +The initial `genesis.json` validator set is consisted of 6 entities (17 validators in total): + +- Gno Core - the gno core team (**6 validators**) +- Gno DevX - the gno devX team (**4 validators**) +- AiB - the AiB DevOps team (**3 validators**) +- Onbloc - the [Onbloc](https://onbloc.xyz/) team (**2 validator**) +- Teritori - the [Teritori](https://teritori.com/) team (**1 validator**) +- Berty - the [Berty](https://berty.tech/) team (**1 validator**) + +Subsequent validators will be added through the governance mechanism in govdao, employing a preliminary simplified +version Proof of Contribution. + +The addresses premined belong to different faucet accounts, validator entities and implementation partners. + +## `config.toml` + +The `config.toml` located in this directory is a **_guideline_**, and not a definitive configuration on how +all nodes should be configured in the network. + +### Important params + +Some configuration params are required, while others are advised to be set. + +- `moniker` - the recognizable identifier of the node. +- `consensus.timeout_commit` - the timeout value after the consensus commit phase. ⚠️ **Required to be `1s`** ⚠️. +- `conseuns.peer_gossip_sleep_duration` - the timeout for peer gossip. ⚠️ **Required to be `10ms`** ⚠️. +- `mempool.size` - the maximum number of txs in the mempool. **Advised to be `10000`**. +- `p2p.laddr` - the listen address for P2P traffic, **specific to every node deployment**. It is advised to use a + reverse-proxy, and keep this value at `tcp://0.0.0.0:`. +- `p2p.max_num_outbound_peers` - the max number of outbound peer connections. **Advised to be `40`**. +- `p2p.persistent_peers` - the persistent peers. ⚠️ **Required to be + `g16384atcuf6ew3ufpwtvhymwfyl2aw390aq8jtt@gno-core-sen-01.test5.gnoteam.com:26656,g1ty443uhf6qr2n0gv3dkemr4slt96e5hnmx90qh@gno-core-sen-02.test5.gnoteam.com:26656,g19x2gsyn02fldtq44dpgtcq2dq28kszlf5jn2es@gno-core-sen-03.test5.gnoteam.com:26656,g12p9l546ah4qeenhum8v4m2dg92jxcsrfy67yww@163.172.33.181:26656,g1s40khr8fruvsp2e9tveqyfwgzrqw4fs9kr4hwc@3.18.33.75:26656,g1gdt4c8rs3l4gpmp0f840nj93sv59cag6hn00cd@3.133.216.128:26656,g18vg9lgndagym626q8jsgv2peyjatscykde3xju@devx-sen-1.test5.gnodevx.network:26656,g1fnwswr6p5nqfvusglv7g2vy0tzwt5npwe7stvv@devx-sen-2.test5.gnodevx.network:26656,g1q887j0vrwpg7admfn4n203u8k30rj8k84zxvn9@195.154.203.189:26656` + ** ⚠️. +- `p2p.pex` - if using a sentry node architecture, should be `false`. **If not, please set to `true`**. +- `p2p.external_address` - the advertised peer dial address. If empty, will use the same port as the `p2p.laddr`. This + value should be **changed to `{{ your_ip_address }}:26656`** +- `p2p.flush_throttle_timeout` - the timeout for flushing multiplex data. ⚠️ **Required to be `10ms`** ⚠️. +- `rpc.laddr` - the JSON-RPC listen address, **specific to every node deployment**. +- `telemetry.enabled` - flag indicating if telemetry should be turned on. **Advised to be `true`**. +- `telemetry.exporter_endpoint` - endpoint for the otel exported. ⚠️ **Required if `telemetry.enabled=true`** ⚠️. +- `telemetry.service_instance_id` - unique ID of the node telemetry instance, **specific to every node deployment**. diff --git a/misc/deployments/test5.gno.land/config.toml b/misc/deployments/test5.gno.land/config.toml new file mode 100644 index 00000000000..0b5d97e979e --- /dev/null +++ b/misc/deployments/test5.gno.land/config.toml @@ -0,0 +1,246 @@ +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# Database backend: goleveldb | boltdb +# * goleveldb (github.com/syndtr/goleveldb - most popular implementation) +# - pure go +# - stable +#* boltdb (uses etcd's fork of bolt - go.etcd.io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) +db_backend = "goleveldb" + +# Database directory +db_dir = "db" + +# If this node is many blocks behind the tip of the chain, FastSync +# allows them to catchup quickly by downloading blocks in parallel +# and verifying their commits +fast_sync = true + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter_peers = false +home = "" + +# A custom human readable name for this node +moniker = "artemis.local" # Change me! + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "secrets/node_key.json" + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +priv_validator_key_file = "secrets/priv_validator_key.json" + +# TCP or UNIX socket address for Tendermint to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + +# Path to the JSON file containing the last sign state of a validator +priv_validator_state_file = "secrets/priv_validator_state.json" + +# TCP or UNIX socket address for the profiling server to listen on +prof_laddr = "" + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the Tendermint binary +proxy_app = "tcp://127.0.0.1:26658" + +##### consensus configuration options ##### +[consensus] + +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" +home = "" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "10ms" # Do NOT change me, leave me at 10ms! +peer_query_maj23_sleep_duration = "2s" + +# Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) +skip_timeout_commit = false +timeout_commit = "1s" # Do NOT change me, leave me at 1s! +timeout_precommit = "1s" +timeout_precommit_delta = "500ms" +timeout_prevote = "1s" +timeout_prevote_delta = "500ms" +timeout_propose = "3s" +timeout_propose_delta = "500ms" +wal_file = "wal/cs.wal/wal" + +##### mempool configuration options ##### +[mempool] +broadcast = true + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache_size = 10000 +home = "" + +# Limit the total size of all txs in the mempool. +# This only accounts for raw transactions (e.g. given 1MB transactions and +# max_txs_bytes=5MB, mempool will only accept 5 transactions). +max_pending_txs_bytes = 1073741824 # ~1GB +recheck = true + +# Maximum number of transactions in the mempool +size = 10000 # Advised value is 10000 +wal_dir = "" + +##### peer to peer configuration options ##### +[p2p] + +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = false +dial_timeout = "3s" + +# Address to advertise to peers for them to dial +# If empty, will use the same port as the laddr, +# and will introspect on the listener or use UPnP +# to figure out the address. +external_address = "" # Change me! + +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "10ms" # Do NOT change me, leave me at 10ms! + +# Peer connection configuration. +handshake_timeout = "20s" +home = "" + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26656" # Change me! + +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 40 # Advised value is 40 + +# Maximum size of a message packet payload, in bytes +max_packet_msg_payload_size = 1024 + +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "g16384atcuf6ew3ufpwtvhymwfyl2aw390aq8jtt@gno-core-sen-01.test5.gnoteam.com:26656,g1ty443uhf6qr2n0gv3dkemr4slt96e5hnmx90qh@gno-core-sen-02.test5.gnoteam.com:26656,g19x2gsyn02fldtq44dpgtcq2dq28kszlf5jn2es@gno-core-sen-03.test5.gnoteam.com:26656,g12p9l546ah4qeenhum8v4m2dg92jxcsrfy67yww@163.172.33.181:26656,g1s40khr8fruvsp2e9tveqyfwgzrqw4fs9kr4hwc@3.18.33.75:26656,g1gdt4c8rs3l4gpmp0f840nj93sv59cag6hn00cd@3.133.216.128:26656,g18vg9lgndagym626q8jsgv2peyjatscykde3xju@devx-sen-1.test5.gnodevx.network:26656,g1fnwswr6p5nqfvusglv7g2vy0tzwt5npwe7stvv@devx-sen-2.test5.gnodevx.network:26656,g1q887j0vrwpg7admfn4n203u8k30rj8k84zxvn9@195.154.203.189:26656" + +# Set true to enable the peer-exchange reactor +pex = false # Should be `false` if using a sentry node. Otherwise `true`! + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "" + +# Rate at which packets can be received, in bytes/second +recv_rate = 5120000 + +# Seed mode, in which node constantly crawls the network and looks for +# peers. If another node asks it for addresses, it responds and disconnects. +# +# Does not work if the peer-exchange reactor is disabled. +seed_mode = false + +# Comma separated list of seed nodes to connect to +seeds = "" + +# Rate at which packets can be sent, in bytes/second +send_rate = 5120000 +test_dial_fail = false +test_fuzz = false + +# UPNP port forwarding +upnp = false + +[p2p.test_fuzz_config] +MaxDelay = "3s" +Mode = 0 +ProbDropConn = 0.0 +ProbDropRW = 0.2 +ProbSleep = 0.0 + +##### rpc server configuration options ##### +[rpc] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", "OPTIONS"] + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = ["*"] + +# TCP or UNIX socket address for the gRPC server to listen on +# NOTE: This server only supports /broadcast_tx_commit +grpc_laddr = "" + +# Maximum number of simultaneous connections. +# Does not include RPC (HTTP&WebSocket) connections. See max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +grpc_max_open_connections = 900 +home = "" + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://0.0.0.0:26657" # Please use a reverse proxy! + +# Maximum size of request body, in bytes +max_body_bytes = 1000000 + +# Maximum size of request header, in bytes +max_header_bytes = 1048576 + +# Maximum number of simultaneous connections (including WebSocket). +# Does not include gRPC connections. See grpc_max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max_open_connections = 900 + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/classic/issues/3435 +timeout_broadcast_tx_commit = "10s" + +# The path to a file containing certificate that is used to create the HTTPS server. +# Might be either absolute path or path related to tendermint's config directory. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. +tls_cert_file = "" + +# The path to a file containing matching private key that is used to create the HTTPS server. +# Might be either absolute path or path related to tendermint's config directory. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. +tls_key_file = "" + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = false + +##### node telemetry ##### +[telemetry] +enabled = true # Advised to be `true` + +# the endpoint to export metrics to, like a local OpenTelemetry collector +exporter_endpoint = "" # Change me to the OTEL endpoint! +meter_name = "test5.gno.land" + +# the ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service) +service_instance_id = "gno-node-1" +service_name = "gno.land" + +##### event store ##### +[tx_event_store] + +# Type of event store +event_store_type = "none" + +# Event store parameters +[tx_event_store.event_store_params] diff --git a/misc/deployments/test5.gno.land/genesis.json b/misc/deployments/test5.gno.land/genesis.json new file mode 100644 index 00000000000..c149dac2444 --- /dev/null +++ b/misc/deployments/test5.gno.land/genesis.json @@ -0,0 +1,5915 @@ +{ + "genesis_time": "2024-11-12T08:00:00Z", + "chain_id": "test5", + "consensus_params": { + "Block": { + "MaxTxBytes": "1000000", + "MaxDataBytes": "2000000", + "MaxBlockBytes": "0", + "MaxGas": "100000000", + "TimeIotaMS": "100" + }, + "Validator": { + "PubKeyTypeURLs": [ + "/tm.PubKeyEd25519" + ] + } + }, + "validators": [ + { + "address": "g1qn3jwvdpva622j3fyudqy65zstnqx2wnqhrs3s", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "zaBcr0biE2vRjIopHCLtDgte/5tKuCEdlBLvmfgRuZI=" + }, + "power": "1", + "name": "gnocore-val-01" + }, + { + "address": "g1gtu9czw9qavrtdnf936usvwjwyjz0x0jk243au", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "VI8ITXN0TXGvOBjCbu5Rmus5zzqn79ws7AQeIqr6t2o=" + }, + "power": "1", + "name": "gnocore-val-02" + }, + { + "address": "g19emxxnzzfa0pkffvthrss5drgccjnwj8mdme4f", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KOdOfBaohI3Vc4JXfn/IhNzu0xcHpQLLDUpdeONtV5k=" + }, + "power": "1", + "name": "gnocore-val-03" + }, + { + "address": "g1hyxtsgjr5zt06jcx4z0xenn3u442ad2xgzu7lp", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "kruC6RrR7xeto1qc9guTHFCtRylfZumW4ohSYdQLJvY=" + }, + "power": "1", + "name": "gnocore-val-04" + }, + { + "address": "g1l072ma0vfhx7s4vpevfvuxd6wzkv5ztt7gh99w", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "LYKKNTZyLdOaIvnek1yoSxqnIU4dEjh3Xd+wd4Ru2lc=" + }, + "power": "1", + "name": "gnocore-val-05" + }, + { + "address": "g1uwqd3284kuzm56auwyc9d87jf3953tp9pnt506", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "nNt5eD77biTXnPj4/qt0CA83qJfRbPJsYIGY8X0o+vA=" + }, + "power": "1", + "name": "gnocore-val-06" + }, + { + "address": "g1ut590acnamvhkrh4qz6dz9zt9e3hyu499u0gvl", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KRPAQ2SLZvQUrc9P2l/DCEH6okMX13bds5Ma/wOQgBM=" + }, + "power": "1", + "name": "berty-val-1" + }, + { + "address": "g1arkzjfrte9l97v9q2qye07v0lw07039gaa3hfy", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "2Y0npl9WoOk9pIs2Dwv9IjsNbiuCWw6srsPKRf4sUro=" + }, + "power": "1", + "name": "onbloc-val-1" + }, + { + "address": "g1x0m33nyne064xdx7tvlfcjwd4xkajjar6h523z", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "6h57Ku4O6NFK7SS53Zxm/2rJEq4zVeznKvphAei5Z5g=" + }, + "power": "1", + "name": "onbloc-val-2" + }, + { + "address": "g1mxguhd5zacar64txhfm0v7hhtph5wur5hx86vs", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "C0l3PwIhnCmIG3YiAa9jf/uuvZj1ob7lolasnEhDgBE=" + }, + "power": "1", + "name": "devx-val-1" + }, + { + "address": "g1t9ctfa468hn6czff8kazw08crazehcxaqa2uaa", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "wBqj3F9RgIJ12UqGq5okzHfVbbd6H5AF5y4as8Ovx00=" + }, + "power": "1", + "name": "devx-val-2" + }, + { + "address": "g1sll4rtvrepdyzcvg5ml0kjtl7fnwgcsxgg9s5q", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "/HUSFQ2i/azLfjsEdvQZ25MijKlWtrVgM2EvGPYQfi8=" + }, + "power": "1", + "name": "devx-val-3" + }, + { + "address": "g1aa5pp94eaextkump38766hpdra74xtfh805msv", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "ZPTP49G2+f0YPhHQbJz2SUKRLgvAl8T5d7SHNfkj7NI=" + }, + "power": "1", + "name": "devx-val-4" + }, + { + "address": "g1r2lwzu0y0na4686a0lz4f2zqxlffqkfm7lqqqp", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KBwS/hUFKZQ5OMsgJmA+HGYpMaWmRZ9jebigROovjDw=" + }, + "power": "1", + "name": "tori-val-1" + }, + { + "address": "g1ecdu2gwz9d46srrhpu7k60pnrquvle5z2a5nn0", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "THkdb/gDwFKygxeDjT10HSuE1kvUSkeNovFQr0iqMa4=" + }, + "power": "1", + "name": "aib-val-01" + }, + { + "address": "g169wsuqlrscnvxtsu6wrc0zuwn39tmctw7q9f0q", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "s1avVD0bSF+yAPn/ow3+WKKzd/AyxhLng4gq/8Ho51w=" + }, + "power": "1", + "name": "aib-val-02" + }, + { + "address": "g1hfwh3ufph3zczs5wu4qvpgtv79fzh30rgzdux8", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "GNEDRfKnX85qpl127T/OSuZClLzAjnUV1Ojp9QQgvhM=" + }, + "power": "1", + "name": "aib-val-03" + } + ], + "app_hash": null, + "app_state": { + "@type": "/gno.GenesisState", + "balances": [ + "g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=9000000000000000000ugnot", + "g1mdy2f562he07a5txs8nvjelstur90e5sg5tkux=9000000000000000000ugnot", + "g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr=9000000000000000000ugnot", + "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=9000000000000000000ugnot", + "g1cx6s2rd4274vhvg509cwglw8senpq00ldqrntv=9000000000000000000ugnot", + "g1mpkp5lm8lwpm0pym4388836d009zfe4maxlqsq=9000000000000000000ugnot", + "g1yllclm55ls04dtemcwqgd0nyvyem0s8v6arwzt=9000000000000000000ugnot", + "g1x7rewh0w7u7yrmsmadq6w6t3jwh7ec6ql02klh=9000000000000000000ugnot", + "g1dnllrdzwfhxv3evyk09y48mgn5phfjvtyrlzm7=9000000000000000000ugnot", + "g1acn3xssksatydd0fcuslvgmjyw0fzkjdhusddg=9000000000000000000ugnot", + "g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun=9000000000000000000ugnot", + "g14vzc065ntj3rq3gfz9my3aja0yyezv7frmjsy3=9000000000000000000ugnot", + "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5=9000000000000000000ugnot", + "g1wmw2czwy260sydkupu53k6aeh6gxtf3e0egtku=9000000000000000000ugnot", + "g18amm3fc00t43dcxsys6udug0czyvqt9e7p23rd=9000000000000000000ugnot", + "g1er355fkjksqpdtwmhf5penwa82p0rhqxkkyhk5=9000000000000000000ugnot", + "g14vxq5e5pt5sev7rkz2ej438scmxtylnzv5vnkw=9000000000000000000ugnot", + "g1qrvwpcw0uxr22d8kgydfz3wp8rtl2h2l3lqmva=9000000000000000000ugnot", + "g1dvkfj5q79r3fnepqa0u5ym9d5l3dw83z203j02=9000000000000000000ugnot", + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=131000000ugnot", + "g1mx4pum9976th863jgry4sdjzfwu03qan5w2v9j=9000000000000000000ugnot", + "g1l8j7ts0gmghag7zmnatq5ta5xg83ylyxnmaxlh=9000000000000000000ugnot", + "g16tfrrul20g4jzt3z303raqw8vs8s2pqqh5clwu=9000000000000000000ugnot", + "g1jazghxvvgz3egnr2fc8uf72z4g0l03596y9ls7=9000000000000000000ugnot", + "g127l4gkhk0emwsx5tmxe96sp86c05h8vg5tufzq=9000000000000000000ugnot", + "g19p3yzr3cuhzqa02j0ce6kzvyjqfzwemw3vam0x=9000000000000000000ugnot", + "g16f5chytu99dmjqtekxf8qzg04vcv7dck6qny6d=9000000000000000000ugnot", + "g13fzhe4655aqdfr3flydd3pt9s0f4a775g96wj7=9000000000000000000ugnot", + "g1778y2yphxs2wpuaflsy5y9qwcd4gttn4g5yjx5=9000000000000000000ugnot", + "g1qhskthp2uycmg4zsdc9squ2jds7yv3t0qyrlnp=9000000000000000000ugnot", + "g1gu6wrz7xcavjtk2dudsfl586qrz5g4ahhhz2j3=9000000000000000000ugnot", + "g1cpx59z5r8vzeww2fm4ezpz7yvjs7kptywkm864=9000000000000000000ugnot", + "g1dfr24yhk5ztwtqn2a36m8f6ud8cx5hww4dkjfl=9000000000000000000ugnot", + "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj=9000000000000000000ugnot", + "g1a6jf5g6gkhn5rxcvwxq5zjxgwaznjr9r8gehey=9000000000000000000ugnot", + "g1q6jrp203fq0239pv38sdq3y3urvd6vt5azacpv=9000000000000000000ugnot", + "g197q5e9v00vuz256ly7fq7v3ekaun5cr7wmjgfh=9000000000000000000ugnot", + "g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3=9000000000000000000ugnot", + "g1ker4vvggvsyatexxn3hkthp2hu80pkhrwmuczr=9000000000000000000ugnot", + "g1g69npft5fav254rvuay7xlmlvt7ddfucgvx8xf=9000000000000000000ugnot", + "g1whzkakk4hzjkvy60d5pwfk484xu67ar2cl62h2=9000000000000000000ugnot", + "g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a=9000000000000000000ugnot", + "g1j40cmy9yefpwtesqzutc347d48uzk4428zu536=9000000000000000000ugnot", + "g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r=9000000000000000000ugnot", + "g18x425qmujg99cfz3q97y4uep5pxjq3z8lmpt25=9000000000000000000ugnot", + "g1e6gxg5tvc55mwsn7t7dymmlasratv7mkv0rap2=9000000000000000000ugnot", + "g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=9000000000000000000ugnot", + "g1pw4ju09ac9y0nj9lltglctk9zq7klk0tkttygk=9000000000000000000ugnot", + "g15ruzptpql4dpuyzej0wkt5rq6r26kw4nxu9fwd=9000000000000000000ugnot", + "g1qynsu9dwj9lq0m5fkje7jh6qy3md80ztqnshhm=9000000000000000000ugnot" + ], + "txs": [ + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "bank", + "path": "gno.land/p/demo/bank", + "files": [ + { + "name": "types.gno", + "body": "// TODO: this is an example, and needs to be fixed up and tested.\n\npackage bank\n\n// NOTE: unexposed struct for security.\ntype order struct {\n\tfrom Address\n\tto Address\n\tamount Coins\n\tprocessed bool\n}\n\n// NOTE: unexposed methods for security.\nfunc (ch *order) string() string {\n\treturn \"TODO\"\n}\n\n// Wraps the internal *order for external use.\ntype Order struct {\n\t*order\n}\n\n// XXX only exposed for demonstration. TODO unexpose, make full demo.\nfunc NewOrder(from Address, to Address, amount Coins) Order {\n\treturn Order{\n\t\torder: \u0026order{\n\t\t\tfrom: from,\n\t\t\tto: to,\n\t\t\tamount: amount,\n\t\t},\n\t}\n}\n\n// Panics if error, or already processed.\nfunc (o Order) Execute() {\n\tif o.order.processed {\n\t\tpanic(\"order already processed\")\n\t}\n\to.order.processed = true\n\t// TODO implemement.\n}\n\nfunc (o Order) IsZero() bool {\n\treturn o.order == nil\n}\n\nfunc (o Order) From() Address {\n\treturn o.order.from\n}\n\nfunc (o Order) To() Address {\n\treturn o.order.to\n}\n\nfunc (o Order) Amount() Coins {\n\treturn o.order.amount\n}\n\nfunc (o Order) Processed() bool {\n\treturn o.order.processed\n}\n\n//----------------------------------------\n// Escrow\n\ntype EscrowTerms struct {\n\tPartyA Address\n\tPartyB Address\n\tAmountA Coins\n\tAmountB Coins\n}\n\ntype EscrowContract struct {\n\tEscrowTerms\n\tOrderA Order\n\tOrderB Order\n}\n\nfunc CreateEscrow(terms EscrowTerms) *EscrowContract {\n\treturn \u0026EscrowContract{\n\t\tEscrowTerms: terms,\n\t}\n}\n\nfunc (esc *EscrowContract) SetOrderA(order Order) {\n\tif !esc.OrderA.IsZero() {\n\t\tpanic(\"order-a already set\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.From() {\n\t\tpanic(\"invalid order-a:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.To() {\n\t\tpanic(\"invalid order-a:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountA.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-a amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) SetOrderB(order Order) {\n\tif !esc.OrderB.IsZero() {\n\t\tpanic(\"order-b already set\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.From() {\n\t\tpanic(\"invalid order-b:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.To() {\n\t\tpanic(\"invalid order-b:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountB.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-b amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) Execute() {\n\tif esc.OrderA.IsZero() {\n\t\tpanic(\"order-a not yet set\")\n\t}\n\tif esc.OrderB.IsZero() {\n\t\tpanic(\"order-b not yet set\")\n\t}\n\t// NOTE: succeeds atomically.\n\tesc.OrderA.Execute()\n\tesc.OrderB.Execute()\n}\n\n//----------------------------------------\n// TODO: actually implement these in std package.\n\ntype (\n\tAddress string\n\tCoins []Coin\n\tCoin struct {\n\t\tDenom bool\n\t\tAmount int64\n\t}\n)\n\nfunc (a Coins) Equal(b Coins) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "avl", + "path": "gno.land/p/demo/avl", + "files": [ + { + "name": "node.gno", + "body": "package avl\n\n//----------------------------------------\n// Node\n\n// Node represents a node in an AVL tree.\ntype Node struct {\n\tkey string // key is the unique identifier for the node.\n\tvalue interface{} // value is the data stored in the node.\n\theight int8 // height is the height of the node in the tree.\n\tsize int // size is the number of nodes in the subtree rooted at this node.\n\tleftNode *Node // leftNode is the left child of the node.\n\trightNode *Node // rightNode is the right child of the node.\n}\n\n// NewNode creates a new node with the given key and value.\nfunc NewNode(key string, value interface{}) *Node {\n\treturn \u0026Node{\n\t\tkey: key,\n\t\tvalue: value,\n\t\theight: 0,\n\t\tsize: 1,\n\t}\n}\n\n// Size returns the size of the subtree rooted at the node.\nfunc (node *Node) Size() int {\n\tif node == nil {\n\t\treturn 0\n\t}\n\treturn node.size\n}\n\n// IsLeaf checks if the node is a leaf node (has no children).\nfunc (node *Node) IsLeaf() bool {\n\treturn node.height == 0\n}\n\n// Key returns the key of the node.\nfunc (node *Node) Key() string {\n\treturn node.key\n}\n\n// Value returns the value of the node.\nfunc (node *Node) Value() interface{} {\n\treturn node.value\n}\n\n// _copy creates a copy of the node (excluding value).\nfunc (node *Node) _copy() *Node {\n\tif node.height == 0 {\n\t\tpanic(\"Why are you copying a value node?\")\n\t}\n\treturn \u0026Node{\n\t\tkey: node.key,\n\t\theight: node.height,\n\t\tsize: node.size,\n\t\tleftNode: node.leftNode,\n\t\trightNode: node.rightNode,\n\t}\n}\n\n// Has checks if a node with the given key exists in the subtree rooted at the node.\nfunc (node *Node) Has(key string) (has bool) {\n\tif node == nil {\n\t\treturn false\n\t}\n\tif node.key == key {\n\t\treturn true\n\t}\n\tif node.height == 0 {\n\t\treturn false\n\t}\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Has(key)\n\t}\n\treturn node.getRightNode().Has(key)\n}\n\n// Get searches for a node with the given key in the subtree rooted at the node\n// and returns its index, value, and whether it exists.\nfunc (node *Node) Get(key string) (index int, value interface{}, exists bool) {\n\tif node == nil {\n\t\treturn 0, nil, false\n\t}\n\n\tif node.height == 0 {\n\t\tif node.key == key {\n\t\t\treturn 0, node.value, true\n\t\t}\n\t\tif node.key \u003c key {\n\t\t\treturn 1, nil, false\n\t\t}\n\t\treturn 0, nil, false\n\t}\n\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Get(key)\n\t}\n\n\trightNode := node.getRightNode()\n\tindex, value, exists = rightNode.Get(key)\n\tindex += node.size - rightNode.size\n\treturn index, value, exists\n}\n\n// GetByIndex retrieves the key-value pair of the node at the given index\n// in the subtree rooted at the node.\nfunc (node *Node) GetByIndex(index int) (key string, value interface{}) {\n\tif node.height == 0 {\n\t\tif index == 0 {\n\t\t\treturn node.key, node.value\n\t\t}\n\t\tpanic(\"GetByIndex asked for invalid index\")\n\t}\n\t// TODO: could improve this by storing the sizes\n\tleftNode := node.getLeftNode()\n\tif index \u003c leftNode.size {\n\t\treturn leftNode.GetByIndex(index)\n\t}\n\treturn node.getRightNode().GetByIndex(index - leftNode.size)\n}\n\n// Set inserts a new node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\n//\n// XXX consider a better way to do this... perhaps split Node from Node.\nfunc (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) {\n\tif node == nil {\n\t\treturn NewNode(key, value), false\n\t}\n\n\tif node.height == 0 {\n\t\treturn node.setLeaf(key, value)\n\t}\n\n\tnode = node._copy()\n\tif key \u003c node.key {\n\t\tnode.leftNode, updated = node.getLeftNode().Set(key, value)\n\t} else {\n\t\tnode.rightNode, updated = node.getRightNode().Set(key, value)\n\t}\n\n\tif updated {\n\t\treturn node, updated\n\t}\n\n\tnode.calcHeightAndSize()\n\treturn node.balance(), updated\n}\n\n// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\nfunc (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) {\n\tif key == node.key {\n\t\treturn NewNode(key, value), true\n\t}\n\n\tif key \u003c node.key {\n\t\treturn \u0026Node{\n\t\t\tkey: node.key,\n\t\t\theight: 1,\n\t\t\tsize: 2,\n\t\t\tleftNode: NewNode(key, value),\n\t\t\trightNode: node,\n\t\t}, false\n\t}\n\n\treturn \u0026Node{\n\t\tkey: key,\n\t\theight: 1,\n\t\tsize: 2,\n\t\tleftNode: node,\n\t\trightNode: NewNode(key, value),\n\t}, false\n}\n\n// Remove deletes the node with the given key from the subtree rooted at the node.\n// returns the new root of the subtree, the new leftmost leaf key (if changed),\n// the removed value and the removal was successful.\nfunc (node *Node) Remove(key string) (\n\tnewNode *Node, newKey string, value interface{}, removed bool,\n) {\n\tif node == nil {\n\t\treturn nil, \"\", nil, false\n\t}\n\tif node.height == 0 {\n\t\tif key == node.key {\n\t\t\treturn nil, \"\", node.value, true\n\t\t}\n\t\treturn node, \"\", nil, false\n\t}\n\tif key \u003c node.key {\n\t\tvar newLeftNode *Node\n\t\tnewLeftNode, newKey, value, removed = node.getLeftNode().Remove(key)\n\t\tif !removed {\n\t\t\treturn node, \"\", value, false\n\t\t}\n\t\tif newLeftNode == nil { // left node held value, was removed\n\t\t\treturn node.rightNode, node.key, value, true\n\t\t}\n\t\tnode = node._copy()\n\t\tnode.leftNode = newLeftNode\n\t\tnode.calcHeightAndSize()\n\t\tnode = node.balance()\n\t\treturn node, newKey, value, true\n\t}\n\n\tvar newRightNode *Node\n\tnewRightNode, newKey, value, removed = node.getRightNode().Remove(key)\n\tif !removed {\n\t\treturn node, \"\", value, false\n\t}\n\tif newRightNode == nil { // right node held value, was removed\n\t\treturn node.leftNode, \"\", value, true\n\t}\n\tnode = node._copy()\n\tnode.rightNode = newRightNode\n\tif newKey != \"\" {\n\t\tnode.key = newKey\n\t}\n\tnode.calcHeightAndSize()\n\tnode = node.balance()\n\treturn node, \"\", value, true\n}\n\n// getLeftNode returns the left child of the node.\nfunc (node *Node) getLeftNode() *Node {\n\treturn node.leftNode\n}\n\n// getRightNode returns the right child of the node.\nfunc (node *Node) getRightNode() *Node {\n\treturn node.rightNode\n}\n\n// rotateRight performs a right rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateRight() *Node {\n\tnode = node._copy()\n\tl := node.getLeftNode()\n\t_l := l._copy()\n\n\t_lrCached := _l.rightNode\n\t_l.rightNode = node\n\tnode.leftNode = _lrCached\n\n\tnode.calcHeightAndSize()\n\t_l.calcHeightAndSize()\n\n\treturn _l\n}\n\n// rotateLeft performs a left rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateLeft() *Node {\n\tnode = node._copy()\n\tr := node.getRightNode()\n\t_r := r._copy()\n\n\t_rlCached := _r.leftNode\n\t_r.leftNode = node\n\tnode.rightNode = _rlCached\n\n\tnode.calcHeightAndSize()\n\t_r.calcHeightAndSize()\n\n\treturn _r\n}\n\n// calcHeightAndSize updates the height and size of the node based on its children.\n// NOTE: mutates height and size\nfunc (node *Node) calcHeightAndSize() {\n\tnode.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1\n\tnode.size = node.getLeftNode().size + node.getRightNode().size\n}\n\n// calcBalance calculates the balance factor of the node.\nfunc (node *Node) calcBalance() int {\n\treturn int(node.getLeftNode().height) - int(node.getRightNode().height)\n}\n\n// balance balances the subtree rooted at the node and returns the new root.\n// NOTE: assumes that node can be modified\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) balance() (newSelf *Node) {\n\tbalance := node.calcBalance()\n\tif balance \u003e= -1 {\n\t\treturn node\n\t}\n\tif balance \u003e 1 {\n\t\tif node.getLeftNode().calcBalance() \u003e= 0 {\n\t\t\t// Left Left Case\n\t\t\treturn node.rotateRight()\n\t\t}\n\t\t// Left Right Case\n\t\tleft := node.getLeftNode()\n\t\tnode.leftNode = left.rotateLeft()\n\t\treturn node.rotateRight()\n\t}\n\n\tif node.getRightNode().calcBalance() \u003c= 0 {\n\t\t// Right Right Case\n\t\treturn node.rotateLeft()\n\t}\n\n\t// Right Left Case\n\tright := node.getRightNode()\n\tnode.rightNode = right.rotateRight()\n\treturn node.rotateLeft()\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) Iterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, true, true, cb)\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, false, true, cb)\n}\n\n// TraverseInRange traverses all nodes, including inner nodes.\n// Start is inclusive and end is exclusive when ascending,\n// Start and end are inclusive when descending.\n// Empty start and empty end denote no start and no end.\n// If leavesOnly is true, only visit leaf nodes.\n// NOTE: To simulate an exclusive reverse traversal,\n// just append 0x00 to start.\nfunc (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\tafterStart := (start == \"\" || start \u003c node.key)\n\tstartOrAfter := (start == \"\" || start \u003c= node.key)\n\tbeforeEnd := false\n\tif ascending {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c end)\n\t} else {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c= end)\n\t}\n\n\t// Run callback per inner/leaf node.\n\tstop := false\n\tif (!node.IsLeaf() \u0026\u0026 !leavesOnly) ||\n\t\t(node.IsLeaf() \u0026\u0026 startOrAfter \u0026\u0026 beforeEnd) {\n\t\tstop = cb(node)\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t}\n\tif node.IsLeaf() {\n\t\treturn stop\n\t}\n\n\tif ascending {\n\t\t// check lower nodes, then higher\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t} else {\n\t\t// check the higher nodes first\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t}\n\n\treturn stop\n}\n\n// TraverseByOffset traverses all nodes, including inner nodes.\n// A limit of math.MaxInt means no limit.\nfunc (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\n\t// fast paths. these happen only if TraverseByOffset is called directly on a leaf.\n\tif limit \u003c= 0 || offset \u003e= node.size {\n\t\treturn false\n\t}\n\tif node.IsLeaf() {\n\t\tif offset \u003e 0 {\n\t\t\treturn false\n\t\t}\n\t\treturn cb(node)\n\t}\n\n\t// go to the actual recursive function.\n\treturn node.traverseByOffset(offset, limit, descending, leavesOnly, cb)\n}\n\n// TraverseByOffset traverses the subtree rooted at the node by offset and limit,\n// in either ascending or descending order, and applies the callback function to each traversed node.\n// If leavesOnly is true, only leaf nodes are visited.\nfunc (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\t// caller guarantees: offset \u003c node.size; limit \u003e 0.\n\tif !leavesOnly {\n\t\tif cb(node) {\n\t\t\treturn true\n\t\t}\n\t}\n\tfirst, second := node.getLeftNode(), node.getRightNode()\n\tif descending {\n\t\tfirst, second = second, first\n\t}\n\tif first.IsLeaf() {\n\t\t// either run or skip, based on offset\n\t\tif offset \u003e 0 {\n\t\t\toffset--\n\t\t} else {\n\t\t\tcb(first)\n\t\t\tlimit--\n\t\t\tif limit \u003c= 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// possible cases:\n\t\t// 1 the offset given skips the first node entirely\n\t\t// 2 the offset skips none or part of the first node, but the limit requires some of the second node.\n\t\t// 3 the offset skips none or part of the first node, and the limit stops our search on the first node.\n\t\tif offset \u003e= first.size {\n\t\t\toffset -= first.size // 1\n\t\t} else {\n\t\t\tif first.traverseByOffset(offset, limit, descending, leavesOnly, cb) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// number of leaves which could actually be called from inside\n\t\t\tdelta := first.size - offset\n\t\t\toffset = 0\n\t\t\tif delta \u003e= limit {\n\t\t\t\treturn true // 3\n\t\t\t}\n\t\t\tlimit -= delta // 2\n\t\t}\n\t}\n\n\t// because of the caller guarantees and the way we handle the first node,\n\t// at this point we know that limit \u003e 0 and there must be some values in\n\t// this second node that we include.\n\n\t// =\u003e if the second node is a leaf, it has to be included.\n\tif second.IsLeaf() {\n\t\treturn cb(second)\n\t}\n\t// =\u003e if it is not a leaf, it will still be enough to recursively call this\n\t// function with the updated offset and limit\n\treturn second.traverseByOffset(offset, limit, descending, leavesOnly, cb)\n}\n\n// Only used in testing...\nfunc (node *Node) lmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getLeftNode().lmd()\n}\n\n// Only used in testing...\nfunc (node *Node) rmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getRightNode().rmd()\n}\n\nfunc maxInt8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n" + }, + { + "name": "node_test.gno", + "body": "package avl\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestTraverseByOffset(t *testing.T) {\n\tconst testStrings = `Alfa\nAlfred\nAlpha\nAlphabet\nBeta\nBeth\nBook\nBrowser`\n\ttt := []struct {\n\t\tname string\n\t\tdesc bool\n\t}{\n\t\t{\"ascending\", false},\n\t\t{\"descending\", true},\n\t}\n\n\tfor _, tt := range tt {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsl := strings.Split(testStrings, \"\\n\")\n\n\t\t\t// sort a first time in the order opposite to how we'll be traversing\n\t\t\t// the tree, to ensure that we are not just iterating through with\n\t\t\t// insertion order.\n\t\t\tsort.Strings(sl)\n\t\t\tif !tt.desc {\n\t\t\t\treverseSlice(sl)\n\t\t\t}\n\n\t\t\tr := NewNode(sl[0], nil)\n\t\t\tfor _, v := range sl[1:] {\n\t\t\t\tr, _ = r.Set(v, nil)\n\t\t\t}\n\n\t\t\t// then sort sl in the order we'll be traversing it, so that we can\n\t\t\t// compare the result with sl.\n\t\t\treverseSlice(sl)\n\n\t\t\tvar result []string\n\t\t\tfor i := 0; i \u003c len(sl); i++ {\n\t\t\t\tr.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif !slicesEqual(sl, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", sl, result)\n\t\t\t}\n\n\t\t\tfor l := 2; l \u003c= len(sl); l++ {\n\t\t\t\t// \"slices\"\n\t\t\t\tfor i := 0; i \u003c= len(sl); i++ {\n\t\t\t\t\tmax := i + l\n\t\t\t\t\tif max \u003e len(sl) {\n\t\t\t\t\t\tmax = len(sl)\n\t\t\t\t\t}\n\t\t\t\t\texp := sl[i:max]\n\t\t\t\t\tactual := []string{}\n\n\t\t\t\t\tr.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool {\n\t\t\t\t\t\tactual = append(actual, tr.Key())\n\t\t\t\t\t\treturn false\n\t\t\t\t\t})\n\t\t\t\t\tif !slicesEqual(exp, actual) {\n\t\t\t\t\t\tt.Errorf(\"want %v got %v\", exp, actual)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\thasKey string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"has key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"has key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"A\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"B\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tresult := tree.Has(tt.hasKey)\n\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tgetKey string\n\t\texpectIdx int\n\t\texpectVal interface{}\n\t\texpectExists bool\n\t}{\n\t\t{\n\t\t\t\"get existing key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (smaller)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"@\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (larger)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t5,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get from empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tidx, val, exists := tree.Get(tt.getKey)\n\n\t\t\tif idx != tt.expectIdx {\n\t\t\t\tt.Errorf(\"Expected index %d, got %d\", tt.expectIdx, idx)\n\t\t\t}\n\n\t\t\tif val != tt.expectVal {\n\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t}\n\n\t\t\tif exists != tt.expectExists {\n\t\t\t\tt.Errorf(\"Expected exists %t, got %t\", tt.expectExists, exists)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tidx int\n\t\texpectKey string\n\t\texpectVal interface{}\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\t\"get by valid index\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t2,\n\t\t\t\"C\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (smallest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t0,\n\t\t\t\"A\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (largest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t4,\n\t\t\t\"E\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (negative)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t-1,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (out of range)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t5,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tif tt.expectPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"Expected a panic but didn't get one\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tkey, val := tree.GetByIndex(tt.idx)\n\n\t\t\tif !tt.expectPanic {\n\t\t\t\tif key != tt.expectKey {\n\t\t\t\t\tt.Errorf(\"Expected key %s, got %s\", tt.expectKey, key)\n\t\t\t\t}\n\n\t\t\t\tif val != tt.expectVal {\n\t\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tremoveKey string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"remove leaf node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"B\",\n\t\t\t[]string{\"A\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with one child\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"A\",\n\t\t\t[]string{\"B\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with two children\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove root node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove non-existent key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree, _, _, _ = tree.Remove(tt.removeKey)\n\n\t\t\tresult := make([]string, 0)\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraverse(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"empty tree\",\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"single node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t[]string{\"A\"},\n\t\t},\n\t\t{\n\t\t\t\"small tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"large tree\",\n\t\t\t[]string{\"H\", \"D\", \"L\", \"B\", \"F\", \"J\", \"N\", \"A\", \"C\", \"E\", \"G\", \"I\", \"K\", \"M\", \"O\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tt.Run(\"iterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"ReverseIterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.ReverseIterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, len(tt.expected))\n\t\t\t\tcopy(expected, tt.expected)\n\t\t\t\tfor i, j := 0, len(expected)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\t\t\texpected[i], expected[j] = expected[j], expected[i]\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"TraverseInRange\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\tstart, end := \"C\", \"M\"\n\t\t\t\ttree.TraverseInRange(start, end, true, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, 0)\n\t\t\t\tfor _, key := range tt.expected {\n\t\t\t\t\tif key \u003e= start \u0026\u0026 key \u003c end {\n\t\t\t\t\t\texpected = append(expected, key)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestRotateWhenHeightDiffers(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation when left subtree is higher\",\n\t\t\t[]string{\"E\", \"C\", \"A\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation when right subtree is higher\",\n\t\t\t[]string{\"A\", \"C\", \"E\", \"D\", \"F\"},\n\t\t\t[]string{\"A\", \"C\", \"D\", \"E\", \"F\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"E\", \"A\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"A\", \"E\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\t// perform rotation or balance\n\t\t\ttree = tree.balance()\n\n\t\t\t// check tree structure\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRotateAndBalance(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation\",\n\t\t\t[]string{\"E\", \"D\", \"C\", \"B\", \"A\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"C\", \"A\", \"E\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"C\", \"E\", \"A\", \"D\", \"B\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree = tree.balance()\n\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc slicesEqual(w1, w2 []string) bool {\n\tif len(w1) != len(w2) {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(w1); i++ {\n\t\tif w1[0] != w2[0] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc maxint8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc reverseSlice(ss []string) {\n\tfor i := 0; i \u003c len(ss)/2; i++ {\n\t\tj := len(ss) - 1 - i\n\t\tss[i], ss[j] = ss[j], ss[i]\n\t}\n}\n" + }, + { + "name": "tree.gno", + "body": "package avl\n\ntype IterCbFn func(key string, value interface{}) bool\n\n//----------------------------------------\n// Tree\n\n// The zero struct can be used as an empty tree.\ntype Tree struct {\n\tnode *Node\n}\n\n// NewTree creates a new empty AVL tree.\nfunc NewTree() *Tree {\n\treturn \u0026Tree{\n\t\tnode: nil,\n\t}\n}\n\n// Size returns the number of key-value pair in the tree.\nfunc (tree *Tree) Size() int {\n\treturn tree.node.Size()\n}\n\n// Has checks whether a key exists in the tree.\n// It returns true if the key exists, otherwise false.\nfunc (tree *Tree) Has(key string) (has bool) {\n\treturn tree.node.Has(key)\n}\n\n// Get retrieves the value associated with the given key.\n// It returns the value and a boolean indicating whether the key exists.\nfunc (tree *Tree) Get(key string) (value interface{}, exists bool) {\n\t_, value, exists = tree.node.Get(key)\n\treturn\n}\n\n// GetByIndex retrieves the key-value pair at the specified index in the tree.\n// It returns the key and value at the given index.\nfunc (tree *Tree) GetByIndex(index int) (key string, value interface{}) {\n\treturn tree.node.GetByIndex(index)\n}\n\n// Set inserts a key-value pair into the tree.\n// If the key already exists, the value will be updated.\n// It returns a boolean indicating whether the key was newly inserted or updated.\nfunc (tree *Tree) Set(key string, value interface{}) (updated bool) {\n\tnewnode, updated := tree.node.Set(key, value)\n\ttree.node = newnode\n\treturn updated\n}\n\n// Remove removes a key-value pair from the tree.\n// It returns the removed value and a boolean indicating whether the key was found and removed.\nfunc (tree *Tree) Remove(key string) (value interface{}, removed bool) {\n\tnewnode, _, value, removed := tree.node.Remove(key)\n\ttree.node = newnode\n\treturn value, removed\n}\n\n// Iterate performs an in-order traversal of the tree within the specified key range.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) Iterate(start, end string, cb IterCbFn) bool {\n\treturn tree.node.TraverseInRange(start, end, true, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool {\n\treturn tree.node.TraverseInRange(start, end, false, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// IterateByOffset performs an in-order traversal of the tree starting from the specified offset.\n// It calls the provided callback function for each key-value pair encountered, up to the specified count.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool {\n\treturn tree.node.TraverseByOffset(offset, count, true, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset.\n// It calls the provided callback function for each key-value pair encountered, up to the specified count.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool {\n\treturn tree.node.TraverseByOffset(offset, count, false, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n" + }, + { + "name": "tree_test.gno", + "body": "package avl\n\nimport \"testing\"\n\nfunc TestNewTree(t *testing.T) {\n\ttree := NewTree()\n\tif tree.node != nil {\n\t\tt.Error(\"Expected tree.node to be nil\")\n\t}\n}\n\nfunc TestTreeSize(t *testing.T) {\n\ttree := NewTree()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tif tree.Size() != 2 {\n\t\tt.Error(\"Expected tree size to be 2\")\n\t}\n}\n\nfunc TestTreeHas(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tif !tree.Has(\"key1\") {\n\t\tt.Error(\"Expected tree to have key1\")\n\t}\n\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected tree to not have key2\")\n\t}\n}\n\nfunc TestTreeGet(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, exists := tree.Get(\"key1\")\n\tif !exists || value != \"value1\" {\n\t\tt.Error(\"Expected Get to return value1 and true\")\n\t}\n\n\t_, exists = tree.Get(\"key2\")\n\tif exists {\n\t\tt.Error(\"Expected Get to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeGetByIndex(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\n\tkey, value := tree.GetByIndex(0)\n\tif key != \"key1\" || value != \"value1\" {\n\t\tt.Error(\"Expected GetByIndex(0) to return key1 and value1\")\n\t}\n\n\tkey, value = tree.GetByIndex(1)\n\tif key != \"key2\" || value != \"value2\" {\n\t\tt.Error(\"Expected GetByIndex(1) to return key2 and value2\")\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected GetByIndex to panic for out-of-range index\")\n\t\t}\n\t}()\n\ttree.GetByIndex(2)\n}\n\nfunc TestTreeRemove(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, removed := tree.Remove(\"key1\")\n\tif !removed || value != \"value1\" || tree.Size() != 0 {\n\t\tt.Error(\"Expected Remove to remove key-value pair\")\n\t}\n\n\t_, removed = tree.Remove(\"key2\")\n\tif removed {\n\t\tt.Error(\"Expected Remove to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key1\", \"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key3\", \"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.IterateByOffset(1, 2, func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterateByOffset(1, 2, func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar node *avl.Node\n\nfunc init() {\n\tnode = avl.NewNode(\"key0\", \"value0\")\n\t// node, _ = node.Set(\"key0\", \"value0\")\n}\n\nfunc main() {\n\tvar updated bool\n\tnode, updated = node.Set(\"key1\", \"value1\")\n\t// println(node, updated)\n\tprintln(updated, node.Size())\n}\n\n// Output:\n// false 2\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\",\n// \"ModTime\": \"7\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"627e8e517e7ae5db0f3b753e2a32b607989198b6\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:5\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b28057ab7be6383785c0a5503e8a531bdbc21851\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:7]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"6da365f0d6cacbcdf53cd5a4b125803cddce08c2\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"f216afe7b5a17f4ebdbb98dceccedbc22e237596\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ff1a50d8489090af37a2c7766d659f0d717939b5\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={\n// \"Blank\": {},\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"5\",\n// \"RefCount\": \"2\"\n// },\n// \"Parent\": null,\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"0\",\n// \"File\": \"\",\n// \"Line\": \"0\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Values\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ae86874f9b47fa5e64c30b3e92e9d07f2ec967a4\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"init.1\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"10\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"main\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"15\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// }\n// ]\n// }\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar node *avl.Node\n\nfunc init() {\n\tnode = avl.NewNode(\"key0\", \"value0\")\n\tnode, _ = node.Set(\"key1\", \"value1\")\n}\n\nfunc main() {\n\tvar updated bool\n\tnode, updated = node.Set(\"key2\", \"value2\")\n\t// println(node, updated)\n\tprintln(updated, node.Size())\n}\n\n// Output:\n// false 3\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:14]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"143aebc820da33550f7338723fb1e2eec575b196\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:13]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"2f3adc5d0f2a3fe0331cfa93572a7abdde14c9aa\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"2e733a8e9e74fe14f0a5d10fb0f6728fa53d052d\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:12]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"fe20a19f956511f274dc77854e9e5468387260f4\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:11]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AwAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c89a71bdf045e8bde2059dc9d33839f916e02e5d\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"90fa67f8c47db4b9b2a60425dff08d5a3385100f\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:10]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"83e42caaf53070dd95b5f859053eb51ed900bbda\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={\n// \"Blank\": {},\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"9\",\n// \"RefCount\": \"2\"\n// },\n// \"Parent\": null,\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"0\",\n// \"File\": \"\",\n// \"Line\": \"0\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Values\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"1faa9fa4ba1935121a6d3f0a623772e9d4499b0a\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"init.1\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"10\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"main\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"15\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// }\n// ]\n// }\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar tree avl.Tree\n\nfunc init() {\n\ttree.Set(\"key0\", \"value0\")\n\ttree.Set(\"key1\", \"value1\")\n}\n\nfunc main() {\n\tvar updated bool\n\tupdated = tree.Set(\"key2\", \"value2\")\n\tprintln(updated, tree.Size())\n}\n\n// Output:\n// false 3\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:16]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:16\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"db333c89cd6773709e031f1f4e4ed4d3fed66c11\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:16\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:14]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"849a50d6c78d65742752e3c89ad8dd556e2e63cb\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b4fc2fdd2d0fe936c87ed2ace97136cffeed207f\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:13]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"a1160b0060ad752dbfe5fe436f7734bb19136150\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:12]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AwAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"fd95e08763159ac529e26986d652e752e78b6325\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"3ecdcf148fe2f9e97b72a3bedf303b2ba56d4f4b\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:11]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"63126557dba88f8556f7a0ccbbfc1d218ae7a302\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:3]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"d31c7e797793e03ffe0bbcb72f963264f8300d22\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\",\n// \"ModTime\": \"10\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// }\n// }\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "testutils", + "path": "gno.land/p/demo/testutils", + "files": [ + { + "name": "access.gno", + "body": "package testutils\n\n// for testing access. see tests/files/access*.go\n\n// NOTE: non-package variables cannot be overridden, except during init().\nvar (\n\tTestVar1 int\n\ttestVar2 int\n)\n\nfunc init() {\n\tTestVar1 = 123\n\ttestVar2 = 456\n}\n\ntype TestAccessStruct struct {\n\tPublicField string\n\tprivateField string\n}\n\nfunc (tas TestAccessStruct) PublicMethod() string {\n\treturn tas.PublicField + \"/\" + tas.privateField\n}\n\nfunc (tas TestAccessStruct) privateMethod() string {\n\treturn tas.PublicField + \"/\" + tas.privateField\n}\n\nfunc NewTestAccessStruct(pub, priv string) TestAccessStruct {\n\treturn TestAccessStruct{\n\t\tPublicField: pub,\n\t\tprivateField: priv,\n\t}\n}\n\n// see access6.g0 etc.\ntype PrivateInterface interface {\n\tprivateMethod() string\n}\n\nfunc PrintPrivateInterface(pi PrivateInterface) {\n\tprintln(\"testutils.PrintPrivateInterface\", pi.privateMethod())\n}\n" + }, + { + "name": "crypto.gno", + "body": "package testutils\n\nimport \"std\"\n\nfunc TestAddress(name string) std.Address {\n\tif len(name) \u003e std.RawAddressSize {\n\t\tpanic(\"address name cannot be greater than std.AddressSize bytes\")\n\t}\n\taddr := std.RawAddress{}\n\t// TODO: use strings.RepeatString or similar.\n\t// NOTE: I miss python's \"\".Join().\n\tblanks := \"____________________\"\n\tcopy(addr[:], []byte(blanks))\n\tcopy(addr[:], []byte(name))\n\treturn std.Address(std.EncodeBech32(\"g\", addr))\n}\n" + }, + { + "name": "misc.gno", + "body": "package testutils\n\n// For testing std.GetCallerAt().\nfunc WrapCall(fn func()) {\n\tfn()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "diff", + "path": "gno.land/p/demo/diff", + "files": [ + { + "name": "diff.gno", + "body": "// The diff package implements the Myers diff algorithm to compute the edit distance\n// and generate a minimal edit script between two strings.\n//\n// Edit distance, also known as Levenshtein distance, is a measure of the similarity\n// between two strings. It is defined as the minimum number of single-character edits (insertions,\n// deletions, or substitutions) required to change one string into the other.\npackage diff\n\nimport (\n\t\"strings\"\n)\n\n// EditType represents the type of edit operation in a diff.\ntype EditType uint8\n\nconst (\n\t// EditKeep indicates that a character is unchanged in both strings.\n\tEditKeep EditType = iota\n\n\t// EditInsert indicates that a character was inserted in the new string.\n\tEditInsert\n\n\t// EditDelete indicates that a character was deleted from the old string.\n\tEditDelete\n)\n\n// Edit represent a single edit operation in a diff.\ntype Edit struct {\n\t// Type is the kind of edit operation.\n\tType EditType\n\n\t// Char is the character involved in the edit operation.\n\tChar rune\n}\n\n// MyersDiff computes the difference between two strings using Myers' diff algorithm.\n// It returns a slice of Edit operations that transform the old string into the new string.\n// This implementation finds the shortest edit script (SES) that represents the minimal\n// set of operations to transform one string into the other.\n//\n// The function handles both ASCII and non-ASCII characters correctly.\n//\n// Time complexity: O((N+M)D), where N and M are the lengths of the input strings,\n// and D is the size of the minimum edit script.\n//\n// Space complexity: O((N+M)D)\n//\n// In the worst case, where the strings are completely different, D can be as large as N+M,\n// leading to a time and space complexity of O((N+M)^2). However, for strings with many\n// common substrings, the performance is much better, often closer to O(N+M).\n//\n// Parameters:\n// - old: the original string.\n// - new: the modified string.\n//\n// Returns:\n// - A slice of Edit operations representing the minimum difference between the two strings.\nfunc MyersDiff(old, new string) []Edit {\n\toldRunes, newRunes := []rune(old), []rune(new)\n\tn, m := len(oldRunes), len(newRunes)\n\n\tif n == 0 \u0026\u0026 m == 0 {\n\t\treturn []Edit{}\n\t}\n\n\t// old is empty\n\tif n == 0 {\n\t\tedits := make([]Edit, m)\n\t\tfor i, r := range newRunes {\n\t\t\tedits[i] = Edit{Type: EditInsert, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tif m == 0 {\n\t\tedits := make([]Edit, n)\n\t\tfor i, r := range oldRunes {\n\t\t\tedits[i] = Edit{Type: EditDelete, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tmax := n + m\n\tv := make([]int, 2*max+1)\n\tvar trace [][]int\nsearch:\n\tfor d := 0; d \u003c= max; d++ {\n\t\t// iterate through diagonals\n\t\tfor k := -d; k \u003c= d; k += 2 {\n\t\t\tvar x int\n\t\t\tif k == -d || (k != d \u0026\u0026 v[max+k-1] \u003c v[max+k+1]) {\n\t\t\t\tx = v[max+k+1] // move down\n\t\t\t} else {\n\t\t\t\tx = v[max+k-1] + 1 // move right\n\t\t\t}\n\t\t\ty := x - k\n\n\t\t\t// extend the path as far as possible with matching characters\n\t\t\tfor x \u003c n \u0026\u0026 y \u003c m \u0026\u0026 oldRunes[x] == newRunes[y] {\n\t\t\t\tx++\n\t\t\t\ty++\n\t\t\t}\n\n\t\t\tv[max+k] = x\n\n\t\t\t// check if we've reached the end of both strings\n\t\t\tif x == n \u0026\u0026 y == m {\n\t\t\t\ttrace = append(trace, append([]int(nil), v...))\n\t\t\t\tbreak search\n\t\t\t}\n\t\t}\n\t\ttrace = append(trace, append([]int(nil), v...))\n\t}\n\n\t// backtrack to construct the edit script\n\tedits := make([]Edit, 0, n+m)\n\tx, y := n, m\n\tfor d := len(trace) - 1; d \u003e= 0; d-- {\n\t\tvPrev := trace[d]\n\t\tk := x - y\n\t\tvar prevK int\n\t\tif k == -d || (k != d \u0026\u0026 vPrev[max+k-1] \u003c vPrev[max+k+1]) {\n\t\t\tprevK = k + 1\n\t\t} else {\n\t\t\tprevK = k - 1\n\t\t}\n\t\tprevX := vPrev[max+prevK]\n\t\tprevY := prevX - prevK\n\n\t\t// add keep edits for matching characters\n\t\tfor x \u003e prevX \u0026\u0026 y \u003e prevY {\n\t\t\tif x \u003e 0 \u0026\u0026 y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditKeep, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t\ty--\n\t\t}\n\t\tif y \u003e prevY {\n\t\t\tif y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditInsert, Char: newRunes[y-1]}}, edits...)\n\t\t\t}\n\t\t\ty--\n\t\t} else if x \u003e prevX {\n\t\t\tif x \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditDelete, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t}\n\t}\n\n\treturn edits\n}\n\n// Format converts a slice of Edit operations into a human-readable string representation.\n// It groups consecutive edits of the same type and formats them as follows:\n// - Unchanged characters are left as-is\n// - Inserted characters are wrapped in [+...]\n// - Deleted characters are wrapped in [-...]\n//\n// This function is useful for visualizing the differences between two strings\n// in a compact and intuitive format.\n//\n// Parameters:\n// - edits: A slice of Edit operations, typically produced by MyersDiff\n//\n// Returns:\n// - A formatted string representing the diff\n//\n// Example output:\n//\n//\tFor the diff between \"abcd\" and \"acbd\", the output might be:\n//\t\"a[-b]c[+b]d\"\n//\n// Note:\n//\n//\tThe function assumes that the input slice of edits is in the correct order.\n//\tAn empty input slice will result in an empty string.\nfunc Format(edits []Edit) string {\n\tif len(edits) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar (\n\t\tresult strings.Builder\n\t\tcurrentType EditType\n\t\tcurrentChars strings.Builder\n\t)\n\n\tflushCurrent := func() {\n\t\tif currentChars.Len() \u003e 0 {\n\t\t\tswitch currentType {\n\t\t\tcase EditKeep:\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\tcase EditInsert:\n\t\t\t\tresult.WriteString(\"[+\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\tcase EditDelete:\n\t\t\t\tresult.WriteString(\"[-\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\t}\n\t\t\tcurrentChars.Reset()\n\t\t}\n\t}\n\n\tfor _, edit := range edits {\n\t\tif edit.Type != currentType {\n\t\t\tflushCurrent()\n\t\t\tcurrentType = edit.Type\n\t\t}\n\t\tcurrentChars.WriteRune(edit.Char)\n\t}\n\tflushCurrent()\n\n\treturn result.String()\n}\n" + }, + { + "name": "diff_test.gno", + "body": "package diff\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestMyersDiff(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\told string\n\t\tnew string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"No difference\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"abc\",\n\t\t},\n\t\t{\n\t\t\tname: \"Simple insertion\",\n\t\t\told: \"ac\",\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"a[+b]c\",\n\t\t},\n\t\t{\n\t\t\tname: \"Simple deletion\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"ac\",\n\t\t\texpected: \"a[-b]c\",\n\t\t},\n\t\t{\n\t\t\tname: \"Simple substitution\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"abd\",\n\t\t\texpected: \"ab[-c][+d]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple changes\",\n\t\t\told: \"The quick brown fox jumps over the lazy dog\",\n\t\t\tnew: \"The quick brown cat jumps over the lazy dog\",\n\t\t\texpected: \"The quick brown [-fox][+cat] jumps over the lazy dog\",\n\t\t},\n\t\t{\n\t\t\tname: \"Prefix and suffix\",\n\t\t\told: \"Hello, world!\",\n\t\t\tnew: \"Hello, beautiful world!\",\n\t\t\texpected: \"Hello, [+beautiful ]world!\",\n\t\t},\n\t\t{\n\t\t\tname: \"Complete change\",\n\t\t\told: \"abcdef\",\n\t\t\tnew: \"ghijkl\",\n\t\t\texpected: \"[-abcdef][+ghijkl]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Empty strings\",\n\t\t\told: \"\",\n\t\t\tnew: \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Old empty\",\n\t\t\told: \"\",\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"[+abc]\",\n\t\t},\n\t\t{\n\t\t\tname: \"New empty\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"\",\n\t\t\texpected: \"[-abc]\",\n\t\t},\n\t\t{\n\t\t\tname: \"non-ascii (Korean characters)\",\n\t\t\told: \"ASCII 문자가 아닌 것도 되나?\",\n\t\t\tnew: \"ASCII 문자가 아닌 것도 됨.\",\n\t\t\texpected: \"ASCII 문자가 아닌 것도 [-되나?][+됨.]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Emoji diff\",\n\t\t\told: \"Hello 👋 World 🌍\",\n\t\t\tnew: \"Hello 👋 Beautiful 🌸 World 🌍\",\n\t\t\texpected: \"Hello 👋 [+Beautiful 🌸 ]World 🌍\",\n\t\t},\n\t\t{\n\t\t\tname: \"Mixed multibyte and ASCII\",\n\t\t\told: \"こんにちは World\",\n\t\t\tnew: \"こんばんは World\",\n\t\t\texpected: \"こん[-にち][+ばん]は World\",\n\t\t},\n\t\t{\n\t\t\tname: \"Chinese characters\",\n\t\t\told: \"我喜欢编程\",\n\t\t\tnew: \"我喜欢看书和编程\",\n\t\t\texpected: \"我喜欢[+看书和]编程\",\n\t\t},\n\t\t{\n\t\t\tname: \"Combining characters\",\n\t\t\told: \"e\\u0301\", // é (e + ´)\n\t\t\tnew: \"e\\u0300\", // è (e + `)\n\t\t\texpected: \"e[-\\u0301][+\\u0300]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Right-to-Left languages\",\n\t\t\told: \"שלום\",\n\t\t\tnew: \"שלום עולם\",\n\t\t\texpected: \"שלום[+ עולם]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Normalization NFC and NFD\",\n\t\t\told: \"e\\u0301\", // NFD (decomposed)\n\t\t\tnew: \"\\u00e9\", // NFC (precomposed)\n\t\t\texpected: \"[-e\\u0301][+\\u00e9]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Case sensitivity\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"Abc\",\n\t\t\texpected: \"[-a][+A]bc\",\n\t\t},\n\t\t{\n\t\t\tname: \"Surrogate pairs\",\n\t\t\told: \"Hello 🌍\",\n\t\t\tnew: \"Hello 🌎\",\n\t\t\texpected: \"Hello [-🌍][+🌎]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Control characters\",\n\t\t\told: \"Line1\\nLine2\",\n\t\t\tnew: \"Line1\\r\\nLine2\",\n\t\t\texpected: \"Line1[+\\r]\\nLine2\",\n\t\t},\n\t\t{\n\t\t\tname: \"Mixed scripts\",\n\t\t\told: \"Hello नमस्ते こんにちは\",\n\t\t\tnew: \"Hello สวัสดี こんにちは\",\n\t\t\texpected: \"Hello [-नमस्ते][+สวัสดี] こんにちは\",\n\t\t},\n\t\t{\n\t\t\tname: \"Unicode normalization\",\n\t\t\told: \"é\", // U+00E9 (precomposed)\n\t\t\tnew: \"e\\u0301\", // U+0065 U+0301 (decomposed)\n\t\t\texpected: \"[-é][+e\\u0301]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Directional marks\",\n\t\t\told: \"Hello\\u200Eworld\", // LTR mark\n\t\t\tnew: \"Hello\\u200Fworld\", // RTL mark\n\t\t\texpected: \"Hello[-\\u200E][+\\u200F]world\",\n\t\t},\n\t\t{\n\t\t\tname: \"Zero-width characters\",\n\t\t\told: \"ab\\u200Bc\", // Zero-width space\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"ab[-\\u200B]c\",\n\t\t},\n\t\t{\n\t\t\tname: \"Worst-case scenario (completely different strings)\",\n\t\t\told: strings.Repeat(\"a\", 1000),\n\t\t\tnew: strings.Repeat(\"b\", 1000),\n\t\t\texpected: \"[-\" + strings.Repeat(\"a\", 1000) + \"][+\" + strings.Repeat(\"b\", 1000) + \"]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Very long strings\",\n\t\t\told: strings.Repeat(\"a\", 10000) + \"b\" + strings.Repeat(\"a\", 10000),\n\t\t\tnew: strings.Repeat(\"a\", 10000) + \"c\" + strings.Repeat(\"a\", 10000),\n\t\t\texpected: strings.Repeat(\"a\", 10000) + \"[-b][+c]\" + strings.Repeat(\"a\", 10000),\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdiff := MyersDiff(tc.old, tc.new)\n\t\t\tresult := Format(diff)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected: %s, got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "uassert", + "path": "gno.land/p/demo/uassert", + "files": [ + { + "name": "doc.gno", + "body": "package uassert // import \"gno.land/p/demo/uassert\"\n" + }, + { + "name": "helpers.gno", + "body": "package uassert\n\nimport \"strings\"\n\nfunc fail(t TestingT, customMsgs []string, failureMessage string, args ...interface{}) bool {\n\tcustomMsg := \"\"\n\tif len(customMsgs) \u003e 0 {\n\t\tcustomMsg = strings.Join(customMsgs, \" \")\n\t}\n\tif customMsg != \"\" {\n\t\tfailureMessage += \" - \" + customMsg\n\t}\n\tt.Errorf(failureMessage, args...)\n\treturn false\n}\n\nfunc autofail(t TestingT, success bool, customMsgs []string, failureMessage string, args ...interface{}) bool {\n\tif success {\n\t\treturn true\n\t}\n\treturn fail(t, customMsgs, failureMessage, args...)\n}\n\nfunc checkDidPanic(f func()) (didPanic bool, message string) {\n\tdidPanic = true\n\tdefer func() {\n\t\tr := recover()\n\n\t\tif r == nil {\n\t\t\tmessage = \"nil\"\n\t\t\treturn\n\t\t}\n\n\t\terr, ok := r.(error)\n\t\tif ok {\n\t\t\tmessage = err.Error()\n\t\t\treturn\n\t\t}\n\n\t\terrStr, ok := r.(string)\n\t\tif ok {\n\t\t\tmessage = errStr\n\t\t\treturn\n\t\t}\n\n\t\tmessage = \"recover: unsupported type\"\n\t}()\n\tf()\n\tdidPanic = false\n\treturn\n}\n" + }, + { + "name": "mock_test.gno", + "body": "package uassert\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype mockTestingT struct {\n\tfmt string\n\targs []interface{}\n}\n\n// --- interface mock\n\nvar _ TestingT = (*mockTestingT)(nil)\n\nfunc (mockT *mockTestingT) Helper() { /* noop */ }\nfunc (mockT *mockTestingT) Skip(args ...interface{}) { /* not implmented */ }\nfunc (mockT *mockTestingT) Fail() { /* not implmented */ }\nfunc (mockT *mockTestingT) FailNow() { /* not implmented */ }\nfunc (mockT *mockTestingT) Logf(fmt string, args ...interface{}) { /* noop */ }\n\nfunc (mockT *mockTestingT) Fatalf(fmt string, args ...interface{}) {\n\tmockT.fmt = \"fatal: \" + fmt\n\tmockT.args = args\n}\n\nfunc (mockT *mockTestingT) Errorf(fmt string, args ...interface{}) {\n\tmockT.fmt = \"error: \" + fmt\n\tmockT.args = args\n}\n\n// --- helpers\n\nfunc (mockT *mockTestingT) actualString() string {\n\tres := fmt.Sprintf(mockT.fmt, mockT.args...)\n\tmockT.reset()\n\treturn res\n}\n\nfunc (mockT *mockTestingT) reset() {\n\tmockT.fmt = \"\"\n\tmockT.args = nil\n}\n\nfunc (mockT *mockTestingT) equals(t *testing.T, expected string) {\n\tactual := mockT.actualString()\n\n\tif expected != actual {\n\t\tt.Errorf(\"mockT differs:\\n- expected: %s\\n- actual: %s\\n\", expected, actual)\n\t}\n}\n\nfunc (mockT *mockTestingT) empty(t *testing.T) {\n\tif mockT.fmt != \"\" || mockT.args != nil {\n\t\tactual := mockT.actualString()\n\t\tt.Errorf(\"mockT should be empty, got %s\", actual)\n\t}\n}\n" + }, + { + "name": "types.gno", + "body": "package uassert\n\ntype TestingT interface {\n\tHelper()\n\tSkip(args ...interface{})\n\tFatalf(fmt string, args ...interface{})\n\tErrorf(fmt string, args ...interface{})\n\tLogf(fmt string, args ...interface{})\n\tFail()\n\tFailNow()\n}\n" + }, + { + "name": "uassert.gno", + "body": "// uassert is an adapted lighter version of https://github.com/stretchr/testify/assert.\npackage uassert\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/diff\"\n)\n\n// NoError asserts that a function returned no error (i.e. `nil`).\nfunc NoError(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err != nil {\n\t\treturn fail(t, msgs, \"unexpected error: %s\", err.Error())\n\t}\n\treturn true\n}\n\n// Error asserts that a function returned an error (i.e. not `nil`).\nfunc Error(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err == nil {\n\t\treturn fail(t, msgs, \"an error is expected but got nil\")\n\t}\n\treturn true\n}\n\n// ErrorContains asserts that a function returned an error (i.e. not `nil`)\n// and that the error contains the specified substring.\nfunc ErrorContains(t TestingT, err error, contains string, msgs ...string) bool {\n\tt.Helper()\n\n\tif !Error(t, err, msgs...) {\n\t\treturn false\n\t}\n\n\tactual := err.Error()\n\tif !strings.Contains(actual, contains) {\n\t\treturn fail(t, msgs, \"error %q does not contain %q\", actual, contains)\n\t}\n\n\treturn true\n}\n\n// True asserts that the specified value is true.\nfunc True(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif !value {\n\t\treturn fail(t, msgs, \"should be true\")\n\t}\n\treturn true\n}\n\n// False asserts that the specified value is false.\nfunc False(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif value {\n\t\treturn fail(t, msgs, \"should be false\")\n\t}\n\treturn true\n}\n\n// ErrorIs asserts the given error matches the target error\nfunc ErrorIs(t TestingT, err, target error, msgs ...string) bool {\n\tt.Helper()\n\n\tif err == nil || target == nil {\n\t\treturn err == target\n\t}\n\n\t// XXX: if errors.Is(err, target) return true\n\n\tif err.Error() != target.Error() {\n\t\treturn fail(t, msgs, \"error mismatch, expected %s, got %s\", target.Error(), err.Error())\n\t}\n\n\treturn true\n}\n\n// PanicsWithMessage asserts that the code inside the specified func panics,\n// and that the recovered panic value satisfies the given message\nfunc PanicsWithMessage(t TestingT, msg string, f func(), msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\n\tif !didPanic {\n\t\treturn fail(t, msgs, \"func should panic\\n\\tPanic value:\\t%v\", panicValue)\n\t}\n\n\tif panicValue != msg {\n\t\treturn fail(t, msgs, \"func should panic with message:\\t%s\\n\\tPanic value:\\t%s\", msg, panicValue)\n\t}\n\treturn true\n}\n\n// NotPanics asserts that the code inside the specified func does NOT panic.\nfunc NotPanics(t TestingT, f func(), msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\n\n\tif didPanic {\n\t\treturn fail(t, msgs, \"func should not panic\\n\\tPanic value:\\t%s\", panicValue)\n\t}\n\treturn true\n}\n\n// Equal asserts that two objects are equal.\nfunc Equal(t TestingT, expected, actual interface{}, msgs ...string) bool {\n\tt.Helper()\n\n\tif expected == nil || actual == nil {\n\t\treturn expected == actual\n\t}\n\n\t// XXX: errors\n\t// XXX: slices\n\t// XXX: pointers\n\n\tequal := false\n\tok_ := false\n\tes, as := \"unsupported type\", \"unsupported type\"\n\n\tswitch ev := expected.(type) {\n\tcase string:\n\t\tif av, ok := actual.(string); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = ev, av\n\t\t\tif !equal {\n\t\t\t\tdif := diff.MyersDiff(ev, av)\n\t\t\t\treturn fail(t, msgs, \"uassert.Equal: strings are different\\n\\tDiff: %s\", diff.Format(dif))\n\t\t\t}\n\t\t}\n\tcase std.Address:\n\t\tif av, ok := actual.(std.Address); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = string(ev), string(av)\n\t\t}\n\tcase int:\n\t\tif av, ok := actual.(int); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(ev), strconv.Itoa(av)\n\t\t}\n\tcase int8:\n\t\tif av, ok := actual.(int8); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int16:\n\t\tif av, ok := actual.(int16); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int32:\n\t\tif av, ok := actual.(int32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int64:\n\t\tif av, ok := actual.(int64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase uint:\n\t\tif av, ok := actual.(uint); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint8:\n\t\tif av, ok := actual.(uint8); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint16:\n\t\tif av, ok := actual.(uint16); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint32:\n\t\tif av, ok := actual.(uint32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint64:\n\t\tif av, ok := actual.(uint64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)\n\t\t}\n\tcase bool:\n\t\tif av, ok := actual.(bool); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tif ev {\n\t\t\t\tes, as = \"true\", \"false\"\n\t\t\t} else {\n\t\t\t\tes, as = \"false\", \"true\"\n\t\t\t}\n\t\t}\n\tcase float32:\n\t\tif av, ok := actual.(float32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t}\n\tcase float64:\n\t\tif av, ok := actual.(float64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t}\n\tdefault:\n\t\treturn fail(t, msgs, \"uassert.Equal: unsupported type\")\n\t}\n\n\t/*\n\t\t// XXX: implement stringer and other well known similar interfaces\n\t\ttype stringer interface{ String() string }\n\t\tif ev, ok := expected.(stringer); ok {\n\t\t\tif av, ok := actual.(stringer); ok {\n\t\t\t\tequal = ev.String() == av.String()\n\t\t\t\tok_ = true\n\t\t\t}\n\t\t}\n\t*/\n\n\tif !ok_ {\n\t\treturn fail(t, msgs, \"uassert.Equal: different types\") // XXX: display the types\n\t}\n\tif !equal {\n\t\treturn fail(t, msgs, \"uassert.Equal: same type but different value\\n\\texpected: %s\\n\\tactual: %s\", es, as)\n\t}\n\n\treturn true\n}\n\n// NotEqual asserts that two objects are not equal.\nfunc NotEqual(t TestingT, expected, actual interface{}, msgs ...string) bool {\n\tt.Helper()\n\n\tif expected == nil || actual == nil {\n\t\treturn expected != actual\n\t}\n\n\t// XXX: errors\n\t// XXX: slices\n\t// XXX: pointers\n\n\tnotEqual := false\n\tok_ := false\n\tes, as := \"unsupported type\", \"unsupported type\"\n\n\tswitch ev := expected.(type) {\n\tcase string:\n\t\tif av, ok := actual.(string); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = ev, av\n\t\t}\n\tcase std.Address:\n\t\tif av, ok := actual.(std.Address); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = string(ev), string(av)\n\t\t}\n\tcase int:\n\t\tif av, ok := actual.(int); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(ev), strconv.Itoa(av)\n\t\t}\n\tcase int8:\n\t\tif av, ok := actual.(int8); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int16:\n\t\tif av, ok := actual.(int16); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int32:\n\t\tif av, ok := actual.(int32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int64:\n\t\tif av, ok := actual.(int64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase uint:\n\t\tif av, ok := actual.(uint); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint8:\n\t\tif av, ok := actual.(uint8); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint16:\n\t\tif av, ok := actual.(uint16); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint32:\n\t\tif av, ok := actual.(uint32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint64:\n\t\tif av, ok := actual.(uint64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)\n\t\t}\n\tcase bool:\n\t\tif av, ok := actual.(bool); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tif ev {\n\t\t\t\tes, as = \"true\", \"false\"\n\t\t\t} else {\n\t\t\t\tes, as = \"false\", \"true\"\n\t\t\t}\n\t\t}\n\tcase float32:\n\t\tif av, ok := actual.(float32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t}\n\tcase float64:\n\t\tif av, ok := actual.(float64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t}\n\tdefault:\n\t\treturn fail(t, msgs, \"uassert.NotEqual: unsupported type\")\n\t}\n\n\t/*\n\t\t// XXX: implement stringer and other well known similar interfaces\n\t\ttype stringer interface{ String() string }\n\t\tif ev, ok := expected.(stringer); ok {\n\t\t\tif av, ok := actual.(stringer); ok {\n\t\t\t\tnotEqual = ev.String() != av.String()\n\t\t\t\tok_ = true\n\t\t\t}\n\t\t}\n\t*/\n\n\tif !ok_ {\n\t\treturn fail(t, msgs, \"uassert.NotEqual: different types\") // XXX: display the types\n\t}\n\tif !notEqual {\n\t\treturn fail(t, msgs, \"uassert.NotEqual: same type and same value\\n\\texpected: %s\\n\\tactual: %s\", es, as)\n\t}\n\n\treturn true\n}\n\nfunc isNumberEmpty(n interface{}) (isNumber, isEmpty bool) {\n\tswitch n := n.(type) {\n\t// NOTE: the cases are split individually, so that n becomes of the\n\t// asserted type; the type of '0' was correctly inferred and converted\n\t// to the corresponding type, int, int8, etc.\n\tcase int:\n\t\treturn true, n == 0\n\tcase int8:\n\t\treturn true, n == 0\n\tcase int16:\n\t\treturn true, n == 0\n\tcase int32:\n\t\treturn true, n == 0\n\tcase int64:\n\t\treturn true, n == 0\n\tcase uint:\n\t\treturn true, n == 0\n\tcase uint8:\n\t\treturn true, n == 0\n\tcase uint16:\n\t\treturn true, n == 0\n\tcase uint32:\n\t\treturn true, n == 0\n\tcase uint64:\n\t\treturn true, n == 0\n\tcase float32:\n\t\treturn true, n == 0\n\tcase float64:\n\t\treturn true, n == 0\n\t}\n\treturn false, false\n}\nfunc Empty(t TestingT, obj interface{}, msgs ...string) bool {\n\tt.Helper()\n\n\tisNumber, isEmpty := isNumberEmpty(obj)\n\tif isNumber {\n\t\tif !isEmpty {\n\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty number: %d\", obj)\n\t\t}\n\t} else {\n\t\tswitch val := obj.(type) {\n\t\tcase string:\n\t\t\tif val != \"\" {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty string: %s\", val)\n\t\t\t}\n\t\tcase std.Address:\n\t\t\tvar zeroAddr std.Address\n\t\t\tif val != zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty std.Address: %s\", string(val))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fail(t, msgs, \"uassert.Empty: unsupported type\")\n\t\t}\n\t}\n\treturn true\n}\n\nfunc NotEmpty(t TestingT, obj interface{}, msgs ...string) bool {\n\tt.Helper()\n\tisNumber, isEmpty := isNumberEmpty(obj)\n\tif isNumber {\n\t\tif isEmpty {\n\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty number: %d\", obj)\n\t\t}\n\t} else {\n\t\tswitch val := obj.(type) {\n\t\tcase string:\n\t\t\tif val == \"\" {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty string: %s\", val)\n\t\t\t}\n\t\tcase std.Address:\n\t\t\tvar zeroAddr std.Address\n\t\t\tif val == zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty std.Address: %s\", string(val))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: unsupported type\")\n\t\t}\n\t}\n\treturn true\n}\n" + }, + { + "name": "uassert_test.gno", + "body": "package uassert\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"std\"\n\t\"testing\"\n)\n\nvar _ TestingT = (*testing.T)(nil)\n\nfunc TestMock(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tmockT.empty(t)\n\tNoError(mockT, errors.New(\"foo\"))\n\tmockT.equals(t, \"error: unexpected error: foo\")\n\tNoError(mockT, errors.New(\"foo\"), \"custom message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n\tNoError(mockT, errors.New(\"foo\"), \"custom\", \"message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n}\n\nfunc TestNoError(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tTrue(t, NoError(mockT, nil))\n\tmockT.empty(t)\n\tFalse(t, NoError(mockT, errors.New(\"foo bar\")))\n\tmockT.equals(t, \"error: unexpected error: foo bar\")\n}\n\nfunc TestError(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tTrue(t, Error(mockT, errors.New(\"foo bar\")))\n\tmockT.empty(t)\n\tFalse(t, Error(mockT, nil))\n\tmockT.equals(t, \"error: an error is expected but got nil\")\n}\n\nfunc TestErrorContains(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\t// nil error\n\tvar err error\n\tFalse(t, ErrorContains(mockT, err, \"\"), \"ErrorContains should return false for nil arg\")\n}\n\nfunc TestTrue(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !True(mockT, true) {\n\t\tt.Error(\"True should return true\")\n\t}\n\tmockT.empty(t)\n\tif True(mockT, false) {\n\t\tt.Error(\"True should return false\")\n\t}\n\tmockT.equals(t, \"error: should be true\")\n}\n\nfunc TestFalse(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !False(mockT, false) {\n\t\tt.Error(\"False should return true\")\n\t}\n\tmockT.empty(t)\n\tif False(mockT, true) {\n\t\tt.Error(\"False should return false\")\n\t}\n\tmockT.equals(t, \"error: should be false\")\n}\n\nfunc TestPanicsWithMessage(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !PanicsWithMessage(mockT, \"panic\", func() {\n\t\tpanic(errors.New(\"panic\"))\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif PanicsWithMessage(mockT, \"Panic!\", func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic\\n\\tPanic value:\\tnil\")\n\n\tif PanicsWithMessage(mockT, \"at the disco\", func() {\n\t\tpanic(errors.New(\"panic\"))\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message:\\tat the disco\\n\\tPanic value:\\tpanic\")\n\n\tif PanicsWithMessage(mockT, \"Panic!\", func() {\n\t\tpanic(\"panic\")\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message:\\tPanic!\\n\\tPanic value:\\tpanic\")\n}\n\nfunc TestNotPanics(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !NotPanics(mockT, func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"NotPanics should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif NotPanics(mockT, func() {\n\t\tpanic(\"Panic!\")\n\t}) {\n\t\tt.Error(\"NotPanics should return false\")\n\t}\n}\n\nfunc TestEqual(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\texpected interface{}\n\t\tactual interface{}\n\t\tresult bool\n\t\tremark string\n\t}{\n\t\t// expected to be equal\n\t\t{\"Hello World\", \"Hello World\", true, \"\"},\n\t\t{123, 123, true, \"\"},\n\t\t{123.5, 123.5, true, \"\"},\n\t\t{nil, nil, true, \"\"},\n\t\t{int32(123), int32(123), true, \"\"},\n\t\t{uint64(123), uint64(123), true, \"\"},\n\t\t{std.Address(\"g12345\"), std.Address(\"g12345\"), true, \"\"},\n\t\t// XXX: continue\n\n\t\t// not expected to be equal\n\t\t{\"Hello World\", 42, false, \"\"},\n\t\t{41, 42, false, \"\"},\n\t\t{10, uint(10), false, \"\"},\n\t\t// XXX: continue\n\n\t\t// expected to raise errors\n\t\t// XXX: todo\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"Equal(%v, %v)\", c.expected, c.actual)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := Equal(mockT, c.expected, c.actual)\n\n\t\t\tif res != c.result {\n\t\t\t\tt.Errorf(\"%s should return %v: %s - %s\", name, c.result, c.remark, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotEqual(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\texpected interface{}\n\t\tactual interface{}\n\t\tresult bool\n\t\tremark string\n\t}{\n\t\t// expected to be not equal\n\t\t{\"Hello World\", \"Hello\", true, \"\"},\n\t\t{123, 124, true, \"\"},\n\t\t{123.5, 123.6, true, \"\"},\n\t\t{nil, 123, true, \"\"},\n\t\t{int32(123), int32(124), true, \"\"},\n\t\t{uint64(123), uint64(124), true, \"\"},\n\t\t{std.Address(\"g12345\"), std.Address(\"g67890\"), true, \"\"},\n\t\t// XXX: continue\n\n\t\t// not expected to be not equal\n\t\t{\"Hello World\", \"Hello World\", false, \"\"},\n\t\t{123, 123, false, \"\"},\n\t\t{123.5, 123.5, false, \"\"},\n\t\t{nil, nil, false, \"\"},\n\t\t{int32(123), int32(123), false, \"\"},\n\t\t{uint64(123), uint64(123), false, \"\"},\n\t\t{std.Address(\"g12345\"), std.Address(\"g12345\"), false, \"\"},\n\t\t// XXX: continue\n\n\t\t// expected to raise errors\n\t\t// XXX: todo\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"NotEqual(%v, %v)\", c.expected, c.actual)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := NotEqual(mockT, c.expected, c.actual)\n\n\t\t\tif res != c.result {\n\t\t\t\tt.Errorf(\"%s should return %v: %s - %s\", name, c.result, c.remark, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype myStruct struct {\n\tS string\n\tI int\n}\n\nfunc TestEmpty(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\tobj interface{}\n\t\texpectedEmpty bool\n\t}{\n\t\t// expected to be empty\n\t\t{\"\", true},\n\t\t{0, true},\n\t\t{int(0), true},\n\t\t{int32(0), true},\n\t\t{int64(0), true},\n\t\t{uint(0), true},\n\t\t// XXX: continue\n\n\t\t// not expected to be empty\n\t\t{\"Hello World\", false},\n\t\t{1, false},\n\t\t{int32(1), false},\n\t\t{uint64(1), false},\n\t\t{std.Address(\"g12345\"), false},\n\n\t\t// unsupported\n\t\t{nil, false},\n\t\t{myStruct{}, false},\n\t\t{\u0026myStruct{}, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"Empty(%v)\", c.obj)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := Empty(mockT, c.obj)\n\n\t\t\tif res != c.expectedEmpty {\n\t\t\t\tt.Errorf(\"%s should return %v: %s\", name, c.expectedEmpty, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEqualWithStringDiff(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\texpected string\n\t\tactual string\n\t\tshouldPass bool\n\t\texpectedMsg string\n\t}{\n\t\t{\n\t\t\tname: \"Identical strings\",\n\t\t\texpected: \"Hello, world!\",\n\t\t\tactual: \"Hello, world!\",\n\t\t\tshouldPass: true,\n\t\t\texpectedMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - simple\",\n\t\t\texpected: \"Hello, world!\",\n\t\t\tactual: \"Hello, World!\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: Hello, [-w][+W]orld!\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - complex\",\n\t\t\texpected: \"The quick brown fox jumps over the lazy dog\",\n\t\t\tactual: \"The quick brown cat jumps over the lazy dog\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: The quick brown [-fox][+cat] jumps over the lazy dog\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - prefix\",\n\t\t\texpected: \"prefix_string\",\n\t\t\tactual: \"string\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [-prefix_]string\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - suffix\",\n\t\t\texpected: \"string\",\n\t\t\tactual: \"string_suffix\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: string[+_suffix]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Empty string vs non-empty string\",\n\t\t\texpected: \"\",\n\t\t\tactual: \"non-empty\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [+non-empty]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Non-empty string vs empty string\",\n\t\t\texpected: \"non-empty\",\n\t\t\tactual: \"\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [-non-empty]\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmockT := \u0026mockTestingT{}\n\t\t\tresult := Equal(mockT, tc.expected, tc.actual)\n\n\t\t\tif result != tc.shouldPass {\n\t\t\t\tt.Errorf(\"Expected Equal to return %v, but got %v\", tc.shouldPass, result)\n\t\t\t}\n\n\t\t\tif tc.shouldPass {\n\t\t\t\tmockT.empty(t)\n\t\t\t} else {\n\t\t\t\tmockT.equals(t, tc.expectedMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotEmpty(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\tobj interface{}\n\t\texpectedNotEmpty bool\n\t}{\n\t\t// expected to be empty\n\t\t{\"\", false},\n\t\t{0, false},\n\t\t{int(0), false},\n\t\t{int32(0), false},\n\t\t{int64(0), false},\n\t\t{uint(0), false},\n\t\t{std.Address(\"\"), false},\n\n\t\t// not expected to be empty\n\t\t{\"Hello World\", true},\n\t\t{1, true},\n\t\t{int32(1), true},\n\t\t{uint64(1), true},\n\t\t{std.Address(\"g12345\"), true},\n\n\t\t// unsupported\n\t\t{nil, false},\n\t\t{myStruct{}, false},\n\t\t{\u0026myStruct{}, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"NotEmpty(%v)\", c.obj)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := NotEmpty(mockT, c.obj)\n\n\t\t\tif res != c.expectedNotEmpty {\n\t\t\t\tt.Errorf(\"%s should return %v: %s\", name, c.expectedNotEmpty, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "ufmt", + "path": "gno.land/p/demo/ufmt", + "files": [ + { + "name": "ufmt.gno", + "body": "// Package ufmt provides utility functions for formatting strings, similarly\n// to the Go package \"fmt\", of which only a subset is currently supported\n// (hence the name µfmt - micro fmt).\npackage ufmt\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Println formats using the default formats for its operands and writes to standard output.\n// Println writes the given arguments to standard output with spaces between arguments\n// and a newline at the end.\nfunc Println(args ...interface{}) {\n\tvar strs []string\n\tfor _, arg := range args {\n\t\tswitch v := arg.(type) {\n\t\tcase string:\n\t\t\tstrs = append(strs, v)\n\t\tcase (interface{ String() string }):\n\t\t\tstrs = append(strs, v.String())\n\t\tcase error:\n\t\t\tstrs = append(strs, v.Error())\n\t\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t\tstrs = append(strs, Sprintf(\"%d\", v))\n\t\tcase bool:\n\t\t\tif v {\n\t\t\t\tstrs = append(strs, \"true\")\n\t\t\t} else {\n\t\t\t\tstrs = append(strs, \"false\")\n\t\t\t}\n\t\tcase nil:\n\t\t\tstrs = append(strs, \"\u003cnil\u003e\")\n\t\tdefault:\n\t\t\tstrs = append(strs, \"(unhandled)\")\n\t\t}\n\t}\n\n\t// TODO: remove println after gno supports os.Stdout\n\tprintln(strings.Join(strs, \" \"))\n}\n\n// Sprintf offers similar functionality to Go's fmt.Sprintf, or the sprintf\n// equivalent available in many languages, including C/C++.\n// The number of args passed must exactly match the arguments consumed by the format.\n// A limited number of formatting verbs and features are currently supported,\n// hence the name ufmt (µfmt, micro-fmt).\n//\n// The currently formatted verbs are the following:\n//\n//\t%s: places a string value directly.\n//\t If the value implements the interface interface{ String() string },\n//\t the String() method is called to retrieve the value. Same about Error()\n//\t string.\n//\t%c: formats the character represented by Unicode code point\n//\t%d: formats an integer value using package \"strconv\".\n//\t Currently supports only uint, uint64, int, int64.\n//\t%t: formats a boolean value to \"true\" or \"false\".\n//\t%x: formats an integer value as a hexadecimal string.\n//\t Currently supports only uint8, []uint8, [32]uint8.\n//\t%c: formats a rune value as a string.\n//\t Currently supports only rune, int.\n//\t%q: formats a string value as a quoted string.\n//\t%T: formats the type of the value.\n//\t%%: outputs a literal %. Does not consume an argument.\nfunc Sprintf(format string, args ...interface{}) string {\n\t// we use runes to handle multi-byte characters\n\tsTor := []rune(format)\n\tend := len(sTor)\n\targNum := 0\n\targLen := len(args)\n\tbuf := \"\"\n\n\tfor i := 0; i \u003c end; {\n\t\tisLast := i == end-1\n\t\tc := string(sTor[i])\n\n\t\tif isLast || c != \"%\" {\n\t\t\t// we don't check for invalid format like a one ending with \"%\"\n\t\t\tbuf += string(c)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tverb := string(sTor[i+1])\n\t\tif verb == \"%\" {\n\t\t\tbuf += \"%\"\n\t\t\ti += 2\n\t\t\tcontinue\n\t\t}\n\n\t\tif argNum \u003e argLen {\n\t\t\tpanic(\"invalid number of arguments to ufmt.Sprintf\")\n\t\t}\n\t\targ := args[argNum]\n\t\targNum++\n\n\t\tswitch verb {\n\t\tcase \"s\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase interface{ String() string }:\n\t\t\t\tbuf += v.String()\n\t\t\tcase error:\n\t\t\t\tbuf += v.Error()\n\t\t\tcase string:\n\t\t\t\tbuf += v\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"c\":\n\t\t\tswitch v := arg.(type) {\n\t\t\t// rune is int32. Exclude overflowing numeric types and dups (byte, int32):\n\t\t\tcase rune:\n\t\t\t\tbuf += string(v)\n\t\t\tcase int:\n\t\t\t\tbuf += string(v)\n\t\t\tcase int8:\n\t\t\t\tbuf += string(v)\n\t\t\tcase int16:\n\t\t\t\tbuf += string(v)\n\t\t\tcase uint:\n\t\t\t\tbuf += string(v)\n\t\t\tcase uint8:\n\t\t\t\tbuf += string(v)\n\t\t\tcase uint16:\n\t\t\t\tbuf += string(v)\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"d\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase int:\n\t\t\t\tbuf += strconv.Itoa(v)\n\t\t\tcase int8:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase int16:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase int32:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase int64:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase uint:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint8:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint16:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint32:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint64:\n\t\t\t\tbuf += strconv.FormatUint(v, 10)\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"t\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase bool:\n\t\t\t\tif v {\n\t\t\t\t\tbuf += \"true\"\n\t\t\t\t} else {\n\t\t\t\t\tbuf += \"false\"\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"x\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase uint8:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 16)\n\t\t\tdefault:\n\t\t\t\tbuf += \"(unhandled)\"\n\t\t\t}\n\t\tcase \"q\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase string:\n\t\t\t\tbuf += strconv.Quote(v)\n\t\t\tdefault:\n\t\t\t\tbuf += \"(unhandled)\"\n\t\t\t}\n\t\tcase \"T\":\n\t\t\tswitch arg.(type) {\n\t\t\tcase bool:\n\t\t\t\tbuf += \"bool\"\n\t\t\tcase int:\n\t\t\t\tbuf += \"int\"\n\t\t\tcase int8:\n\t\t\t\tbuf += \"int8\"\n\t\t\tcase int16:\n\t\t\t\tbuf += \"int16\"\n\t\t\tcase int32:\n\t\t\t\tbuf += \"int32\"\n\t\t\tcase int64:\n\t\t\t\tbuf += \"int64\"\n\t\t\tcase uint:\n\t\t\t\tbuf += \"uint\"\n\t\t\tcase uint8:\n\t\t\t\tbuf += \"uint8\"\n\t\t\tcase uint16:\n\t\t\t\tbuf += \"uint16\"\n\t\t\tcase uint32:\n\t\t\t\tbuf += \"uint32\"\n\t\t\tcase uint64:\n\t\t\t\tbuf += \"uint64\"\n\t\t\tcase string:\n\t\t\t\tbuf += \"string\"\n\t\t\tcase []byte:\n\t\t\t\tbuf += \"[]byte\"\n\t\t\tcase []rune:\n\t\t\t\tbuf += \"[]rune\"\n\t\t\tdefault:\n\t\t\t\tbuf += \"unknown\"\n\t\t\t}\n\t\t// % handled before, as it does not consume an argument\n\t\tdefault:\n\t\t\tbuf += \"(unhandled verb: %\" + verb + \")\"\n\t\t}\n\n\t\ti += 2\n\t}\n\tif argNum \u003c argLen {\n\t\tpanic(\"too many arguments to ufmt.Sprintf\")\n\t}\n\treturn buf\n}\n\n// This function is used to mimic Go's fmt.Sprintf\n// specific behaviour of showing verb/type mismatches,\n// where for example:\n//\n//\tfmt.Sprintf(\"%d\", \"foo\") gives \"%!d(string=foo)\"\n//\n// Here:\n//\n//\tfallback(\"s\", 8) -\u003e \"%!s(int=8)\"\n//\tfallback(\"d\", nil) -\u003e \"%!d(\u003cnil\u003e)\", and so on.\nfunc fallback(verb string, arg interface{}) string {\n\tvar s string\n\tswitch v := arg.(type) {\n\tcase string:\n\t\ts = \"string=\" + v\n\tcase (interface{ String() string }):\n\t\ts = \"string=\" + v.String()\n\tcase error:\n\t\t// note: also \"string=\" in Go fmt\n\t\ts = \"string=\" + v.Error()\n\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t// note: rune, byte would be dups, being aliases\n\t\tif typename, e := typeToString(v); e != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t} else {\n\t\t\ts = typename + \"=\" + Sprintf(\"%d\", v)\n\t\t}\n\tcase bool:\n\t\tif v {\n\t\t\ts = \"bool=true\"\n\t\t} else {\n\t\t\ts = \"bool=false\"\n\t\t}\n\tcase nil:\n\t\ts = \"\u003cnil\u003e\"\n\tdefault:\n\t\ts = \"(unhandled)\"\n\t}\n\treturn \"%!\" + verb + \"(\" + s + \")\"\n}\n\n// Get the name of the type of `v` as a string.\n// The recognized type of v is currently limited to native non-composite types.\n// An error is returned otherwise.\nfunc typeToString(v interface{}) (string, error) {\n\tswitch v.(type) {\n\tcase string:\n\t\treturn \"string\", nil\n\tcase int:\n\t\treturn \"int\", nil\n\tcase int8:\n\t\treturn \"int8\", nil\n\tcase int16:\n\t\treturn \"int16\", nil\n\tcase int32:\n\t\treturn \"int32\", nil\n\tcase int64:\n\t\treturn \"int64\", nil\n\tcase uint:\n\t\treturn \"uint\", nil\n\tcase uint8:\n\t\treturn \"uint8\", nil\n\tcase uint16:\n\t\treturn \"uint16\", nil\n\tcase uint32:\n\t\treturn \"uint32\", nil\n\tcase uint64:\n\t\treturn \"uint64\", nil\n\tcase float32:\n\t\treturn \"float32\", nil\n\tcase float64:\n\t\treturn \"float64\", nil\n\tcase bool:\n\t\treturn \"bool\", nil\n\tdefault:\n\t\treturn \"\", errors.New(\"(unsupported type)\")\n\t}\n}\n\n// errMsg implements the error interface.\ntype errMsg struct {\n\tmsg string\n}\n\n// Error defines the requirements of the error interface.\n// It functions similarly to Go's errors.New()\nfunc (e *errMsg) Error() string {\n\treturn e.msg\n}\n\n// Errorf is a function that mirrors the functionality of fmt.Errorf.\n//\n// It takes a format string and arguments to create a formatted string,\n// then sets this string as the 'msg' field of an errMsg struct and returns a pointer to this struct.\n//\n// This function operates in a similar manner to Go's fmt.Errorf,\n// providing a way to create formatted error messages.\n//\n// The currently formatted verbs are the following:\n//\n//\t%s: places a string value directly.\n//\t If the value implements the interface interface{ String() string },\n//\t the String() method is called to retrieve the value. Same for error.\n//\t%c: formats the character represented by Unicode code point\n//\t%d: formats an integer value using package \"strconv\".\n//\t Currently supports only uint, uint64, int, int64.\n//\t%t: formats a boolean value to \"true\" or \"false\".\n//\t%%: outputs a literal %. Does not consume an argument.\nfunc Errorf(format string, args ...interface{}) error {\n\treturn \u0026errMsg{Sprintf(format, args...)}\n}\n" + }, + { + "name": "ufmt_test.gno", + "body": "package ufmt\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype stringer struct{}\n\nfunc (stringer) String() string {\n\treturn \"I'm a stringer\"\n}\n\nfunc TestSprintf(t *testing.T) {\n\ttru := true\n\tcases := []struct {\n\t\tformat string\n\t\tvalues []interface{}\n\t\texpectedOutput string\n\t}{\n\t\t{\"hello %s!\", []interface{}{\"planet\"}, \"hello planet!\"},\n\t\t{\"hi %%%s!\", []interface{}{\"worl%d\"}, \"hi %worl%d!\"},\n\t\t{\"%s %c %d %t\", []interface{}{\"foo\", 'α', 421, true}, \"foo α 421 true\"},\n\t\t{\"string [%s]\", []interface{}{\"foo\"}, \"string [foo]\"},\n\t\t{\"int [%d]\", []interface{}{int(42)}, \"int [42]\"},\n\t\t{\"int8 [%d]\", []interface{}{int8(8)}, \"int8 [8]\"},\n\t\t{\"int16 [%d]\", []interface{}{int16(16)}, \"int16 [16]\"},\n\t\t{\"int32 [%d]\", []interface{}{int32(32)}, \"int32 [32]\"},\n\t\t{\"int64 [%d]\", []interface{}{int64(64)}, \"int64 [64]\"},\n\t\t{\"uint [%d]\", []interface{}{uint(42)}, \"uint [42]\"},\n\t\t{\"uint8 [%d]\", []interface{}{uint8(8)}, \"uint8 [8]\"},\n\t\t{\"uint16 [%d]\", []interface{}{uint16(16)}, \"uint16 [16]\"},\n\t\t{\"uint32 [%d]\", []interface{}{uint32(32)}, \"uint32 [32]\"},\n\t\t{\"uint64 [%d]\", []interface{}{uint64(64)}, \"uint64 [64]\"},\n\t\t{\"bool [%t]\", []interface{}{true}, \"bool [true]\"},\n\t\t{\"bool [%t]\", []interface{}{false}, \"bool [false]\"},\n\t\t{\"no args\", nil, \"no args\"},\n\t\t{\"finish with %\", nil, \"finish with %\"},\n\t\t{\"stringer [%s]\", []interface{}{stringer{}}, \"stringer [I'm a stringer]\"},\n\t\t{\"â\", nil, \"â\"},\n\t\t{\"Hello, World! 😊\", nil, \"Hello, World! 😊\"},\n\t\t{\"unicode formatting: %s\", []interface{}{\"😊\"}, \"unicode formatting: 😊\"},\n\t\t{\"invalid hex [%x]\", []interface{}{\"invalid\"}, \"invalid hex [(unhandled)]\"},\n\t\t{\"rune as character [%c]\", []interface{}{rune('A')}, \"rune as character [A]\"},\n\t\t{\"int as character [%c]\", []interface{}{int('B')}, \"int as character [B]\"},\n\t\t{\"quoted string [%q]\", []interface{}{\"hello\"}, \"quoted string [\\\"hello\\\"]\"},\n\t\t{\"quoted string with escape [%q]\", []interface{}{\"\\thello\\nworld\\\\\"}, \"quoted string with escape [\\\"\\\\thello\\\\nworld\\\\\\\\\\\"]\"},\n\t\t{\"invalid quoted string [%q]\", []interface{}{123}, \"invalid quoted string [(unhandled)]\"},\n\t\t{\"type of bool [%T]\", []interface{}{true}, \"type of bool [bool]\"},\n\t\t{\"type of int [%T]\", []interface{}{123}, \"type of int [int]\"},\n\t\t{\"type of string [%T]\", []interface{}{\"hello\"}, \"type of string [string]\"},\n\t\t{\"type of []byte [%T]\", []interface{}{[]byte{1, 2, 3}}, \"type of []byte [[]byte]\"},\n\t\t{\"type of []rune [%T]\", []interface{}{[]rune{'a', 'b', 'c'}}, \"type of []rune [[]rune]\"},\n\t\t{\"type of unknown [%T]\", []interface{}{struct{}{}}, \"type of unknown [unknown]\"},\n\t\t// mismatch printing\n\t\t{\"%s\", []interface{}{nil}, \"%!s(\u003cnil\u003e)\"},\n\t\t{\"%s\", []interface{}{421}, \"%!s(int=421)\"},\n\t\t{\"%s\", []interface{}{\"z\"}, \"z\"},\n\t\t{\"%s\", []interface{}{tru}, \"%!s(bool=true)\"},\n\t\t{\"%s\", []interface{}{'z'}, \"%!s(int32=122)\"},\n\n\t\t{\"%c\", []interface{}{nil}, \"%!c(\u003cnil\u003e)\"},\n\t\t{\"%c\", []interface{}{421}, \"ƥ\"},\n\t\t{\"%c\", []interface{}{\"z\"}, \"%!c(string=z)\"},\n\t\t{\"%c\", []interface{}{tru}, \"%!c(bool=true)\"},\n\t\t{\"%c\", []interface{}{'z'}, \"z\"},\n\n\t\t{\"%d\", []interface{}{nil}, \"%!d(\u003cnil\u003e)\"},\n\t\t{\"%d\", []interface{}{421}, \"421\"},\n\t\t{\"%d\", []interface{}{\"z\"}, \"%!d(string=z)\"},\n\t\t{\"%d\", []interface{}{tru}, \"%!d(bool=true)\"},\n\t\t{\"%d\", []interface{}{'z'}, \"122\"},\n\n\t\t{\"%t\", []interface{}{nil}, \"%!t(\u003cnil\u003e)\"},\n\t\t{\"%t\", []interface{}{421}, \"%!t(int=421)\"},\n\t\t{\"%t\", []interface{}{\"z\"}, \"%!t(string=z)\"},\n\t\t{\"%t\", []interface{}{tru}, \"true\"},\n\t\t{\"%t\", []interface{}{'z'}, \"%!t(int32=122)\"},\n\t}\n\n\tfor _, tc := range cases {\n\t\tname := fmt.Sprintf(tc.format, tc.values...)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Sprintf(tc.format, tc.values...)\n\t\t\tif got != tc.expectedOutput {\n\t\t\t\tt.Errorf(\"got %q, want %q.\", got, tc.expectedOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestErrorf(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tformat string\n\t\targs []interface{}\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"simple string\",\n\t\t\tformat: \"error: %s\",\n\t\t\targs: []interface{}{\"something went wrong\"},\n\t\t\texpected: \"error: something went wrong\",\n\t\t},\n\t\t{\n\t\t\tname: \"integer value\",\n\t\t\tformat: \"value: %d\",\n\t\t\targs: []interface{}{42},\n\t\t\texpected: \"value: 42\",\n\t\t},\n\t\t{\n\t\t\tname: \"boolean value\",\n\t\t\tformat: \"success: %t\",\n\t\t\targs: []interface{}{true},\n\t\t\texpected: \"success: true\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple values\",\n\t\t\tformat: \"error %d: %s (success=%t)\",\n\t\t\targs: []interface{}{123, \"failure occurred\", false},\n\t\t\texpected: \"error 123: failure occurred (success=false)\",\n\t\t},\n\t\t{\n\t\t\tname: \"literal percent\",\n\t\t\tformat: \"literal %%\",\n\t\t\targs: []interface{}{},\n\t\t\texpected: \"literal %\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := Errorf(tt.format, tt.args...)\n\t\t\tif err.Error() != tt.expected {\n\t\t\t\tt.Errorf(\"Errorf(%q, %v) = %q, expected %q\", tt.format, tt.args, err.Error(), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrintErrors(t *testing.T) {\n\tgot := Sprintf(\"error: %s\", errors.New(\"can I be printed?\"))\n\texpectedOutput := \"error: can I be printed?\"\n\tif got != expectedOutput {\n\t\tt.Errorf(\"got %q, want %q.\", got, expectedOutput)\n\t}\n}\n\n// NOTE: Currently, there is no way to get the output of Println without using os.Stdout,\n// so we can only test that it doesn't panic and print arguments well.\nfunc TestPrintln(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\targs []interface{}\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Empty args\",\n\t\t\targs: []interface{}{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"String args\",\n\t\t\targs: []interface{}{\"Hello\", \"World\"},\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname: \"Integer args\",\n\t\t\targs: []interface{}{1, 2, 3},\n\t\t\texpected: \"1 2 3\",\n\t\t},\n\t\t{\n\t\t\tname: \"Mixed args\",\n\t\t\targs: []interface{}{\"Hello\", 42, true, false, \"World\"},\n\t\t\texpected: \"Hello 42 true false World\",\n\t\t},\n\t\t{\n\t\t\tname: \"Unhandled type\",\n\t\t\targs: []interface{}{\"Hello\", 3.14, []int{1, 2, 3}},\n\t\t\texpected: \"Hello (unhandled) (unhandled)\",\n\t\t},\n\t}\n\n\t// TODO: replace os.Stdout with a buffer to capture the output and test it.\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tPrintln(tt.args...)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "acl", + "path": "gno.land/p/demo/acl", + "files": [ + { + "name": "acl.gno", + "body": "package acl\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nfunc New() *Directory {\n\treturn \u0026Directory{\n\t\tuserGroups: avl.Tree{},\n\t\tpermBuckets: avl.Tree{},\n\t}\n}\n\ntype Directory struct {\n\tpermBuckets avl.Tree // identifier -\u003e perms\n\tuserGroups avl.Tree // std.Address -\u003e []string\n}\n\nfunc (d *Directory) HasPerm(addr std.Address, verb, resource string) bool {\n\t// FIXME: consider memoize.\n\n\t// user perms\n\tif d.getBucketPerms(\"u:\"+addr.String()).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// everyone's perms.\n\tif d.getBucketPerms(\"g:\"+Everyone).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// user groups' perms.\n\tgroups, ok := d.userGroups.Get(addr.String())\n\tif ok {\n\t\tfor _, group := range groups.([]string) {\n\t\t\tif d.getBucketPerms(\"g:\"+group).hasPerm(verb, resource) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (d *Directory) getBucketPerms(bucket string) perms {\n\tres, ok := d.permBuckets.Get(bucket)\n\tif ok {\n\t\treturn res.(perms)\n\t}\n\treturn perms{}\n}\n\nfunc (d *Directory) HasRole(addr std.Address, role string) bool {\n\treturn d.HasPerm(addr, \"role\", role)\n}\n\nfunc (d *Directory) AddUserPerm(addr std.Address, verb, resource string) {\n\tbucket := \"u:\" + addr.String()\n\tp := perm{\n\t\tverbs: []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) AddGroupPerm(name string, verb, resource string) {\n\tbucket := \"g:\" + name\n\tp := perm{\n\t\tverbs: []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) addPermToBucket(bucket string, p perm) {\n\tvar ps perms\n\n\texisting, ok := d.permBuckets.Get(bucket)\n\tif ok {\n\t\tps = existing.(perms)\n\t}\n\tps = append(ps, p)\n\n\td.permBuckets.Set(bucket, ps)\n}\n\nfunc (d *Directory) AddUserToGroup(user std.Address, group string) {\n\texisting, ok := d.userGroups.Get(user.String())\n\tvar groups []string\n\tif ok {\n\t\tgroups = existing.([]string)\n\t}\n\tgroups = append(groups, group)\n\td.userGroups.Set(user.String(), groups)\n}\n\n// TODO: helpers to remove permissions.\n// TODO: helpers to adds multiple permissions at once -\u003e {verbs: []string{\"read\",\"write\"}}.\n// TODO: helpers to delete users from gorups.\n// TODO: helpers to quickly reset states.\n" + }, + { + "name": "acl_test.gno", + "body": "package acl\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Test(t *testing.T) {\n\tadm := testutils.TestAddress(\"admin\")\n\tmod := testutils.TestAddress(\"mod\")\n\tusr := testutils.TestAddress(\"user\")\n\tcst := testutils.TestAddress(\"custom\")\n\n\tdir := New()\n\n\t// by default, no one has perm.\n\tshouldNotHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldNotHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding all the rights to admin.\n\tdir.AddUserPerm(adm, \".*\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding custom regexp rule for user \"cst\".\n\tdir.AddUserPerm(cst, \"write\", \"r/demo/boards:gnolang/.*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding a group perm for a new group.\n\t// no changes expected.\n\tdir.AddGroupPerm(\"mods\", \"role\", \"moderator\")\n\tdir.AddGroupPerm(\"mods\", \"write\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// assigning the user \"mod\" to the \"mods\" group.\n\tdir.AddUserToGroup(mod, \"mods\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding \"read\" permission for everyone.\n\tdir.AddGroupPerm(Everyone, \"read\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\") // new\n}\n\nfunc shouldHasRole(t *testing.T, dir *Directory, addr std.Address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has role %s\", addr.String(), role))\n}\n\nfunc shouldNotHasRole(t *testing.T, dir *Directory, addr std.Address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has role %s\", addr.String(), role))\n}\n\nfunc shouldHasPerm(t *testing.T, dir *Directory, addr std.Address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has perm for %s - %s\", addr.String(), verb, resource))\n}\n\nfunc shouldNotHasPerm(t *testing.T, dir *Directory, addr std.Address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has perm for %s - %s\", addr.String(), verb, resource))\n}\n" + }, + { + "name": "const.gno", + "body": "package acl\n\nconst Everyone string = \"everyone\"\n" + }, + { + "name": "perm.gno", + "body": "package acl\n\nimport \"regexp\"\n\ntype perm struct {\n\tverbs []string\n\tresources []string\n}\n\nfunc (perm perm) hasPerm(verb, resource string) bool {\n\t// check verb\n\tverbOK := false\n\tfor _, pattern := range perm.verbs {\n\t\tif match(pattern, verb) {\n\t\t\tverbOK = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !verbOK {\n\t\treturn false\n\t}\n\n\t// check resource\n\tfor _, pattern := range perm.resources {\n\t\tif match(pattern, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc match(pattern, target string) bool {\n\tif pattern == \".*\" {\n\t\treturn true\n\t}\n\n\tif pattern == target {\n\t\treturn true\n\t}\n\n\t// regexp handling\n\tmatch, _ := regexp.MatchString(pattern, target)\n\treturn match\n}\n" + }, + { + "name": "perms.gno", + "body": "package acl\n\ntype perms []perm\n\nfunc (perms perms) hasPerm(verb, resource string) bool {\n\tfor _, perm := range perms {\n\t\tif perm.hasPerm(verb, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "urequire", + "path": "gno.land/p/demo/urequire", + "files": [ + { + "name": "urequire.gno", + "body": "// urequire is a sister package for uassert.\n// XXX: codegen the package.\npackage urequire\n\nimport \"gno.land/p/demo/uassert\"\n\n// type TestingT = uassert.TestingT // XXX: bug, should work\n\nfunc NoError(t uassert.TestingT, err error, msgs ...string) {\n\tt.Helper()\n\tif uassert.NoError(t, err, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc Error(t uassert.TestingT, err error, msgs ...string) {\n\tt.Helper()\n\tif uassert.Error(t, err, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc ErrorContains(t uassert.TestingT, err error, contains string, msgs ...string) {\n\tt.Helper()\n\tif uassert.ErrorContains(t, err, contains, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc True(t uassert.TestingT, value bool, msgs ...string) {\n\tt.Helper()\n\tif uassert.True(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc False(t uassert.TestingT, value bool, msgs ...string) {\n\tt.Helper()\n\tif uassert.False(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc ErrorIs(t uassert.TestingT, err, target error, msgs ...string) {\n\tt.Helper()\n\tif uassert.ErrorIs(t, err, target, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc PanicsWithMessage(t uassert.TestingT, msg string, f func(), msgs ...string) {\n\tt.Helper()\n\tif uassert.PanicsWithMessage(t, msg, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotPanics(t uassert.TestingT, f func(), msgs ...string) {\n\tt.Helper()\n\tif uassert.NotPanics(t, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc Equal(t uassert.TestingT, expected, actual interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.Equal(t, expected, actual, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotEqual(t uassert.TestingT, expected, actual interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotEqual(t, expected, actual, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc Empty(t uassert.TestingT, obj interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.Empty(t, obj, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotEmpty(t uassert.TestingT, obj interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotEmpty(t, obj, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n" + }, + { + "name": "urequire_test.gno", + "body": "package urequire\n\nimport \"testing\"\n\nfunc TestPackage(t *testing.T) {\n\tEqual(t, 42, 42)\n\t// XXX: find a way to unit test this package\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "pager", + "path": "gno.land/p/demo/avl/pager", + "files": [ + { + "name": "pager.gno", + "body": "package pager\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Pager is a struct that holds the AVL tree and pagination parameters.\ntype Pager struct {\n\tTree *avl.Tree\n\tPageQueryParam string\n\tSizeQueryParam string\n\tDefaultPageSize int\n}\n\n// Page represents a single page of results.\ntype Page struct {\n\tItems []Item\n\tPageNumber int\n\tPageSize int\n\tTotalItems int\n\tTotalPages int\n\tHasPrev bool\n\tHasNext bool\n\tPager *Pager // Reference to the parent Pager\n}\n\n// Item represents a key-value pair in the AVL tree.\ntype Item struct {\n\tKey string\n\tValue interface{}\n}\n\n// NewPager creates a new Pager with default values.\nfunc NewPager(tree *avl.Tree, defaultPageSize int) *Pager {\n\treturn \u0026Pager{\n\t\tTree: tree,\n\t\tPageQueryParam: \"page\",\n\t\tSizeQueryParam: \"size\",\n\t\tDefaultPageSize: defaultPageSize,\n\t}\n}\n\n// GetPage retrieves a page of results from the AVL tree.\nfunc (p *Pager) GetPage(pageNumber int) *Page {\n\treturn p.GetPageWithSize(pageNumber, p.DefaultPageSize)\n}\n\nfunc (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {\n\ttotalItems := p.Tree.Size()\n\ttotalPages := int(math.Ceil(float64(totalItems) / float64(pageSize)))\n\n\tpage := \u0026Page{\n\t\tTotalItems: totalItems,\n\t\tTotalPages: totalPages,\n\t\tPageSize: pageSize,\n\t\tPager: p,\n\t}\n\n\t// pages without content\n\tif pageSize \u003c 1 {\n\t\treturn page\n\t}\n\n\t// page number provided is not available\n\tif pageNumber \u003c 1 {\n\t\tpage.HasNext = totalPages \u003e 0\n\t\treturn page\n\t}\n\n\t// page number provided is outside the range of total pages\n\tif pageNumber \u003e totalPages {\n\t\tpage.PageNumber = pageNumber\n\t\tpage.HasPrev = pageNumber \u003e 0\n\t\treturn page\n\t}\n\n\tstartIndex := (pageNumber - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\tif endIndex \u003e totalItems {\n\t\tendIndex = totalItems\n\t}\n\n\titems := []Item{}\n\tp.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool {\n\t\titems = append(items, Item{Key: key, Value: value})\n\t\treturn false\n\t})\n\n\tpage.Items = items\n\tpage.PageNumber = pageNumber\n\tpage.HasPrev = pageNumber \u003e 1\n\tpage.HasNext = pageNumber \u003c totalPages\n\treturn page\n}\n\nfunc (p *Pager) MustGetPageByPath(rawURL string) *Page {\n\tpage, err := p.GetPageByPath(rawURL)\n\tif err != nil {\n\t\tpanic(\"invalid path\")\n\t}\n\treturn page\n}\n\n// GetPageByPath retrieves a page of results based on the query parameters in the URL path.\nfunc (p *Pager) GetPageByPath(rawURL string) (*Page, error) {\n\tpageNumber, pageSize, err := p.ParseQuery(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.GetPageWithSize(pageNumber, pageSize), nil\n}\n\n// UI generates the Markdown UI for the page selector.\nfunc (p *Page) Selector() string {\n\tpageNumber := p.PageNumber\n\tpageNumber = max(pageNumber, 1)\n\n\tif p.TotalPages \u003c= 1 {\n\t\treturn \"\"\n\t}\n\n\tmd := \"\"\n\n\tif p.HasPrev {\n\t\t// Always show the first page link\n\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", 1, p.Pager.PageQueryParam, 1)\n\n\t\t// Before\n\t\tif p.PageNumber \u003e 4 {\n\t\t\tmd += \"… | \"\n\t\t}\n\n\t\tif p.PageNumber \u003e 3 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2)\n\t\t}\n\n\t\tif p.PageNumber \u003e 2 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1)\n\t\t}\n\t}\n\n\tif p.PageNumber \u003e 0 \u0026\u0026 p.PageNumber \u003c= p.TotalPages {\n\t\t// Current page\n\t\tmd += ufmt.Sprintf(\"**%d**\", p.PageNumber)\n\t} else {\n\t\tmd += ufmt.Sprintf(\"_%d_\", p.PageNumber)\n\t}\n\n\tif p.HasNext {\n\t\tmd += \" | \"\n\n\t\tif p.PageNumber \u003c p.TotalPages-1 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-2 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-3 {\n\t\t\tmd += \"… | \"\n\t\t}\n\n\t\t// Always show the last page link\n\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d)\", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages)\n\t}\n\n\treturn md\n}\n\n// ParseQuery parses the URL to extract the page number and page size.\nfunc (p *Pager) ParseQuery(rawURL string) (int, int, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn 1, p.DefaultPageSize, err\n\t}\n\n\tquery := u.Query()\n\tpageNumber := 1\n\tpageSize := p.DefaultPageSize\n\n\tif p.PageQueryParam != \"\" {\n\t\tif pageStr := query.Get(p.PageQueryParam); pageStr != \"\" {\n\t\t\tpageNumber, err = strconv.Atoi(pageStr)\n\t\t\tif err != nil || pageNumber \u003c 1 {\n\t\t\t\tpageNumber = 1\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.SizeQueryParam != \"\" {\n\t\tif sizeStr := query.Get(p.SizeQueryParam); sizeStr != \"\" {\n\t\t\tpageSize, err = strconv.Atoi(sizeStr)\n\t\t\tif err != nil || pageSize \u003c 1 {\n\t\t\t\tpageSize = p.DefaultPageSize\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pageNumber, pageSize, nil\n}\n\nfunc max(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n" + }, + { + "name": "pager_test.gno", + "body": "package pager\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestPager_GetPage(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize int\n\t\texpected []Item\n\t}{\n\t\t{1, 2, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}}},\n\t\t{2, 2, []Item{{Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}}},\n\t\t{3, 2, []Item{{Key: \"e\", Value: 5}}},\n\t\t{1, 3, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}}},\n\t\t{2, 3, []Item{{Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t{1, 5, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t{2, 5, []Item{}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tuassert.Equal(t, len(tt.expected), len(page.Items))\n\n\t\tfor i, item := range page.Items {\n\t\t\tuassert.Equal(t, tt.expected[i].Key, item.Key)\n\t\t\tuassert.Equal(t, tt.expected[i].Value, item.Value)\n\t\t}\n\t}\n}\n\nfunc TestPager_GetPageByPath(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 50; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL string\n\t\texpectedPage int\n\t\texpectedSize int\n\t}{\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=1\", 1, 10},\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=2\", 2, 10},\n\t\t{\"/r/foo:bar/baz?page=3\", 3, pager.DefaultPageSize},\n\t\t{\"/r/foo:bar/baz?size=20\", 1, 20},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, err := pager.GetPageByPath(tt.rawURL)\n\t\turequire.NoError(t, err, ufmt.Sprintf(\"GetPageByPath(%s) returned error: %v\", tt.rawURL, err))\n\n\t\tuassert.Equal(t, tt.expectedPage, page.PageNumber)\n\t\tuassert.Equal(t, tt.expectedSize, page.PageSize)\n\t}\n}\n\nfunc TestPage_Selector(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize int\n\t\texpected string\n\t}{\n\t\t{1, 2, \"**1** | [2](?page=2) | [3](?page=3)\"},\n\t\t{2, 2, \"[1](?page=1) | **2** | [3](?page=3)\"},\n\t\t{3, 2, \"[1](?page=1) | [2](?page=2) | **3**\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Selector()\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_UI_WithManyPages(t *testing.T) {\n\t// Create a new AVL tree and populate it with many key-value pairs.\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases for a large number of pages.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize int\n\t\texpected string\n\t}{\n\t\t// XXX: -1\n\t\t// XXX: 0\n\t\t{1, 10, \"**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)\"},\n\t\t{2, 10, \"[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)\"},\n\t\t{3, 10, \"[1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | … | [10](?page=10)\"},\n\t\t{4, 10, \"[1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) | … | [10](?page=10)\"},\n\t\t{5, 10, \"[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)\"},\n\t\t{6, 10, \"[1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** | [7](?page=7) | [8](?page=8) | … | [10](?page=10)\"},\n\t\t{7, 10, \"[1](?page=1) | … | [5](?page=5) | [6](?page=6) | **7** | [8](?page=8) | [9](?page=9) | [10](?page=10)\"},\n\t\t{8, 10, \"[1](?page=1) | … | [6](?page=6) | [7](?page=7) | **8** | [9](?page=9) | [10](?page=10)\"},\n\t\t{9, 10, \"[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)\"},\n\t\t{10, 10, \"[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**\"},\n\t\t// XXX: 11\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Selector()\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_ParseQuery(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL string\n\t\texpectedPage int\n\t\texpectedSize int\n\t\texpectedError bool\n\t}{\n\t\t{\"/r/foo:bar/baz?size=2\u0026page=1\", 1, 2, false},\n\t\t{\"/r/foo:bar/baz?size=3\u0026page=2\", 2, 3, false},\n\t\t{\"/r/foo:bar/baz?size=5\u0026page=3\", 3, 5, false},\n\t\t{\"/r/foo:bar/baz?page=2\", 2, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=3\", 1, 3, false},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=0\u0026page=0\", 1, pager.DefaultPageSize, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, size, err := pager.ParseQuery(tt.rawURL)\n\t\tif tt.expectedError {\n\t\t\tuassert.Error(t, err, ufmt.Sprintf(\"ParseQuery(%s) expected error but got none\", tt.rawURL))\n\t\t} else {\n\t\t\turequire.NoError(t, err, ufmt.Sprintf(\"ParseQuery(%s) returned error: %v\", tt.rawURL, err))\n\t\t\tuassert.Equal(t, tt.expectedPage, page, ufmt.Sprintf(\"ParseQuery(%s) returned page %d, expected %d\", tt.rawURL, page, tt.expectedPage))\n\t\t\tuassert.Equal(t, tt.expectedSize, size, ufmt.Sprintf(\"ParseQuery(%s) returned size %d, expected %d\", tt.rawURL, size, tt.expectedSize))\n\t\t}\n\t}\n}\n" + }, + { + "name": "z_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\tvar id seqid.ID\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 42; i++ {\n\t\ttree.Set(id.Next().String(), i)\n\t}\n\n\t// Create a new pager.\n\tpager := pager.NewPager(tree, 7)\n\n\tfor pn := -1; pn \u003c 8; pn++ {\n\t\tpage := pager.GetPage(pn)\n\n\t\tprintln(ufmt.Sprintf(\"## Page %d of %d\", page.PageNumber, page.TotalPages))\n\t\tfor idx, item := range page.Items {\n\t\t\tprintln(ufmt.Sprintf(\"- idx=%d key=%s value=%d\", idx, item.Key, item.Value))\n\t\t}\n\t\tprintln(page.Selector())\n\t\tprintln()\n\t}\n}\n\n// Output:\n// ## Page 0 of 6\n// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n//\n// ## Page 0 of 6\n// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n//\n// ## Page 1 of 6\n// - idx=0 key=0000001 value=0\n// - idx=1 key=0000002 value=1\n// - idx=2 key=0000003 value=2\n// - idx=3 key=0000004 value=3\n// - idx=4 key=0000005 value=4\n// - idx=5 key=0000006 value=5\n// - idx=6 key=0000007 value=6\n// **1** | [2](?page=2) | [3](?page=3) | … | [6](?page=6)\n//\n// ## Page 2 of 6\n// - idx=0 key=0000008 value=7\n// - idx=1 key=0000009 value=8\n// - idx=2 key=000000a value=9\n// - idx=3 key=000000b value=10\n// - idx=4 key=000000c value=11\n// - idx=5 key=000000d value=12\n// - idx=6 key=000000e value=13\n// [1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [6](?page=6)\n//\n// ## Page 3 of 6\n// - idx=0 key=000000f value=14\n// - idx=1 key=000000g value=15\n// - idx=2 key=000000h value=16\n// - idx=3 key=000000j value=17\n// - idx=4 key=000000k value=18\n// - idx=5 key=000000m value=19\n// - idx=6 key=000000n value=20\n// [1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | [6](?page=6)\n//\n// ## Page 4 of 6\n// - idx=0 key=000000p value=21\n// - idx=1 key=000000q value=22\n// - idx=2 key=000000r value=23\n// - idx=3 key=000000s value=24\n// - idx=4 key=000000t value=25\n// - idx=5 key=000000v value=26\n// - idx=6 key=000000w value=27\n// [1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6)\n//\n// ## Page 5 of 6\n// - idx=0 key=000000x value=28\n// - idx=1 key=000000y value=29\n// - idx=2 key=000000z value=30\n// - idx=3 key=0000010 value=31\n// - idx=4 key=0000011 value=32\n// - idx=5 key=0000012 value=33\n// - idx=6 key=0000013 value=34\n// [1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6)\n//\n// ## Page 6 of 6\n// - idx=0 key=0000014 value=35\n// - idx=1 key=0000015 value=36\n// - idx=2 key=0000016 value=37\n// - idx=3 key=0000017 value=38\n// - idx=4 key=0000018 value=39\n// - idx=5 key=0000019 value=40\n// - idx=6 key=000001a value=41\n// [1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6**\n//\n// ## Page 7 of 6\n// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "avlhelpers", + "path": "gno.land/p/demo/avlhelpers", + "files": [ + { + "name": "avlhelpers.gno", + "body": "package avlhelpers\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// Iterate the keys in-order starting from the given prefix.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc IterateByteStringKeysByPrefix(tree avl.Tree, prefix string, cb avl.IterCbFn) {\n\tend := \"\"\n\tn := len(prefix)\n\t// To make the end of the search, increment the final character ASCII by one.\n\tfor n \u003e 0 {\n\t\tif ascii := int(prefix[n-1]); ascii \u003c 0xff {\n\t\t\tend = prefix[0:n-1] + string(ascii+1)\n\t\t\tbreak\n\t\t}\n\n\t\t// The last character is 0xff. Try the previous character.\n\t\tn--\n\t}\n\n\ttree.Iterate(prefix, end, cb)\n}\n\n// Get a list of keys starting from the given prefix. Limit the\n// number of results to maxResults.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc ListByteStringKeysByPrefix(tree avl.Tree, prefix string, maxResults int) []string {\n\tresult := []string{}\n\tIterateByteStringKeysByPrefix(tree, prefix, func(key string, value interface{}) bool {\n\t\tresult = append(result, key)\n\t\tif len(result) \u003e= maxResults {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn result\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"encoding/hex\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avlhelpers\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n\ttree := avl.Tree{}\n\n\t{\n\t\t// Empty tree.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t}\n\n\ttree.Set(\"alice\", \"\")\n\ttree.Set(\"andy\", \"\")\n\ttree.Set(\"bob\", \"\")\n\n\t{\n\t\t// Match only alice.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"al\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\t{\n\t\t// Match alice and andy.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t\tprintln(\"match: \" + matches[1])\n\t}\n\n\t{\n\t\t// Match alice and andy limited to 1.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\", 1)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\ttree = avl.Tree{}\n\ttree.Set(\"a\\xff\", \"\")\n\ttree.Set(\"a\\xff\\xff\", \"\")\n\ttree.Set(\"b\", \"\")\n\ttree.Set(\"\\xff\\xff\\x00\", \"\")\n\n\t{\n\t\t// Match only \"a\\xff\\xff\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n\n\t{\n\t\t// Match \"a\\xff\" and \"a\\xff\\xff\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[1]))))\n\t}\n\n\t{\n\t\t// Edge case: Match only \"\\xff\\xff\\x00\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n}\n\n// Output:\n// # matches: 0\n// # matches: 1\n// match: alice\n// # matches: 2\n// match: alice\n// match: andy\n// # matches: 1\n// match: alice\n// # matches: 1\n// match: 61ffff\n// # matches: 2\n// match: 61ff\n// match: 61ffff\n// # matches: 1\n// match: ffff00\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "bf", + "path": "gno.land/p/demo/bf", + "files": [ + { + "name": "bf.gno", + "body": "package bf\n\nimport (\n\t\"strings\"\n)\n\nconst maxlen = 30000\n\nfunc Execute(code string) string {\n\tvar (\n\t\tmemory = make([]byte, maxlen) // memory tape\n\t\tpointer = 0 // initial memory pointer\n\t\tbuf strings.Builder\n\t)\n\n\t// Loop through each character in the code\n\tfor i := 0; i \u003c len(code); i++ {\n\t\tswitch code[i] {\n\t\tcase '\u003e':\n\t\t\t// Increment memory pointer\n\t\t\tpointer++\n\t\t\tif pointer \u003e= maxlen {\n\t\t\t\tpointer = 0\n\t\t\t}\n\t\tcase '\u003c':\n\t\t\t// Decrement memory pointer\n\t\t\tpointer--\n\t\t\tif pointer \u003c 0 {\n\t\t\t\tpointer = maxlen - 1\n\t\t\t}\n\t\tcase '+':\n\t\t\t// Increment the byte at the memory pointer\n\t\t\tmemory[pointer]++\n\t\tcase '-':\n\t\t\t// Decrement the byte at the memory pointer\n\t\t\tmemory[pointer]--\n\t\tcase '.':\n\t\t\t// Output the byte at the memory pointer\n\t\t\tbuf.WriteByte(memory[pointer])\n\t\tcase ',':\n\t\t\t// Input a byte and store it in the memory\n\t\t\tpanic(\"unsupported\")\n\t\t\t// fmt.Scan(\u0026memory[pointer])\n\t\tcase '[':\n\t\t\t// Jump forward past the matching ']' if the byte at the memory pointer is zero\n\t\t\tif memory[pointer] == 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti++\n\t\t\t\t\tif code[i] == '[' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == ']' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase ']':\n\t\t\t// Jump backward to the matching '[' if the byte at the memory pointer is nonzero\n\t\t\tif memory[pointer] != 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti--\n\t\t\t\t\tif code[i] == ']' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == '[' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti-- // Move back one more to compensate for the upcoming increment in the loop\n\t\t\t}\n\t\t}\n\t}\n\treturn buf.String()\n}\n" + }, + { + "name": "bf_test.gno", + "body": "package bf\n\nimport \"testing\"\n\nfunc TestExecuteBrainfuck(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tcode string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"hello\",\n\t\t\tcode: \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\",\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname: \"increment\",\n\t\t\tcode: \"+++++ +++++ [ \u003e +++++ ++ \u003c - ] \u003e +++++ .\",\n\t\t\texpected: \"K\",\n\t\t},\n\t\t// Add more test cases as needed\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := Execute(tc.code)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected output: %s, but got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package bf implements a minimalist Brainfuck virtual machine in Gno.\n//\n// Brainfuck is an esoteric programming language known for its simplicity and minimalistic design.\n// It operates on an array of memory cells, with a memory pointer that can move left or right.\n// The language consists of eight commands: \u003e \u003c + - . , [ ].\n//\n// Usage:\n// To execute Brainfuck code, use the Execute function and provide the code as a string.\n//\n//\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n//\toutput := bf.Execute(code)\n//\n// Note:\n// This implementation is a minimalist version and may not handle all edge cases or advanced features of the Brainfuck language.\n//\n// Reference:\n// For more information on Brainfuck, refer to the Wikipedia page: https://en.wikipedia.org/wiki/Brainfuck\npackage bf // import \"gno.land/p/demo/bf\"\n" + }, + { + "name": "run.gno", + "body": "package bf\n\n// for `gno run`\nfunc main() {\n\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n\t// TODO: code = os.Args...\n\tExecute(code)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "mux", + "path": "gno.land/p/demo/mux", + "files": [ + { + "name": "doc.gno", + "body": "// Package mux provides a simple routing and rendering library for handling dynamic path-based requests in Gno contracts.\n//\n// The `mux` package aims to offer similar functionality to `http.ServeMux` in Go, but for Gno's Render() requests.\n// It allows you to define routes with dynamic parts and associate them with corresponding handler functions for rendering outputs.\n//\n// Usage:\n// 1. Create a new Router instance using `NewRouter()` to handle routing and rendering logic.\n// 2. Register routes and their associated handler functions using the `Handle(route, handler)` method.\n// 3. Implement the rendering logic within the handler functions, utilizing the `Request` and `ResponseWriter` types.\n// 4. Use the `Render(path)` method to process a given path and execute the corresponding handler function to obtain the rendered output.\n//\n// Route Patterns:\n// Routes can include dynamic parts enclosed in braces, such as \"users/{id}\" or \"hello/{name}\". The `Request` object's `GetVar(key)`\n// method allows you to extract the value of a specific variable from the path based on routing rules.\n//\n// Example:\n//\n//\trouter := mux.NewRouter()\n//\n//\t// Define a route with a variable and associated handler function\n//\trouter.HandleFunc(\"hello/{name}\", func(res *mux.ResponseWriter, req *mux.Request) {\n//\t\tname := req.GetVar(\"name\")\n//\t\tif name != \"\" {\n//\t\t\tres.Write(\"Hello, \" + name + \"!\")\n//\t\t} else {\n//\t\t\tres.Write(\"Hello, world!\")\n//\t\t}\n//\t})\n//\n//\t// Render the output for the \"/hello/Alice\" path\n//\toutput := router.Render(\"hello/Alice\")\n//\t// Output: \"Hello, Alice!\"\n//\n// Note: The `mux` package provides a basic routing and rendering mechanism for simple use cases. For more advanced routing features,\n// consider using more specialized libraries or frameworks.\npackage mux\n" + }, + { + "name": "handler.gno", + "body": "package mux\n\ntype Handler struct {\n\tPattern string\n\tFn HandlerFunc\n}\n\ntype HandlerFunc func(*ResponseWriter, *Request)\n\n// TODO: type ErrHandlerFunc func(*ResponseWriter, *Request) error\n// TODO: NotFoundHandler\n// TODO: AutomaticIndex\n" + }, + { + "name": "helpers.gno", + "body": "package mux\n\nfunc defaultNotFoundHandler(res *ResponseWriter, req *Request) {\n\tres.Write(\"404\")\n}\n" + }, + { + "name": "request.gno", + "body": "package mux\n\nimport \"strings\"\n\n// Request represents an incoming request.\ntype Request struct {\n\tPath string\n\tHandlerPath string\n}\n\n// GetVar retrieves a variable from the path based on routing rules.\nfunc (r *Request) GetVar(key string) string {\n\tvar (\n\t\thandlerParts = strings.Split(r.HandlerPath, \"/\")\n\t\treqParts = strings.Split(r.Path, \"/\")\n\t)\n\n\tfor i := 0; i \u003c len(handlerParts); i++ {\n\t\thandlerPart := handlerParts[i]\n\t\tswitch {\n\t\tcase handlerPart == \"*\":\n\t\t\t// XXX: implement a/b/*/d/e\n\t\t\tpanic(\"not implemented\")\n\t\tcase strings.HasPrefix(handlerPart, \"{\") \u0026\u0026 strings.HasSuffix(handlerPart, \"}\"):\n\t\t\tparameter := handlerPart[1 : len(handlerPart)-1]\n\t\t\tif parameter == key {\n\t\t\t\treturn reqParts[i]\n\t\t\t}\n\t\tdefault:\n\t\t\t// continue\n\t\t}\n\t}\n\n\treturn \"\"\n}\n" + }, + { + "name": "request_test.gno", + "body": "package mux\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestRequest_GetVar(t *testing.T) {\n\tcases := []struct {\n\t\thandlerPath string\n\t\treqPath string\n\t\tgetVarKey string\n\t\texpectedOutput string\n\t}{\n\t\t{\"users/{id}\", \"users/123\", \"id\", \"123\"},\n\t\t{\"users/123\", \"users/123\", \"id\", \"\"},\n\t\t{\"users/{id}\", \"users/123\", \"nonexistent\", \"\"},\n\t\t{\"a/{b}/c/{d}\", \"a/42/c/1337\", \"b\", \"42\"},\n\t\t{\"a/{b}/c/{d}\", \"a/42/c/1337\", \"d\", \"1337\"},\n\t\t{\"{a}\", \"foo\", \"a\", \"foo\"},\n\t\t// TODO: wildcards: a/*/c\n\t\t// TODO: multiple patterns per slashes: a/{b}-{c}/d\n\t}\n\n\tfor _, tt := range cases {\n\t\tname := fmt.Sprintf(\"%s-%s\", tt.handlerPath, tt.reqPath)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\treq := \u0026Request{\n\t\t\t\tHandlerPath: tt.handlerPath,\n\t\t\t\tPath: tt.reqPath,\n\t\t\t}\n\n\t\t\toutput := req.GetVar(tt.getVarKey)\n\t\t\tif output != tt.expectedOutput {\n\t\t\t\tt.Errorf(\"Expected '%q, but got %q\", tt.expectedOutput, output)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "response.gno", + "body": "package mux\n\nimport \"strings\"\n\n// ResponseWriter represents the response writer.\ntype ResponseWriter struct {\n\toutput strings.Builder\n}\n\n// Write appends data to the response output.\nfunc (rw *ResponseWriter) Write(data string) {\n\trw.output.WriteString(data)\n}\n\n// Output returns the final response output.\nfunc (rw *ResponseWriter) Output() string {\n\treturn rw.output.String()\n}\n\n// TODO: func (rw *ResponseWriter) Header()...\n" + }, + { + "name": "router.gno", + "body": "package mux\n\nimport \"strings\"\n\n// Router handles the routing and rendering logic.\ntype Router struct {\n\troutes []Handler\n\tNotFoundHandler HandlerFunc\n}\n\n// NewRouter creates a new Router instance.\nfunc NewRouter() *Router {\n\treturn \u0026Router{\n\t\troutes: make([]Handler, 0),\n\t\tNotFoundHandler: defaultNotFoundHandler,\n\t}\n}\n\n// Render renders the output for the given path using the registered route handler.\nfunc (r *Router) Render(reqPath string) string {\n\treqParts := strings.Split(reqPath, \"/\")\n\n\tfor _, route := range r.routes {\n\t\tpatParts := strings.Split(route.Pattern, \"/\")\n\n\t\tif len(patParts) != len(reqParts) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatch := true\n\t\tfor i := 0; i \u003c len(patParts); i++ {\n\t\t\tpatPart := patParts[i]\n\t\t\treqPart := reqParts[i]\n\n\t\t\tif patPart == \"*\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(patPart, \"{\") \u0026\u0026 strings.HasSuffix(patPart, \"}\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif patPart != reqPart {\n\t\t\t\tmatch = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif match {\n\t\t\treq := \u0026Request{\n\t\t\t\tPath: reqPath,\n\t\t\t\tHandlerPath: route.Pattern,\n\t\t\t}\n\t\t\tres := \u0026ResponseWriter{}\n\t\t\troute.Fn(res, req)\n\t\t\treturn res.Output()\n\t\t}\n\t}\n\n\t// not found\n\treq := \u0026Request{Path: reqPath}\n\tres := \u0026ResponseWriter{}\n\tr.NotFoundHandler(res, req)\n\treturn res.Output()\n}\n\n// Handle registers a route and its handler function.\nfunc (r *Router) HandleFunc(pattern string, fn HandlerFunc) {\n\troute := Handler{Pattern: pattern, Fn: fn}\n\tr.routes = append(r.routes, route)\n}\n" + }, + { + "name": "router_test.gno", + "body": "package mux\n\nimport \"testing\"\n\nfunc TestRouter_Render(t *testing.T) {\n\t// Define handlers and route configuration\n\trouter := NewRouter()\n\trouter.HandleFunc(\"hello/{name}\", func(res *ResponseWriter, req *Request) {\n\t\tname := req.GetVar(\"name\")\n\t\tif name != \"\" {\n\t\t\tres.Write(\"Hello, \" + name + \"!\")\n\t\t} else {\n\t\t\tres.Write(\"Hello, world!\")\n\t\t}\n\t})\n\trouter.HandleFunc(\"hi\", func(res *ResponseWriter, req *Request) {\n\t\tres.Write(\"Hi, earth!\")\n\t})\n\n\tcases := []struct {\n\t\tpath string\n\t\texpectedOutput string\n\t}{\n\t\t{\"hello/Alice\", \"Hello, Alice!\"},\n\t\t{\"hi\", \"Hi, earth!\"},\n\t\t{\"hello/Bob\", \"Hello, Bob!\"},\n\t\t// TODO: {\"hello\", \"Hello, world!\"},\n\t\t// TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc\n\t}\n\tfor _, tt := range cases {\n\t\tt.Run(tt.path, func(t *testing.T) {\n\t\t\toutput := router.Render(tt.path)\n\t\t\tif output != tt.expectedOutput {\n\t\t\t\tt.Errorf(\"Expected output %q, but got %q\", tt.expectedOutput, output)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "blog", + "path": "gno.land/p/demo/blog", + "files": [ + { + "name": "blog.gno", + "body": "package blog\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Blog struct {\n\tTitle string\n\tPrefix string // i.e. r/gnoland/blog:\n\tPosts avl.Tree // slug -\u003e *Post\n\tPostsPublished avl.Tree // published-date -\u003e *Post\n\tPostsAlphabetical avl.Tree // title -\u003e *Post\n\tNoBreadcrumb bool\n}\n\nfunc (b Blog) RenderLastPostsWidget(limit int) string {\n\tif b.PostsPublished.Size() == 0 {\n\t\treturn \"No posts.\"\n\t}\n\n\toutput := \"\"\n\ti := 0\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tp := value.(*Post)\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", p.Title, p.URL())\n\t\ti++\n\t\treturn i \u003e= limit\n\t})\n\treturn output\n}\n\nfunc (b Blog) RenderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tif !b.NoBreadcrumb {\n\t\tres.Write(breadcrumb([]string{b.Title}))\n\t}\n\n\tif b.Posts.Size() == 0 {\n\t\tres.Write(\"No posts.\")\n\t\treturn\n\t}\n\n\tres.Write(\"\u003cdiv class='columns-3'\u003e\")\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpost := value.(*Post)\n\t\tres.Write(post.RenderListItem())\n\t\treturn false\n\t})\n\tres.Write(\"\u003c/div\u003e\")\n\n\t// FIXME: tag list/cloud.\n}\n\nfunc (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) {\n\tslug := req.GetVar(\"slug\")\n\n\tpost, found := b.Posts.Get(slug)\n\tif !found {\n\t\tres.Write(\"404\")\n\t\treturn\n\t}\n\tp := post.(*Post)\n\n\tres.Write(\"\u003cmain class='gno-tmpl-page'\u003e\" + \"\\n\\n\")\n\n\tres.Write(\"# \" + p.Title + \"\\n\\n\")\n\tres.Write(p.Body + \"\\n\\n\")\n\tres.Write(\"---\\n\\n\")\n\n\tres.Write(p.RenderTagList() + \"\\n\\n\")\n\tres.Write(p.RenderAuthorList() + \"\\n\\n\")\n\tres.Write(p.RenderPublishData() + \"\\n\\n\")\n\n\tres.Write(\"---\\n\")\n\tres.Write(\"\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\\n\\n\")\n\n\t// comments\n\tp.Comments.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tcomment := value.(*Comment)\n\t\tres.Write(comment.RenderListItem())\n\t\treturn false\n\t})\n\n\tres.Write(\"\u003c/details\u003e\\n\")\n\tres.Write(\"\u003c/main\u003e\")\n}\n\nfunc (b Blog) RenderTag(res *mux.ResponseWriter, req *mux.Request) {\n\tslug := req.GetVar(\"slug\")\n\n\tif slug == \"\" {\n\t\tres.Write(\"404\")\n\t\treturn\n\t}\n\n\tif !b.NoBreadcrumb {\n\t\tbreadStr := breadcrumb([]string{\n\t\t\tufmt.Sprintf(\"[%s](%s)\", b.Title, b.Prefix),\n\t\t\t\"t\",\n\t\t\tslug,\n\t\t})\n\t\tres.Write(breadStr)\n\t}\n\n\tnb := 0\n\tb.Posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpost := value.(*Post)\n\t\tif !post.HasTag(slug) {\n\t\t\treturn false\n\t\t}\n\t\tres.Write(post.RenderListItem())\n\t\tnb++\n\t\treturn false\n\t})\n\tif nb == 0 {\n\t\tres.Write(\"No posts.\")\n\t}\n}\n\nfunc (b Blog) Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", b.RenderHome)\n\trouter.HandleFunc(\"p/{slug}\", b.RenderPost)\n\trouter.HandleFunc(\"t/{slug}\", b.RenderTag)\n\treturn router.Render(path)\n}\n\nfunc (b *Blog) NewPost(publisher std.Address, slug, title, body, pubDate string, authors, tags []string) error {\n\tif _, found := b.Posts.Get(slug); found {\n\t\treturn ErrPostSlugExists\n\t}\n\n\tvar parsedTime time.Time\n\tvar err error\n\tif pubDate != \"\" {\n\t\tparsedTime, err = time.Parse(time.RFC3339, pubDate)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// If no publication date was passed in by caller, take current block time\n\t\tparsedTime = time.Now()\n\t}\n\n\tpost := \u0026Post{\n\t\tPublisher: publisher,\n\t\tAuthors: authors,\n\t\tSlug: slug,\n\t\tTitle: title,\n\t\tBody: body,\n\t\tTags: tags,\n\t\tCreatedAt: parsedTime,\n\t}\n\n\treturn b.prepareAndSetPost(post, false)\n}\n\nfunc (b *Blog) prepareAndSetPost(post *Post, edit bool) error {\n\tpost.Title = strings.TrimSpace(post.Title)\n\tpost.Body = strings.TrimSpace(post.Body)\n\n\tif post.Title == \"\" {\n\t\treturn ErrPostTitleMissing\n\t}\n\tif post.Body == \"\" {\n\t\treturn ErrPostBodyMissing\n\t}\n\tif post.Slug == \"\" {\n\t\treturn ErrPostSlugMissing\n\t}\n\n\tpost.Blog = b\n\tpost.UpdatedAt = time.Now()\n\n\ttrimmedTitleKey := getTitleKey(post.Title)\n\tpubDateKey := getPublishedKey(post.CreatedAt)\n\n\tif !edit {\n\t\t// Cannot have two posts with same title key\n\t\tif _, found := b.PostsAlphabetical.Get(trimmedTitleKey); found {\n\t\t\treturn ErrPostTitleExists\n\t\t}\n\t\t// Cannot have two posts with *exact* same timestamp\n\t\tif _, found := b.PostsPublished.Get(pubDateKey); found {\n\t\t\treturn ErrPostPubDateExists\n\t\t}\n\t}\n\n\t// Store post under keys\n\tb.PostsAlphabetical.Set(trimmedTitleKey, post)\n\tb.PostsPublished.Set(pubDateKey, post)\n\tb.Posts.Set(post.Slug, post)\n\n\treturn nil\n}\n\nfunc (b *Blog) RemovePost(slug string) {\n\tp, exists := b.Posts.Get(slug)\n\tif !exists {\n\t\tpanic(\"post with specified slug doesn't exist\")\n\t}\n\n\tpost := p.(*Post)\n\n\ttitleKey := getTitleKey(post.Title)\n\tpublishedKey := getPublishedKey(post.CreatedAt)\n\n\t_, _ = b.Posts.Remove(slug)\n\t_, _ = b.PostsAlphabetical.Remove(titleKey)\n\t_, _ = b.PostsPublished.Remove(publishedKey)\n}\n\nfunc (b *Blog) GetPost(slug string) *Post {\n\tpost, found := b.Posts.Get(slug)\n\tif !found {\n\t\treturn nil\n\t}\n\treturn post.(*Post)\n}\n\ntype Post struct {\n\tBlog *Blog\n\tSlug string // FIXME: save space?\n\tTitle string\n\tBody string\n\tCreatedAt time.Time\n\tUpdatedAt time.Time\n\tComments avl.Tree\n\tAuthors []string\n\tPublisher std.Address\n\tTags []string\n\tCommentIndex int\n}\n\nfunc (p *Post) Update(title, body, publicationDate string, authors, tags []string) error {\n\tp.Title = title\n\tp.Body = body\n\tp.Tags = tags\n\tp.Authors = authors\n\n\tparsedTime, err := time.Parse(time.RFC3339, publicationDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.CreatedAt = parsedTime\n\treturn p.Blog.prepareAndSetPost(p, true)\n}\n\nfunc (p *Post) AddComment(author std.Address, comment string) error {\n\tif p == nil {\n\t\treturn ErrNoSuchPost\n\t}\n\tp.CommentIndex++\n\tcommentKey := strconv.Itoa(p.CommentIndex)\n\tcomment = strings.TrimSpace(comment)\n\tp.Comments.Set(commentKey, \u0026Comment{\n\t\tPost: p,\n\t\tCreatedAt: time.Now(),\n\t\tAuthor: author,\n\t\tComment: comment,\n\t})\n\n\treturn nil\n}\n\nfunc (p *Post) DeleteComment(index int) error {\n\tif p == nil {\n\t\treturn ErrNoSuchPost\n\t}\n\tcommentKey := strconv.Itoa(index)\n\tp.Comments.Remove(commentKey)\n\treturn nil\n}\n\nfunc (p *Post) HasTag(tag string) bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\tfor _, t := range p.Tags {\n\t\tif t == tag {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (p *Post) RenderListItem() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\toutput := \"\u003cdiv\u003e\\n\\n\"\n\toutput += ufmt.Sprintf(\"### [%s](%s)\\n\", p.Title, p.URL())\n\t// output += ufmt.Sprintf(\"**[Learn More](%s)**\\n\\n\", p.URL())\n\n\toutput += \" \" + p.CreatedAt.Format(\"02 Jan 2006\")\n\t// output += p.Summary() + \"\\n\\n\"\n\t// output += p.RenderTagList() + \"\\n\\n\"\n\toutput += \"\\n\"\n\toutput += \"\u003c/div\u003e\"\n\treturn output\n}\n\n// Render post tags\nfunc (p *Post) RenderTagList() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\tif len(p.Tags) == 0 {\n\t\treturn \"\"\n\t}\n\n\toutput := \"Tags: \"\n\tfor idx, tag := range p.Tags {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" \"\n\t\t}\n\t\ttagURL := p.Blog.Prefix + \"t/\" + tag\n\t\toutput += ufmt.Sprintf(\"[#%s](%s)\", tag, tagURL)\n\n\t}\n\treturn output\n}\n\n// Render authors if there are any\nfunc (p *Post) RenderAuthorList() string {\n\tout := \"Written\"\n\tif len(p.Authors) != 0 {\n\t\tout += \" by \"\n\n\t\tfor idx, author := range p.Authors {\n\t\t\tout += author\n\t\t\tif idx \u003c len(p.Authors)-1 {\n\t\t\t\tout += \", \"\n\t\t\t}\n\t\t}\n\t}\n\tout += \" on \" + p.CreatedAt.Format(\"02 Jan 2006\")\n\n\treturn out\n}\n\nfunc (p *Post) RenderPublishData() string {\n\tout := \"Published \"\n\tif p.Publisher != \"\" {\n\t\tout += \"by \" + p.Publisher.String() + \" \"\n\t}\n\tout += \"to \" + p.Blog.Title\n\n\treturn out\n}\n\nfunc (p *Post) URL() string {\n\tif p == nil {\n\t\treturn p.Blog.Prefix + \"404\"\n\t}\n\treturn p.Blog.Prefix + \"p/\" + p.Slug\n}\n\nfunc (p *Post) Summary() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\n\t// FIXME: better summary.\n\tlines := strings.Split(p.Body, \"\\n\")\n\tif len(lines) \u003c= 3 {\n\t\treturn p.Body\n\t}\n\treturn strings.Join(lines[0:3], \"\\n\") + \"...\"\n}\n\ntype Comment struct {\n\tPost *Post\n\tCreatedAt time.Time\n\tAuthor std.Address\n\tComment string\n}\n\nfunc (c Comment) RenderListItem() string {\n\toutput := \"\u003ch5\u003e\"\n\toutput += c.Comment + \"\\n\\n\"\n\toutput += \"\u003c/h5\u003e\"\n\n\toutput += \"\u003ch6\u003e\"\n\toutput += ufmt.Sprintf(\"by %s on %s\", c.Author, c.CreatedAt.Format(time.RFC822))\n\toutput += \"\u003c/h6\u003e\\n\\n\"\n\n\toutput += \"---\\n\\n\"\n\n\treturn output\n}\n" + }, + { + "name": "blog_test.gno", + "body": "package blog\n\n// TODO: add generic tests here.\n// right now, you can checkout r/gnoland/blog/*_test.gno.\n" + }, + { + "name": "errors.gno", + "body": "package blog\n\nimport \"errors\"\n\nvar (\n\tErrPostTitleMissing = errors.New(\"post title is missing\")\n\tErrPostSlugMissing = errors.New(\"post slug is missing\")\n\tErrPostBodyMissing = errors.New(\"post body is missing\")\n\tErrPostSlugExists = errors.New(\"post with specified slug already exists\")\n\tErrPostPubDateExists = errors.New(\"post with specified publication date exists\")\n\tErrPostTitleExists = errors.New(\"post with specified title already exists\")\n\tErrNoSuchPost = errors.New(\"no such post\")\n)\n" + }, + { + "name": "util.gno", + "body": "package blog\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\nfunc breadcrumb(parts []string) string {\n\treturn \"# \" + strings.Join(parts, \" / \") + \"\\n\\n\"\n}\n\nfunc getTitleKey(title string) string {\n\treturn strings.Replace(title, \" \", \"\", -1)\n}\n\nfunc getPublishedKey(t time.Time) string {\n\treturn t.Format(time.RFC3339)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "cford32", + "path": "gno.land/p/demo/cford32", + "files": [ + { + "name": "LICENSE", + "body": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "README.md", + "body": "# cford32\n\n```\npackage cford32 // import \"gno.land/p/demo/cford32\"\n\nPackage cford32 implements a base32-like encoding/decoding package, with the\nencoding scheme specified by Douglas Crockford.\n\nFrom the website, the requirements of said encoding scheme are to:\n\n - Be human readable and machine readable.\n - Be compact. Humans have difficulty in manipulating long strings of arbitrary\n symbols.\n - Be error resistant. Entering the symbols must not require keyboarding\n gymnastics.\n - Be pronounceable. Humans should be able to accurately transmit the symbols\n to other humans using a telephone.\n\nThis is slightly different from a simple difference in encoding table from\nthe Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\nparsed as 1, and o O is parsed as 0.\n\nThis package additionally provides ways to encode uint64's efficiently, as well\nas efficient encoding to a lowercase variation of the encoding. The encodings\nnever use paddings.\n\n# Uint64 Encoding\n\nAside from lower/uppercase encoding, there is a compact encoding, allowing to\nencode all values in [0,2^34), and the full encoding, allowing all values in\n[0,2^64). The compact encoding uses 7 characters, and the full encoding uses 13\ncharacters. Both are parsed unambiguously by the Uint64 decoder.\n\nThe compact encodings have the first character between ['0','f'], while the\nfull encoding's first character ranges between ['g','z']. Practically, in your\nusage of the package, you should consider which one to use and stick with it,\nwhile considering that the compact encoding, once it reaches 2^34, automatically\nswitches to the full encoding. The properties of the generated strings are still\nmaintained: for instance, any two encoded uint64s x,y consistently generated\nwith the compact encoding, if the numeric value is x \u003c y, will also be x \u003c y in\nlexical ordering. However, values [0,2^34) have a \"double encoding\", which if\nmixed together lose the lexical ordering property.\n\nThe Uint64 encoding is most useful for generating string versions of Uint64 IDs.\nPractically, it allows you to retain sleek and compact IDs for your application\nfor the first 2^34 (\u003e17 billion) entities, while seamlessly rolling over to the\nfull encoding should you exceed that. You are encouraged to use it unless you\nhave a requirement or preferences for IDs consistently being always the same\nsize.\n\nTo use the cford32 encoding for IDs, you may want to consider using package\ngno.land/p/demo/seqid.\n\n[specified by Douglas Crockford]: https://www.crockford.com/base32.html\n\nfunc AppendCompact(id uint64, b []byte) []byte\nfunc AppendDecode(dst, src []byte) ([]byte, error)\nfunc AppendEncode(dst, src []byte) []byte\nfunc AppendEncodeLower(dst, src []byte) []byte\nfunc Decode(dst, src []byte) (n int, err error)\nfunc DecodeString(s string) ([]byte, error)\nfunc DecodedLen(n int) int\nfunc Encode(dst, src []byte)\nfunc EncodeLower(dst, src []byte)\nfunc EncodeToString(src []byte) string\nfunc EncodeToStringLower(src []byte) string\nfunc EncodedLen(n int) int\nfunc NewDecoder(r io.Reader) io.Reader\nfunc NewEncoder(w io.Writer) io.WriteCloser\nfunc NewEncoderLower(w io.Writer) io.WriteCloser\nfunc PutCompact(id uint64) []byte\nfunc PutUint64(id uint64) [13]byte\nfunc PutUint64Lower(id uint64) [13]byte\nfunc Uint64(b []byte) (uint64, error)\ntype CorruptInputError int64\n```\n" + }, + { + "name": "cford32.gno", + "body": "// Modified from the Go Source code for encoding/base32.\n// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package cford32 implements a base32-like encoding/decoding package, with the\n// encoding scheme [specified by Douglas Crockford].\n//\n// From the website, the requirements of said encoding scheme are to:\n//\n// - Be human readable and machine readable.\n// - Be compact. Humans have difficulty in manipulating long strings of arbitrary symbols.\n// - Be error resistant. Entering the symbols must not require keyboarding gymnastics.\n// - Be pronounceable. Humans should be able to accurately transmit the symbols to other humans using a telephone.\n//\n// This is slightly different from a simple difference in encoding table from\n// the Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\n// parsed as 1, and o O is parsed as 0.\n//\n// This package additionally provides ways to encode uint64's efficiently,\n// as well as efficient encoding to a lowercase variation of the encoding.\n// The encodings never use paddings.\n//\n// # Uint64 Encoding\n//\n// Aside from lower/uppercase encoding, there is a compact encoding, allowing\n// to encode all values in [0,2^34), and the full encoding, allowing all\n// values in [0,2^64). The compact encoding uses 7 characters, and the full\n// encoding uses 13 characters. Both are parsed unambiguously by the Uint64\n// decoder.\n//\n// The compact encodings have the first character between ['0','f'], while the\n// full encoding's first character ranges between ['g','z']. Practically, in\n// your usage of the package, you should consider which one to use and stick\n// with it, while considering that the compact encoding, once it reaches 2^34,\n// automatically switches to the full encoding. The properties of the generated\n// strings are still maintained: for instance, any two encoded uint64s x,y\n// consistently generated with the compact encoding, if the numeric value is\n// x \u003c y, will also be x \u003c y in lexical ordering. However, values [0,2^34) have a\n// \"double encoding\", which if mixed together lose the lexical ordering property.\n//\n// The Uint64 encoding is most useful for generating string versions of Uint64\n// IDs. Practically, it allows you to retain sleek and compact IDs for your\n// application for the first 2^34 (\u003e17 billion) entities, while seamlessly\n// rolling over to the full encoding should you exceed that. You are encouraged\n// to use it unless you have a requirement or preferences for IDs consistently\n// being always the same size.\n//\n// To use the cford32 encoding for IDs, you may want to consider using package\n// [gno.land/p/demo/seqid].\n//\n// [specified by Douglas Crockford]: https://www.crockford.com/base32.html\npackage cford32\n\nimport (\n\t\"io\"\n\t\"strconv\"\n)\n\nconst (\n\tencTable = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"\n\tencTableLower = \"0123456789abcdefghjkmnpqrstvwxyz\"\n\n\t// each line is 16 bytes\n\tdecTable = \"\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 00-0f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 10-1f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 20-2f\n\t\t\"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\xff\\xff\\xff\\xff\\xff\\xff\" + // 30-3f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 40-4f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 50-5f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 60-6f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 70-7f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 80-ff (not ASCII)\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n)\n\n// CorruptInputError is returned by parsing functions when an invalid character\n// in the input is found. The integer value represents the byte index where\n// the error occurred.\n//\n// This is typically because the given character does not exist in the encoding.\ntype CorruptInputError int64\n\nfunc (e CorruptInputError) Error() string {\n\treturn \"illegal cford32 data at input byte \" + strconv.FormatInt(int64(e), 10)\n}\n\n// Uint64 parses a cford32-encoded byte slice into a uint64.\n//\n// - The parser requires all provided character to be valid cford32 characters.\n// - The parser disregards case.\n// - If the first character is '0' \u003c= c \u003c= 'f', then the passed value is assumed\n// encoded in the compact encoding, and must be 7 characters long.\n// - If the first character is 'g' \u003c= c \u003c= 'z', then the passed value is\n// assumed encoded in the full encoding, and must be 13 characters long.\n//\n// If any of these requirements fail, a CorruptInputError will be returned.\nfunc Uint64(b []byte) (uint64, error) {\n\tswitch {\n\tdefault:\n\t\treturn 0, CorruptInputError(0)\n\tcase len(b) == 7 \u0026\u0026 b[0] \u003e= '0' \u0026\u0026 b[0] \u003c= 'f':\n\t\tdecVals := [7]byte{\n\t\t\tdecTable[b[0]],\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c30 |\n\t\t\tuint64(decVals[1])\u003c\u003c25 |\n\t\t\tuint64(decVals[2])\u003c\u003c20 |\n\t\t\tuint64(decVals[3])\u003c\u003c15 |\n\t\t\tuint64(decVals[4])\u003c\u003c10 |\n\t\t\tuint64(decVals[5])\u003c\u003c5 |\n\t\t\tuint64(decVals[6]), nil\n\tcase len(b) == 13 \u0026\u0026 b[0] \u003e= 'g' \u0026\u0026 b[0] \u003c= 'z':\n\t\tdecVals := [13]byte{\n\t\t\tdecTable[b[0]] \u0026 0x0F, // disregard high bit\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t\tdecTable[b[7]],\n\t\t\tdecTable[b[8]],\n\t\t\tdecTable[b[9]],\n\t\t\tdecTable[b[10]],\n\t\t\tdecTable[b[11]],\n\t\t\tdecTable[b[12]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c60 |\n\t\t\tuint64(decVals[1])\u003c\u003c55 |\n\t\t\tuint64(decVals[2])\u003c\u003c50 |\n\t\t\tuint64(decVals[3])\u003c\u003c45 |\n\t\t\tuint64(decVals[4])\u003c\u003c40 |\n\t\t\tuint64(decVals[5])\u003c\u003c35 |\n\t\t\tuint64(decVals[6])\u003c\u003c30 |\n\t\t\tuint64(decVals[7])\u003c\u003c25 |\n\t\t\tuint64(decVals[8])\u003c\u003c20 |\n\t\t\tuint64(decVals[9])\u003c\u003c15 |\n\t\t\tuint64(decVals[10])\u003c\u003c10 |\n\t\t\tuint64(decVals[11])\u003c\u003c5 |\n\t\t\tuint64(decVals[12]), nil\n\t}\n}\n\nconst mask = 31\n\n// PutUint64 returns a cford32-encoded byte slice.\nfunc PutUint64(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTable[id\u003e\u003e60\u0026mask|0x10], // specify full encoding\n\t\tencTable[id\u003e\u003e55\u0026mask],\n\t\tencTable[id\u003e\u003e50\u0026mask],\n\t\tencTable[id\u003e\u003e45\u0026mask],\n\t\tencTable[id\u003e\u003e40\u0026mask],\n\t\tencTable[id\u003e\u003e35\u0026mask],\n\t\tencTable[id\u003e\u003e30\u0026mask],\n\t\tencTable[id\u003e\u003e25\u0026mask],\n\t\tencTable[id\u003e\u003e20\u0026mask],\n\t\tencTable[id\u003e\u003e15\u0026mask],\n\t\tencTable[id\u003e\u003e10\u0026mask],\n\t\tencTable[id\u003e\u003e5\u0026mask],\n\t\tencTable[id\u0026mask],\n\t}\n}\n\n// PutUint64Lower returns a cford32-encoded byte array, swapping uppercase\n// letters with lowercase.\n//\n// For more information on how the value is encoded, see [Uint64].\nfunc PutUint64Lower(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t}\n}\n\n// PutCompact returns a cford32-encoded byte slice, using the compact\n// representation of cford32 described in the package documentation where\n// possible (all values of id \u003c 1\u003c\u003c34). The lowercase encoding is used.\n//\n// The resulting byte slice will be 7 bytes long for all compact values,\n// and 13 bytes long for\nfunc PutCompact(id uint64) []byte {\n\treturn AppendCompact(id, nil)\n}\n\n// AppendCompact works like [PutCompact] but appends to the given byte slice\n// instead of allocating one anew.\nfunc AppendCompact(id uint64, b []byte) []byte {\n\tconst maxCompact = 1 \u003c\u003c 34\n\tif id \u003c maxCompact {\n\t\treturn append(b,\n\t\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\t\tencTableLower[id\u0026mask],\n\t\t)\n\t}\n\treturn append(b,\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t)\n}\n\nfunc DecodedLen(n int) int {\n\treturn n/8*5 + n%8*5/8\n}\n\nfunc EncodedLen(n int) int {\n\treturn n/5*8 + (n%5*8+4)/5\n}\n\n// Encode encodes src using the encoding enc,\n// writing [EncodedLen](len(src)) bytes to dst.\n//\n// The encoding does not contain any padding, unlike Go's base32.\nfunc Encode(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTable[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTable[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTable[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTable[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTable[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTable[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTable[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTable[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTable[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTable[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTable[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTable[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTable[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTable[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTable[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// EncodeLower is like [Encode], but uses the lowercase\nfunc EncodeLower(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTableLower[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTableLower[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTableLower[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTableLower[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTableLower[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTableLower[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTableLower[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTableLower[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTableLower[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTableLower[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTableLower[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTableLower[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTableLower[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTableLower[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTableLower[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// AppendEncode appends the cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncode(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncode(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\n// AppendEncodeLower appends the lowercase cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncodeLower(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncodeLower(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\nfunc grow(s []byte, n int) []byte {\n\t// slices.Grow\n\tif n -= cap(s) - len(s); n \u003e 0 {\n\t\tnews := make([]byte, cap(s)+n)\n\t\tcopy(news[:cap(s)], s[:cap(s)])\n\t\treturn news[:len(s)]\n\t}\n\treturn s\n}\n\n// EncodeToString returns the cford32 encoding of src.\nfunc EncodeToString(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncode(buf, src)\n\treturn string(buf)\n}\n\n// EncodeToStringLower returns the cford32 lowercase encoding of src.\nfunc EncodeToStringLower(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncodeLower(buf, src)\n\treturn string(buf)\n}\n\nfunc decode(dst, src []byte) (n int, err error) {\n\tdsti := 0\n\tolen := len(src)\n\n\tfor len(src) \u003e 0 {\n\t\t// Decode quantum using the base32 alphabet\n\t\tvar dbuf [8]byte\n\t\tdlen := 8\n\n\t\tfor j := 0; j \u003c 8; {\n\t\t\tif len(src) == 0 {\n\t\t\t\t// We have reached the end and are not expecting any padding\n\t\t\t\tdlen = j\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tin := src[0]\n\t\t\tsrc = src[1:]\n\t\t\tdbuf[j] = decTable[in]\n\t\t\tif dbuf[j] == 0xFF {\n\t\t\t\treturn n, CorruptInputError(olen - len(src) - 1)\n\t\t\t}\n\t\t\tj++\n\t\t}\n\n\t\t// Pack 8x 5-bit source blocks into 5 byte destination\n\t\t// quantum\n\t\tswitch dlen {\n\t\tcase 8:\n\t\t\tdst[dsti+4] = dbuf[6]\u003c\u003c5 | dbuf[7]\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 7:\n\t\t\tdst[dsti+3] = dbuf[4]\u003c\u003c7 | dbuf[5]\u003c\u003c2 | dbuf[6]\u003e\u003e3\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 5:\n\t\t\tdst[dsti+2] = dbuf[3]\u003c\u003c4 | dbuf[4]\u003e\u003e1\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 4:\n\t\t\tdst[dsti+1] = dbuf[1]\u003c\u003c6 | dbuf[2]\u003c\u003c1 | dbuf[3]\u003e\u003e4\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 2:\n\t\t\tdst[dsti+0] = dbuf[0]\u003c\u003c3 | dbuf[1]\u003e\u003e2\n\t\t\tn++\n\t\t}\n\t\tdsti += 5\n\t}\n\treturn n, nil\n}\n\ntype encoder struct {\n\terr error\n\tw io.Writer\n\tenc func(dst, src []byte)\n\tbuf [5]byte // buffered data waiting to be encoded\n\tnbuf int // number of bytes in buf\n\tout [1024]byte // output buffer\n}\n\nfunc NewEncoder(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: Encode}\n}\n\nfunc NewEncoderLower(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: EncodeLower}\n}\n\nfunc (e *encoder) Write(p []byte) (n int, err error) {\n\tif e.err != nil {\n\t\treturn 0, e.err\n\t}\n\n\t// Leading fringe.\n\tif e.nbuf \u003e 0 {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(p) \u0026\u0026 e.nbuf \u003c 5; i++ {\n\t\t\te.buf[e.nbuf] = p[i]\n\t\t\te.nbuf++\n\t\t}\n\t\tn += i\n\t\tp = p[i:]\n\t\tif e.nbuf \u003c 5 {\n\t\t\treturn\n\t\t}\n\t\te.enc(e.out[0:], e.buf[0:])\n\t\tif _, e.err = e.w.Write(e.out[0:8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\te.nbuf = 0\n\t}\n\n\t// Large interior chunks.\n\tfor len(p) \u003e= 5 {\n\t\tnn := len(e.out) / 8 * 5\n\t\tif nn \u003e len(p) {\n\t\t\tnn = len(p)\n\t\t\tnn -= nn % 5\n\t\t}\n\t\te.enc(e.out[0:], p[0:nn])\n\t\tif _, e.err = e.w.Write(e.out[0 : nn/5*8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\tn += nn\n\t\tp = p[nn:]\n\t}\n\n\t// Trailing fringe.\n\tcopy(e.buf[:], p)\n\te.nbuf = len(p)\n\tn += len(p)\n\treturn\n}\n\n// Close flushes any pending output from the encoder.\n// It is an error to call Write after calling Close.\nfunc (e *encoder) Close() error {\n\t// If there's anything left in the buffer, flush it out\n\tif e.err == nil \u0026\u0026 e.nbuf \u003e 0 {\n\t\te.enc(e.out[0:], e.buf[0:e.nbuf])\n\t\tencodedLen := EncodedLen(e.nbuf)\n\t\te.nbuf = 0\n\t\t_, e.err = e.w.Write(e.out[0:encodedLen])\n\t}\n\treturn e.err\n}\n\n// Decode decodes src using cford32. It writes at most\n// [DecodedLen](len(src)) bytes to dst and returns the number of bytes\n// written. If src contains invalid cford32 data, it will return the\n// number of bytes successfully written and [CorruptInputError].\n// Newline characters (\\r and \\n) are ignored.\nfunc Decode(dst, src []byte) (n int, err error) {\n\tbuf := make([]byte, len(src))\n\tl := stripNewlines(buf, src)\n\treturn decode(dst, buf[:l])\n}\n\n// AppendDecode appends the cford32 decoded src to dst\n// and returns the extended buffer.\n// If the input is malformed, it returns the partially decoded src and an error.\nfunc AppendDecode(dst, src []byte) ([]byte, error) {\n\tn := DecodedLen(len(src))\n\n\tdst = grow(dst, n)\n\tdstsl := dst[len(dst) : len(dst)+n]\n\tn, err := Decode(dstsl, src)\n\treturn dst[:len(dst)+n], err\n}\n\n// DecodeString returns the bytes represented by the cford32 string s.\nfunc DecodeString(s string) ([]byte, error) {\n\tbuf := []byte(s)\n\tl := stripNewlines(buf, buf)\n\tn, err := decode(buf, buf[:l])\n\treturn buf[:n], err\n}\n\n// stripNewlines removes newline characters and returns the number\n// of non-newline characters copied to dst.\nfunc stripNewlines(dst, src []byte) int {\n\toffset := 0\n\tfor _, b := range src {\n\t\tif b == '\\r' || b == '\\n' {\n\t\t\tcontinue\n\t\t}\n\t\tdst[offset] = b\n\t\toffset++\n\t}\n\treturn offset\n}\n\ntype decoder struct {\n\terr error\n\tr io.Reader\n\tbuf [1024]byte // leftover input\n\tnbuf int\n\tout []byte // leftover decoded output\n\toutbuf [1024 / 8 * 5]byte\n}\n\n// NewDecoder constructs a new base32 stream decoder.\nfunc NewDecoder(r io.Reader) io.Reader {\n\treturn \u0026decoder{r: \u0026newlineFilteringReader{r}}\n}\n\nfunc readEncodedData(r io.Reader, buf []byte) (n int, err error) {\n\tfor n \u003c 1 \u0026\u0026 err == nil {\n\t\tvar nn int\n\t\tnn, err = r.Read(buf[n:])\n\t\tn += nn\n\t}\n\treturn\n}\n\nfunc (d *decoder) Read(p []byte) (n int, err error) {\n\t// Use leftover decoded output from last read.\n\tif len(d.out) \u003e 0 {\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t\tif len(d.out) == 0 {\n\t\t\treturn n, d.err\n\t\t}\n\t\treturn n, nil\n\t}\n\n\tif d.err != nil {\n\t\treturn 0, d.err\n\t}\n\n\t// Read nn bytes from input, bounded [8,len(d.buf)]\n\tnn := (len(p)/5 + 1) * 8\n\tif nn \u003e len(d.buf) {\n\t\tnn = len(d.buf)\n\t}\n\n\tnn, d.err = readEncodedData(d.r, d.buf[d.nbuf:nn])\n\td.nbuf += nn\n\tif d.nbuf \u003c 1 {\n\t\treturn 0, d.err\n\t}\n\n\t// Decode chunk into p, or d.out and then p if p is too small.\n\tnr := d.nbuf\n\tif d.err != io.EOF \u0026\u0026 nr%8 != 0 {\n\t\tnr -= nr % 8\n\t}\n\tnw := DecodedLen(d.nbuf)\n\n\tif nw \u003e len(p) {\n\t\tnw, err = decode(d.outbuf[0:], d.buf[0:nr])\n\t\td.out = d.outbuf[0:nw]\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t} else {\n\t\tn, err = decode(p, d.buf[0:nr])\n\t}\n\td.nbuf -= nr\n\tfor i := 0; i \u003c d.nbuf; i++ {\n\t\td.buf[i] = d.buf[i+nr]\n\t}\n\n\tif err != nil \u0026\u0026 (d.err == nil || d.err == io.EOF) {\n\t\td.err = err\n\t}\n\n\tif len(d.out) \u003e 0 {\n\t\t// We cannot return all the decoded bytes to the caller in this\n\t\t// invocation of Read, so we return a nil error to ensure that Read\n\t\t// will be called again. The error stored in d.err, if any, will be\n\t\t// returned with the last set of decoded bytes.\n\t\treturn n, nil\n\t}\n\n\treturn n, d.err\n}\n\ntype newlineFilteringReader struct {\n\twrapped io.Reader\n}\n\nfunc (r *newlineFilteringReader) Read(p []byte) (int, error) {\n\tn, err := r.wrapped.Read(p)\n\tfor n \u003e 0 {\n\t\ts := p[0:n]\n\t\toffset := stripNewlines(s, s)\n\t\tif err != nil || offset \u003e 0 {\n\t\t\treturn offset, err\n\t\t}\n\t\t// Previous buffer entirely whitespace, read again\n\t\tn, err = r.wrapped.Read(p)\n\t}\n\treturn n, err\n}\n" + }, + { + "name": "cford32_test.gno", + "body": "package cford32\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestCompactRoundtrip(t *testing.T) {\n\tbuf := make([]byte, 13)\n\tprev := make([]byte, 13)\n\tfor i := uint64(0); i \u003c (1 \u003c\u003c 12); i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n\tfor i := uint64(1\u003c\u003c34 - 1024); i \u003c (1\u003c\u003c34 + 1024); i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\t// println(string(res))\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n\tfor i := uint64(1\u003c\u003c64 - 5000); i != 0; i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n}\n\nfunc BenchmarkCompact(b *testing.B) {\n\tbuf := make([]byte, 13)\n\tfor i := 0; i \u003c b.N; i++ {\n\t\t_ = AppendCompact(uint64(i), buf[:0])\n\t}\n}\n\ntype testpair struct {\n\tdecoded, encoded string\n}\n\nvar pairs = []testpair{\n\t{\"\", \"\"},\n\t{\"f\", \"CR\"},\n\t{\"fo\", \"CSQG\"},\n\t{\"foo\", \"CSQPY\"},\n\t{\"foob\", \"CSQPYRG\"},\n\t{\"fooba\", \"CSQPYRK1\"},\n\t{\"foobar\", \"CSQPYRK1E8\"},\n\n\t{\"sure.\", \"EDTQ4S9E\"},\n\t{\"sure\", \"EDTQ4S8\"},\n\t{\"sur\", \"EDTQ4\"},\n\t{\"su\", \"EDTG\"},\n\t{\"leasure.\", \"DHJP2WVNE9JJW\"},\n\t{\"easure.\", \"CNGQ6XBJCMQ0\"},\n\t{\"asure.\", \"C5SQAWK55R\"},\n}\n\nvar bigtest = testpair{\n\t\"Twas brillig, and the slithy toves\",\n\t\"AHVP2WS0C9S6JV3CD5KJR831DSJ20X38CMG76V39EHM7J83MDXV6AWR\",\n}\n\nfunc testEqual(t *testing.T, msg string, args ...interface{}) bool {\n\tt.Helper()\n\tif args[len(args)-2] != args[len(args)-1] {\n\t\tt.Errorf(msg, args...)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc TestEncode(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tgot := EncodeToString([]byte(p.decoded))\n\t\ttestEqual(t, \"Encode(%q) = %q, want %q\", p.decoded, got, p.encoded)\n\t\tdst := AppendEncode([]byte(\"lead\"), []byte(p.decoded))\n\t\ttestEqual(t, `AppendEncode(\"lead\", %q) = %q, want %q`, p.decoded, string(dst), \"lead\"+p.encoded)\n\t}\n}\n\nfunc TestEncoder(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tbb := \u0026strings.Builder{}\n\t\tencoder := NewEncoder(bb)\n\t\tencoder.Write([]byte(p.decoded))\n\t\tencoder.Close()\n\t\ttestEqual(t, \"Encode(%q) = %q, want %q\", p.decoded, bb.String(), p.encoded)\n\t}\n}\n\nfunc TestEncoderBuffering(t *testing.T) {\n\tinput := []byte(bigtest.decoded)\n\tfor bs := 1; bs \u003c= 12; bs++ {\n\t\tbb := \u0026strings.Builder{}\n\t\tencoder := NewEncoder(bb)\n\t\tfor pos := 0; pos \u003c len(input); pos += bs {\n\t\t\tend := pos + bs\n\t\t\tif end \u003e len(input) {\n\t\t\t\tend = len(input)\n\t\t\t}\n\t\t\tn, err := encoder.Write(input[pos:end])\n\t\t\ttestEqual(t, \"Write(%q) gave error %v, want %v\", input[pos:end], err, error(nil))\n\t\t\ttestEqual(t, \"Write(%q) gave length %v, want %v\", input[pos:end], n, end-pos)\n\t\t}\n\t\terr := encoder.Close()\n\t\ttestEqual(t, \"Close gave error %v, want %v\", err, error(nil))\n\t\ttestEqual(t, \"Encoding/%d of %q = %q, want %q\", bs, bigtest.decoded, bb.String(), bigtest.encoded)\n\t}\n}\n\nfunc TestDecode(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tdbuf := make([]byte, DecodedLen(len(p.encoded)))\n\t\tcount, err := decode(dbuf, []byte(p.encoded))\n\t\ttestEqual(t, \"Decode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, \"Decode(%q) = length %v, want %v\", p.encoded, count, len(p.decoded))\n\t\ttestEqual(t, \"Decode(%q) = %q, want %q\", p.encoded, string(dbuf[0:count]), p.decoded)\n\n\t\tdbuf, err = DecodeString(p.encoded)\n\t\ttestEqual(t, \"DecodeString(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, \"DecodeString(%q) = %q, want %q\", p.encoded, string(dbuf), p.decoded)\n\n\t\t// XXX: https://github.com/gnolang/gno/issues/1570\n\t\tdst, err := AppendDecode(append([]byte(nil), []byte(\"lead\")...), []byte(p.encoded))\n\t\ttestEqual(t, \"AppendDecode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, `AppendDecode(\"lead\", %q) = %q, want %q`, p.encoded, string(dst), \"lead\"+p.decoded)\n\n\t\tdst2, err := AppendDecode(dst[:0:len(p.decoded)], []byte(p.encoded))\n\t\ttestEqual(t, \"AppendDecode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, `AppendDecode(\"\", %q) = %q, want %q`, p.encoded, string(dst2), p.decoded)\n\t\t// XXX: https://github.com/gnolang/gno/issues/1569\n\t\t// old used \u0026dst2[0] != \u0026dst[0] as a check.\n\t\tif len(dst) \u003e 0 \u0026\u0026 len(dst2) \u003e 0 \u0026\u0026 cap(dst2) != len(p.decoded) {\n\t\t\tt.Errorf(\"unexpected capacity growth: got %d, want %d\", cap(dst2), len(p.decoded))\n\t\t}\n\t}\n}\n\n// A minimal variation on strings.Reader.\n// Here, we return a io.EOF immediately on Read if the read has reached the end\n// of the reader. It's used to simplify TestDecoder.\ntype stringReader struct {\n\ts string\n\ti int64\n}\n\nfunc (r *stringReader) Read(b []byte) (n int, err error) {\n\tif r.i \u003e= int64(len(r.s)) {\n\t\treturn 0, io.EOF\n\t}\n\tn = copy(b, r.s[r.i:])\n\tr.i += int64(n)\n\tif r.i \u003e= int64(len(r.s)) {\n\t\treturn n, io.EOF\n\t}\n\treturn\n}\n\nfunc TestDecoder(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tdecoder := NewDecoder(\u0026stringReader{p.encoded, 0})\n\t\tdbuf := make([]byte, DecodedLen(len(p.encoded)))\n\t\tcount, err := decoder.Read(dbuf)\n\t\tif err != nil \u0026\u0026 err != io.EOF {\n\t\t\tt.Fatal(\"Read failed\", err)\n\t\t}\n\t\ttestEqual(t, \"Read from %q = length %v, want %v\", p.encoded, count, len(p.decoded))\n\t\ttestEqual(t, \"Decoding of %q = %q, want %q\", p.encoded, string(dbuf[0:count]), p.decoded)\n\t\tif err != io.EOF {\n\t\t\t_, err = decoder.Read(dbuf)\n\t\t}\n\t\ttestEqual(t, \"Read from %q = %v, want %v\", p.encoded, err, io.EOF)\n\t}\n}\n\ntype badReader struct {\n\tdata []byte\n\terrs []error\n\tcalled int\n\tlimit int\n}\n\n// Populates p with data, returns a count of the bytes written and an\n// error. The error returned is taken from badReader.errs, with each\n// invocation of Read returning the next error in this slice, or io.EOF,\n// if all errors from the slice have already been returned. The\n// number of bytes returned is determined by the size of the input buffer\n// the test passes to decoder.Read and will be a multiple of 8, unless\n// badReader.limit is non zero.\nfunc (b *badReader) Read(p []byte) (int, error) {\n\tlim := len(p)\n\tif b.limit != 0 \u0026\u0026 b.limit \u003c lim {\n\t\tlim = b.limit\n\t}\n\tif len(b.data) \u003c lim {\n\t\tlim = len(b.data)\n\t}\n\tfor i := range p[:lim] {\n\t\tp[i] = b.data[i]\n\t}\n\tb.data = b.data[lim:]\n\terr := io.EOF\n\tif b.called \u003c len(b.errs) {\n\t\terr = b.errs[b.called]\n\t}\n\tb.called++\n\treturn lim, err\n}\n\n// TestIssue20044 tests that decoder.Read behaves correctly when the caller\n// supplied reader returns an error.\nfunc TestIssue20044(t *testing.T) {\n\tbadErr := errors.New(\"bad reader error\")\n\ttestCases := []struct {\n\t\tr badReader\n\t\tres string\n\t\terr error\n\t\tdbuflen int\n\t}{\n\t\t// Check valid input data accompanied by an error is processed and the error is propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"d1jprv3fexqq4v34\"), errs: []error{badErr}},\n\t\t\tres: \"helloworld\", err: badErr,\n\t\t},\n\t\t// Check a read error accompanied by input data consisting of newlines only is propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"\\n\\n\\n\\n\\n\\n\\n\\n\"), errs: []error{badErr, nil}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader will be called twice. The first time it will return 8 newline characters. The\n\t\t// second time valid base32 encoded data and an error. The data should be decoded\n\t\t// correctly and the error should be propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"\\n\\n\\n\\n\\n\\n\\n\\nd1jprv3fexqq4v34\"), errs: []error{nil, badErr}},\n\t\t\tres: \"helloworld\", err: badErr, dbuflen: 8,\n\t\t},\n\t\t// Reader returns invalid input data (too short) and an error. Verify the reader\n\t\t// error is returned.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"c\"), errs: []error{badErr}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader returns invalid input data (too short) but no error. Verify io.ErrUnexpectedEOF\n\t\t// is returned.\n\t\t// NOTE(thehowl): I don't think this should applyto us?\n\t\t/* {\n\t\t\tr: badReader{data: []byte(\"c\"), errs: []error{nil}},\n\t\t\tres: \"\", err: io.ErrUnexpectedEOF,\n\t\t},*/\n\t\t// Reader returns invalid input data and an error. Verify the reader and not the\n\t\t// decoder error is returned.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"cu\"), errs: []error{badErr}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader returns valid data and io.EOF. Check data is decoded and io.EOF is propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"csqpyrk1\"), errs: []error{io.EOF}},\n\t\t\tres: \"fooba\", err: io.EOF,\n\t\t},\n\t\t// Check errors are properly reported when decoder.Read is called multiple times.\n\t\t// decoder.Read will be called 8 times, badReader.Read will be called twice, returning\n\t\t// valid data both times but an error on the second call.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjwc9g\"), errs: []error{nil, badErr}},\n\t\t\tres: \"leasure.10\", err: badErr, dbuflen: 1,\n\t\t},\n\t\t// Check io.EOF is properly reported when decoder.Read is called multiple times.\n\t\t// decoder.Read will be called 8 times, badReader.Read will be called twice, returning\n\t\t// valid data both times but io.EOF on the second call.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{nil, io.EOF}},\n\t\t\tres: \"leasure.\", err: io.EOF, dbuflen: 1,\n\t\t},\n\t\t// The following two test cases check that errors are propagated correctly when more than\n\t\t// 8 bytes are read at a time.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{io.EOF}},\n\t\t\tres: \"leasure.\", err: io.EOF, dbuflen: 11,\n\t\t},\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjwc9g\"), errs: []error{badErr}},\n\t\t\tres: \"leasure.10\", err: badErr, dbuflen: 11,\n\t\t},\n\t\t// Check that errors are correctly propagated when the reader returns valid bytes in\n\t\t// groups that are not divisible by 8. The first read will return 11 bytes and no\n\t\t// error. The second will return 7 and an error. The data should be decoded correctly\n\t\t// and the error should be propagated.\n\t\t// NOTE(thehowl): again, this is on the assumption that this is padded, and it's not.\n\t\t/* {\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{nil, badErr}, limit: 11},\n\t\t\tres: \"leasure.\", err: badErr,\n\t\t}, */\n\t}\n\n\tfor idx, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%d-%s\", idx, string(tc.res)), func(t *testing.T) {\n\t\t\tinput := tc.r.data\n\t\t\tdecoder := NewDecoder(\u0026tc.r)\n\t\t\tvar dbuflen int\n\t\t\tif tc.dbuflen \u003e 0 {\n\t\t\t\tdbuflen = tc.dbuflen\n\t\t\t} else {\n\t\t\t\tdbuflen = DecodedLen(len(input))\n\t\t\t}\n\t\t\tdbuf := make([]byte, dbuflen)\n\t\t\tvar err error\n\t\t\tvar res []byte\n\t\t\tfor err == nil {\n\t\t\t\tvar n int\n\t\t\t\tn, err = decoder.Read(dbuf)\n\t\t\t\tif n \u003e 0 {\n\t\t\t\t\tres = append(res, dbuf[:n]...)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttestEqual(t, \"Decoding of %q = %q, want %q\", string(input), string(res), tc.res)\n\t\t\ttestEqual(t, \"Decoding of %q err = %v, expected %v\", string(input), err, tc.err)\n\t\t})\n\t}\n}\n\n// TestDecoderError verifies decode errors are propagated when there are no read\n// errors.\nfunc TestDecoderError(t *testing.T) {\n\tfor _, readErr := range []error{io.EOF, nil} {\n\t\tinput := \"ucsqpyrk1u\"\n\t\tdbuf := make([]byte, DecodedLen(len(input)))\n\t\tbr := badReader{data: []byte(input), errs: []error{readErr}}\n\t\tdecoder := NewDecoder(\u0026br)\n\t\tn, err := decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\tif _, ok := err.(CorruptInputError); !ok {\n\t\t\tt.Errorf(\"Corrupt input error expected. Found %T\", err)\n\t\t}\n\t}\n}\n\n// TestReaderEOF ensures decoder.Read behaves correctly when input data is\n// exhausted.\nfunc TestReaderEOF(t *testing.T) {\n\tfor _, readErr := range []error{io.EOF, nil} {\n\t\tinput := \"MZXW6YTB\"\n\t\tbr := badReader{data: []byte(input), errs: []error{nil, readErr}}\n\t\tdecoder := NewDecoder(\u0026br)\n\t\tdbuf := make([]byte, DecodedLen(len(input)))\n\t\tn, err := decoder.Read(dbuf)\n\t\ttestEqual(t, \"Decoding of %q err = %v, expected %v\", input, err, error(nil))\n\t\tn, err = decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\ttestEqual(t, \"Read after EOF, err = %v, expected %v\", err, io.EOF)\n\t\tn, err = decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\ttestEqual(t, \"Read after EOF, err = %v, expected %v\", err, io.EOF)\n\t}\n}\n\nfunc TestDecoderBuffering(t *testing.T) {\n\tfor bs := 1; bs \u003c= 12; bs++ {\n\t\tdecoder := NewDecoder(strings.NewReader(bigtest.encoded))\n\t\tbuf := make([]byte, len(bigtest.decoded)+12)\n\t\tvar total int\n\t\tvar n int\n\t\tvar err error\n\t\tfor total = 0; total \u003c len(bigtest.decoded) \u0026\u0026 err == nil; {\n\t\t\tn, err = decoder.Read(buf[total : total+bs])\n\t\t\ttotal += n\n\t\t}\n\t\tif err != nil \u0026\u0026 err != io.EOF {\n\t\t\tt.Errorf(\"Read from %q at pos %d = %d, unexpected error %v\", bigtest.encoded, total, n, err)\n\t\t}\n\t\ttestEqual(t, \"Decoding/%d of %q = %q, want %q\", bs, bigtest.encoded, string(buf[0:total]), bigtest.decoded)\n\t}\n}\n\nfunc TestDecodeCorrupt(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput string\n\t\toffset int // -1 means no corruption.\n\t}{\n\t\t{\"\", -1},\n\t\t{\"iIoOlL\", -1},\n\t\t{\"!!!!\", 0},\n\t\t{\"uxp10\", 0},\n\t\t{\"x===\", 1},\n\t\t{\"AA=A====\", 2},\n\t\t{\"AAA=AAAA\", 3},\n\t\t// Much fewer cases compared to Go as there are much fewer cases where input\n\t\t// can be \"corrupted\".\n\t}\n\tfor _, tc := range testCases {\n\t\tdbuf := make([]byte, DecodedLen(len(tc.input)))\n\t\t_, err := Decode(dbuf, []byte(tc.input))\n\t\tif tc.offset == -1 {\n\t\t\tif err != nil {\n\t\t\t\tt.Error(\"Decoder wrongly detected corruption in\", tc.input)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch err := err.(type) {\n\t\tcase CorruptInputError:\n\t\t\ttestEqual(t, \"Corruption in %q at offset %v, want %v\", tc.input, int(err), tc.offset)\n\t\tdefault:\n\t\t\tt.Error(\"Decoder failed to detect corruption in\", tc)\n\t\t}\n\t}\n}\n\nfunc TestBig(t *testing.T) {\n\tn := 3*1000 + 1\n\traw := make([]byte, n)\n\tconst alpha = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\tfor i := 0; i \u003c n; i++ {\n\t\traw[i] = alpha[i%len(alpha)]\n\t}\n\tencoded := new(bytes.Buffer)\n\tw := NewEncoder(encoded)\n\tnn, err := w.Write(raw)\n\tif nn != n || err != nil {\n\t\tt.Fatalf(\"Encoder.Write(raw) = %d, %v want %d, nil\", nn, err, n)\n\t}\n\terr = w.Close()\n\tif err != nil {\n\t\tt.Fatalf(\"Encoder.Close() = %v want nil\", err)\n\t}\n\tdecoded, err := io.ReadAll(NewDecoder(encoded))\n\tif err != nil {\n\t\tt.Fatalf(\"io.ReadAll(NewDecoder(...)): %v\", err)\n\t}\n\n\tif !bytes.Equal(raw, decoded) {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(decoded) \u0026\u0026 i \u003c len(raw); i++ {\n\t\t\tif decoded[i] != raw[i] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tt.Errorf(\"Decode(Encode(%d-byte string)) failed at offset %d\", n, i)\n\t}\n}\n\nfunc testStringEncoding(t *testing.T, expected string, examples []string) {\n\tfor _, e := range examples {\n\t\tbuf, err := DecodeString(e)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Decode(%q) failed: %v\", e, err)\n\t\t\tcontinue\n\t\t}\n\t\tif s := string(buf); s != expected {\n\t\t\tt.Errorf(\"Decode(%q) = %q, want %q\", e, s, expected)\n\t\t}\n\t}\n}\n\nfunc TestNewLineCharacters(t *testing.T) {\n\t// Each of these should decode to the string \"sure\", without errors.\n\texamples := []string{\n\t\t\"EDTQ4S8\",\n\t\t\"EDTQ4S8\\r\",\n\t\t\"EDTQ4S8\\n\",\n\t\t\"EDTQ4S8\\r\\n\",\n\t\t\"EDTQ4S\\r\\n8\",\n\t\t\"EDT\\rQ4S\\n8\",\n\t\t\"edt\\nq4s\\r8\",\n\t\t\"edt\\nq4s8\",\n\t\t\"EDTQ4S\\n8\",\n\t}\n\ttestStringEncoding(t, \"sure\", examples)\n}\n\nfunc BenchmarkEncode(b *testing.B) {\n\tdata := make([]byte, 8192)\n\tbuf := make([]byte, EncodedLen(len(data)))\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tEncode(buf, data)\n\t}\n}\n\nfunc BenchmarkEncodeToString(b *testing.B) {\n\tdata := make([]byte, 8192)\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tEncodeToString(data)\n\t}\n}\n\nfunc BenchmarkDecode(b *testing.B) {\n\tdata := make([]byte, EncodedLen(8192))\n\tEncode(data, make([]byte, 8192))\n\tbuf := make([]byte, 8192)\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tDecode(buf, data)\n\t}\n}\n\nfunc BenchmarkDecodeString(b *testing.B) {\n\tdata := EncodeToString(make([]byte, 8192))\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tDecodeString(data)\n\t}\n}\n\n/* TODO: rewrite without using goroutines\nfunc TestBufferedDecodingSameError(t *testing.T) {\n\ttestcases := []struct {\n\t\tprefix string\n\t\tchunkCombinations [][]string\n\t\texpected error\n\t}{\n\t\t// Normal case, this is valid input\n\t\t{\"helloworld\", [][]string{\n\t\t\t{\"D1JP\", \"RV3F\", \"EXQQ\", \"4V34\"},\n\t\t\t{\"D1JPRV3FEXQQ4V34\"},\n\t\t\t{\"D1J\", \"PRV\", \"3FE\", \"XQQ\", \"4V3\", \"4\"},\n\t\t\t{\"D1JPRV3FEXQQ4V\", \"34\"},\n\t\t}, nil},\n\n\t\t// Normal case, this is valid input\n\t\t{\"fooba\", [][]string{\n\t\t\t{\"CSQPYRK1\"},\n\t\t\t{\"CSQPYRK\", \"1\"},\n\t\t\t{\"CSQPYR\", \"K1\"},\n\t\t\t{\"CSQPY\", \"RK1\"},\n\t\t\t{\"CSQPY\", \"RK\", \"1\"},\n\t\t\t{\"CSQPY\", \"RK1\"},\n\t\t\t{\"CSQP\", \"YR\", \"K1\"},\n\t\t}, nil},\n\n\t\t// NOTE: many test cases have been removed as we don't return ErrUnexpectedEOF.\n\t}\n\n\tfor _, testcase := range testcases {\n\t\tfor _, chunks := range testcase.chunkCombinations {\n\t\t\tpr, pw := io.Pipe()\n\n\t\t\t// Write the encoded chunks into the pipe\n\t\t\tgo func() {\n\t\t\t\tfor _, chunk := range chunks {\n\t\t\t\t\tpw.Write([]byte(chunk))\n\t\t\t\t}\n\t\t\t\tpw.Close()\n\t\t\t}()\n\n\t\t\tdecoder := NewDecoder(pr)\n\t\t\tback, err := io.ReadAll(decoder)\n\n\t\t\tif err != testcase.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v; case %s %+v\", testcase.expected, err, testcase.prefix, chunks)\n\t\t\t}\n\t\t\tif testcase.expected == nil {\n\t\t\t\ttestEqual(t, \"Decode from NewDecoder(chunkReader(%v)) = %q, want %q\", chunks, string(back), testcase.prefix)\n\t\t\t}\n\t\t}\n\t}\n}\n*/\n\nfunc TestEncodedLen(t *testing.T) {\n\ttype test struct {\n\t\tn int\n\t\twant int64\n\t}\n\ttests := []test{\n\t\t{0, 0},\n\t\t{1, 2},\n\t\t{2, 4},\n\t\t{3, 5},\n\t\t{4, 7},\n\t\t{5, 8},\n\t\t{6, 10},\n\t\t{7, 12},\n\t\t{10, 16},\n\t\t{11, 18},\n\t}\n\t// check overflow\n\ttests = append(tests, test{(math.MaxInt-4)/8 + 1, 1844674407370955162})\n\ttests = append(tests, test{math.MaxInt/8*5 + 4, math.MaxInt})\n\tfor _, tt := range tests {\n\t\tif got := EncodedLen(tt.n); int64(got) != tt.want {\n\t\t\tt.Errorf(\"EncodedLen(%d): got %d, want %d\", tt.n, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestDecodedLen(t *testing.T) {\n\ttype test struct {\n\t\tn int\n\t\twant int64\n\t}\n\ttests := []test{\n\t\t{0, 0},\n\t\t{2, 1},\n\t\t{4, 2},\n\t\t{5, 3},\n\t\t{7, 4},\n\t\t{8, 5},\n\t\t{10, 6},\n\t\t{12, 7},\n\t\t{16, 10},\n\t\t{18, 11},\n\t}\n\t// check overflow\n\ttests = append(tests, test{math.MaxInt/5 + 1, 1152921504606846976})\n\ttests = append(tests, test{math.MaxInt, 5764607523034234879})\n\tfor _, tt := range tests {\n\t\tif got := DecodedLen(tt.n); int64(got) != tt.want {\n\t\t\tt.Errorf(\"DecodedLen(%d): got %d, want %d\", tt.n, got, tt.want)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "combinederr", + "path": "gno.land/p/demo/combinederr", + "files": [ + { + "name": "combinederr.gno", + "body": "package combinederr\n\nimport \"strings\"\n\n// CombinedError is a combined execution error\ntype CombinedError struct {\n\terrors []error\n}\n\n// Error returns the combined execution error\nfunc (e *CombinedError) Error() string {\n\tif len(e.errors) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tfor _, err := range e.errors {\n\t\tsb.WriteString(err.Error() + \"; \")\n\t}\n\n\t// Remove the last semicolon and space\n\tresult := sb.String()\n\n\treturn result[:len(result)-2]\n}\n\n// Add adds a new error to the execution error\nfunc (e *CombinedError) Add(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\te.errors = append(e.errors, err)\n}\n\n// Size returns a\nfunc (e *CombinedError) Size() int {\n\treturn len(e.errors)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "context", + "path": "gno.land/p/demo/context", + "files": [ + { + "name": "context.gno", + "body": "// Package context provides a minimal implementation of Go context with support\n// for Value and WithValue.\n//\n// Adapted from https://github.com/golang/go/tree/master/src/context/.\n// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\npackage context\n\ntype Context interface {\n\t// Value returns the value associated with this context for key, or nil\n\t// if no value is associated with key.\n\tValue(key interface{}) interface{}\n}\n\n// Empty returns a non-nil, empty context, similar with context.Background and\n// context.TODO in Go.\nfunc Empty() Context {\n\treturn \u0026emptyCtx{}\n}\n\ntype emptyCtx struct{}\n\nfunc (ctx emptyCtx) Value(key interface{}) interface{} {\n\treturn nil\n}\n\nfunc (ctx emptyCtx) String() string {\n\treturn \"context.Empty\"\n}\n\ntype valueCtx struct {\n\tparent Context\n\tkey, val interface{}\n}\n\nfunc (ctx *valueCtx) Value(key interface{}) interface{} {\n\tif ctx.key == key {\n\t\treturn ctx.val\n\t}\n\treturn ctx.parent.Value(key)\n}\n\nfunc stringify(v interface{}) string {\n\tswitch s := v.(type) {\n\tcase stringer:\n\t\treturn s.String()\n\tcase string:\n\t\treturn s\n\t}\n\treturn \"non-stringer\"\n}\n\ntype stringer interface {\n\tString() string\n}\n\nfunc (c *valueCtx) String() string {\n\treturn stringify(c.parent) + \".WithValue(\" +\n\t\tstringify(c.key) + \", \" +\n\t\tstringify(c.val) + \")\"\n}\n\n// WithValue returns a copy of parent in which the value associated with key is\n// val.\nfunc WithValue(parent Context, key, val interface{}) Context {\n\tif key == nil {\n\t\tpanic(\"nil key\")\n\t}\n\t// XXX: if !reflect.TypeOf(key).Comparable() { panic(\"key is not comparable\") }\n\treturn \u0026valueCtx{parent, key, val}\n}\n" + }, + { + "name": "context_test.gno", + "body": "package context\n\nimport \"testing\"\n\nfunc TestContextExample(t *testing.T) {\n\ttype favContextKey string\n\n\tk := favContextKey(\"language\")\n\tctx := WithValue(Empty(), k, \"Gno\")\n\n\tif v := ctx.Value(k); v != nil {\n\t\tif string(v) != \"Gno\" {\n\t\t\tt.Errorf(\"language value should be Gno, but is %s\", v)\n\t\t}\n\t} else {\n\t\tt.Errorf(\"language key value was not found\")\n\t}\n\n\tif v := ctx.Value(favContextKey(\"color\")); v != nil {\n\t\tt.Errorf(\"color key was found\")\n\t}\n}\n\n// otherContext is a Context that's not one of the types defined in context.go.\n// This lets us test code paths that differ based on the underlying type of the\n// Context.\ntype otherContext struct {\n\tContext\n}\n\ntype (\n\tkey1 int\n\tkey2 int\n)\n\n// func (k key2) String() string { return fmt.Sprintf(\"%[1]T(%[1]d)\", k) }\n\nvar (\n\tk1 = key1(1)\n\tk2 = key2(1) // same int as k1, different type\n\tk3 = key2(3) // same type as k2, different int\n)\n\nfunc TestValues(t *testing.T) {\n\tcheck := func(c Context, nm, v1, v2, v3 string) {\n\t\tif v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {\n\t\t\tt.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {\n\t\t\tt.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {\n\t\t\tt.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)\n\t\t}\n\t}\n\n\tc0 := Empty()\n\tcheck(c0, \"c0\", \"\", \"\", \"\")\n\n\tt.Skip() // XXX: depends on https://github.com/gnolang/gno/issues/2386\n\n\tc1 := WithValue(Empty(), k1, \"c1k1\")\n\tcheck(c1, \"c1\", \"c1k1\", \"\", \"\")\n\n\t/*if got, want := c1.String(), `context.Empty.WithValue(context_test.key1, c1k1)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc2 := WithValue(c1, k2, \"c2k2\")\n\tcheck(c2, \"c2\", \"c1k1\", \"c2k2\", \"\")\n\n\t/*if got, want := fmt.Sprint(c2), `context.Empty.WithValue(context_test.key1, c1k1).WithValue(context_test.key2(1), c2k2)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc3 := WithValue(c2, k3, \"c3k3\")\n\tcheck(c3, \"c2\", \"c1k1\", \"c2k2\", \"c3k3\")\n\n\tc4 := WithValue(c3, k1, nil)\n\tcheck(c4, \"c4\", \"\", \"c2k2\", \"c3k3\")\n\n\to0 := otherContext{Empty()}\n\tcheck(o0, \"o0\", \"\", \"\", \"\")\n\n\to1 := otherContext{WithValue(Empty(), k1, \"c1k1\")}\n\tcheck(o1, \"o1\", \"c1k1\", \"\", \"\")\n\n\to2 := WithValue(o1, k2, \"o2k2\")\n\tcheck(o2, \"o2\", \"c1k1\", \"o2k2\", \"\")\n\n\to3 := otherContext{c4}\n\tcheck(o3, \"o3\", \"\", \"c2k2\", \"c3k3\")\n\n\to4 := WithValue(o3, k3, nil)\n\tcheck(o4, \"o4\", \"\", \"c2k2\", \"\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "dao", + "path": "gno.land/p/demo/dao", + "files": [ + { + "name": "dao.gno", + "body": "package dao\n\nconst (\n\tProposalAddedEvent = \"ProposalAdded\" // emitted when a new proposal has been added\n\tProposalAcceptedEvent = \"ProposalAccepted\" // emitted when a proposal has been accepted\n\tProposalNotAcceptedEvent = \"ProposalNotAccepted\" // emitted when a proposal has not been accepted\n\tProposalExecutedEvent = \"ProposalExecuted\" // emitted when a proposal has been executed\n\n\tProposalEventIDKey = \"proposal-id\"\n\tProposalEventAuthorKey = \"proposal-author\"\n\tProposalEventExecutionKey = \"exec-status\"\n)\n\n// ProposalRequest is a single govdao proposal request\n// that contains the necessary information to\n// log and generate a valid proposal\ntype ProposalRequest struct {\n\tDescription string // the description associated with the proposal\n\tExecutor Executor // the proposal executor\n}\n\n// DAO defines the DAO abstraction\ntype DAO interface {\n\t// PropStore is the DAO proposal storage\n\tPropStore\n\n\t// Propose adds a new proposal to the executor-based GOVDAO.\n\t// Returns the generated proposal ID\n\tPropose(request ProposalRequest) (uint64, error)\n\n\t// ExecuteProposal executes the proposal with the given ID\n\tExecuteProposal(id uint64) error\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package dao houses common DAO building blocks (framework), which can be used or adopted by any\n// specific DAO implementation. By design, the DAO should house the proposals it receives, but not the actual\n// DAO members or proposal votes. These abstractions should be implemented by a separate entity, to keep the DAO\n// agnostic of implementation details such as these (member / vote management).\npackage dao\n" + }, + { + "name": "events.gno", + "body": "package dao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// EmitProposalAdded emits an event signaling that\n// a given proposal was added\nfunc EmitProposalAdded(id uint64, proposer std.Address) {\n\tstd.Emit(\n\t\tProposalAddedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tProposalEventAuthorKey, proposer.String(),\n\t)\n}\n\n// EmitProposalAccepted emits an event signaling that\n// a given proposal was accepted\nfunc EmitProposalAccepted(id uint64) {\n\tstd.Emit(\n\t\tProposalAcceptedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t)\n}\n\n// EmitProposalNotAccepted emits an event signaling that\n// a given proposal was not accepted\nfunc EmitProposalNotAccepted(id uint64) {\n\tstd.Emit(\n\t\tProposalNotAcceptedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t)\n}\n\n// EmitProposalExecuted emits an event signaling that\n// a given proposal was executed, with the given status\nfunc EmitProposalExecuted(id uint64, status ProposalStatus) {\n\tstd.Emit(\n\t\tProposalExecutedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tProposalEventExecutionKey, status.String(),\n\t)\n}\n\n// EmitVoteAdded emits an event signaling that\n// a vote was cast for a given proposal\nfunc EmitVoteAdded(id uint64, voter std.Address, option VoteOption) {\n\tstd.Emit(\n\t\tVoteAddedEvent,\n\t\tVoteAddedIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tVoteAddedAuthorKey, voter.String(),\n\t\tVoteAddedOptionKey, option.String(),\n\t)\n}\n" + }, + { + "name": "executor.gno", + "body": "package dao\n\n// Executor represents a minimal closure-oriented proposal design.\n// It is intended to be used by a govdao governance proposal (v1, v2, etc)\ntype Executor interface {\n\t// Execute executes the given proposal, and returns any error encountered\n\t// during the execution\n\tExecute() error\n}\n" + }, + { + "name": "proposals.gno", + "body": "package dao\n\nimport \"std\"\n\n// ProposalStatus is the currently active proposal status,\n// changed based on DAO functionality.\n// Status transitions:\n//\n// ACTIVE -\u003e ACCEPTED -\u003e EXECUTION(SUCCEEDED/FAILED)\n//\n// ACTIVE -\u003e NOT ACCEPTED\ntype ProposalStatus string\n\nvar (\n\tActive ProposalStatus = \"active\" // proposal is still active\n\tAccepted ProposalStatus = \"accepted\" // proposal gathered quorum\n\tNotAccepted ProposalStatus = \"not accepted\" // proposal failed to gather quorum\n\tExecutionSuccessful ProposalStatus = \"execution successful\" // proposal is executed successfully\n\tExecutionFailed ProposalStatus = \"execution failed\" // proposal is failed during execution\n)\n\nfunc (s ProposalStatus) String() string {\n\treturn string(s)\n}\n\n// PropStore defines the proposal storage abstraction\ntype PropStore interface {\n\t// Proposals returns the given paginated proposals\n\tProposals(offset, count uint64) []Proposal\n\n\t// ProposalByID returns the proposal associated with\n\t// the given ID, if any\n\tProposalByID(id uint64) (Proposal, error)\n\n\t// Size returns the number of proposals in\n\t// the proposal store\n\tSize() int\n}\n\n// Proposal is the single proposal abstraction\ntype Proposal interface {\n\t// Author returns the author of the proposal\n\tAuthor() std.Address\n\n\t// Description returns the description of the proposal\n\tDescription() string\n\n\t// Status returns the status of the proposal\n\tStatus() ProposalStatus\n\n\t// Executor returns the proposal executor\n\tExecutor() Executor\n\n\t// Stats returns the voting stats of the proposal\n\tStats() Stats\n\n\t// IsExpired returns a flag indicating if the proposal expired\n\tIsExpired() bool\n\n\t// Render renders the proposal in a readable format\n\tRender() string\n}\n" + }, + { + "name": "vote.gno", + "body": "package dao\n\n// NOTE:\n// This voting pods will be removed in a future version of the\n// p/demo/dao package. A DAO shouldn't have to comply with or define how the voting mechanism works internally;\n// it should be viewed as an entity that makes decisions\n//\n// The extent of \"votes being enforced\" in this implementation is just in the context\n// of types a DAO can use (import), and in the context of \"Stats\", where\n// there is a notion of \"Yay\", \"Nay\" and \"Abstain\" votes.\nconst (\n\tVoteAddedEvent = \"VoteAdded\" // emitted when a vote was cast for a proposal\n\n\tVoteAddedIDKey = \"proposal-id\"\n\tVoteAddedAuthorKey = \"author\"\n\tVoteAddedOptionKey = \"option\"\n)\n\n// VoteOption is the limited voting option for a DAO proposal\ntype VoteOption string\n\nconst (\n\tYesVote VoteOption = \"YES\" // Proposal should be accepted\n\tNoVote VoteOption = \"NO\" // Proposal should be rejected\n\tAbstainVote VoteOption = \"ABSTAIN\" // Side is not chosen\n)\n\nfunc (v VoteOption) String() string {\n\treturn string(v)\n}\n\n// Stats encompasses the proposal voting stats\ntype Stats struct {\n\tYayVotes uint64\n\tNayVotes uint64\n\tAbstainVotes uint64\n\n\tTotalVotingPower uint64\n}\n\n// YayPercent returns the percentage (0-100) of the yay votes\n// in relation to the total voting power\nfunc (v Stats) YayPercent() uint64 {\n\treturn v.YayVotes * 100 / v.TotalVotingPower\n}\n\n// NayPercent returns the percentage (0-100) of the nay votes\n// in relation to the total voting power\nfunc (v Stats) NayPercent() uint64 {\n\treturn v.NayVotes * 100 / v.TotalVotingPower\n}\n\n// AbstainPercent returns the percentage (0-100) of the abstain votes\n// in relation to the total voting power\nfunc (v Stats) AbstainPercent() uint64 {\n\treturn v.AbstainVotes * 100 / v.TotalVotingPower\n}\n\n// MissingVotes returns the summed voting power that has not\n// participated in proposal voting yet\nfunc (v Stats) MissingVotes() uint64 {\n\treturn v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes)\n}\n\n// MissingVotesPercent returns the percentage (0-100) of the missing votes\n// in relation to the total voting power\nfunc (v Stats) MissingVotesPercent() uint64 {\n\treturn v.MissingVotes() * 100 / v.TotalVotingPower\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "dom", + "path": "gno.land/p/demo/dom", + "files": [ + { + "name": "dom.gno", + "body": "// XXX This is only used for testing in ./tests.\n// Otherwise this package is deprecated.\n// TODO: replace with a package that is supported, and delete this.\n\npackage dom\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Plot struct {\n\tName string\n\tPosts avl.Tree // postsCtr -\u003e *Post\n\tPostsCtr int\n}\n\nfunc (plot *Plot) AddPost(title string, body string) {\n\tctr := plot.PostsCtr\n\tplot.PostsCtr++\n\tkey := strconv.Itoa(ctr)\n\tpost := \u0026Post{\n\t\tTitle: title,\n\t\tBody: body,\n\t}\n\tplot.Posts.Set(key, post)\n}\n\nfunc (plot *Plot) String() string {\n\tstr := \"# [plot] \" + plot.Name + \"\\n\"\n\tif plot.Posts.Size() \u003e 0 {\n\t\tplot.Posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Post).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Post struct {\n\tTitle string\n\tBody string\n\tComments avl.Tree\n}\n\nfunc (post *Post) String() string {\n\tstr := \"## \" + post.Title + \"\\n\"\n\tstr += \"\"\n\tstr += post.Body\n\tif post.Comments.Size() \u003e 0 {\n\t\tpost.Comments.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Comment).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Comment struct {\n\tCreator string\n\tBody string\n}\n\nfunc (cmm Comment) String() string {\n\treturn cmm.Body + \" - @\" + cmm.Creator + \"\\n\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "entropy", + "path": "gno.land/p/demo/entropy", + "files": [ + { + "name": "entropy.gno", + "body": "// Entropy generates fully deterministic, cost-effective, and hard to guess\n// numbers.\n//\n// It is designed both for single-usage, like seeding math/rand or for being\n// reused which increases the entropy and its cost effectiveness.\n//\n// Disclaimer: this package is unsafe and won't prevent others to guess values\n// in advance.\n//\n// It uses the Bernstein's hash djb2 to be CPU-cycle efficient.\npackage entropy\n\nimport (\n\t\"math\"\n\t\"std\"\n\t\"time\"\n)\n\ntype Instance struct {\n\tvalue uint32\n}\n\nfunc New() *Instance {\n\tr := Instance{value: 5381}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc FromSeed(seed uint32) *Instance {\n\tr := Instance{value: seed}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc (i *Instance) Seed() uint32 {\n\treturn i.value\n}\n\nfunc (i *Instance) djb2String(input string) {\n\tfor _, c := range input {\n\t\ti.djb2Uint32(uint32(c))\n\t}\n}\n\n// super fast random algorithm.\n// http://www.cse.yorku.ca/~oz/hash.html\nfunc (i *Instance) djb2Uint32(input uint32) {\n\ti.value = (i.value \u003c\u003c 5) + i.value + input\n}\n\n// AddEntropy uses various runtime variables to add entropy to the existing seed.\nfunc (i *Instance) addEntropy() {\n\t// FIXME: reapply the 5381 initial value?\n\n\t// inherit previous entropy\n\t// nothing to do\n\n\t// handle callers\n\t{\n\t\tcaller1 := std.GetCallerAt(1).String()\n\t\ti.djb2String(caller1)\n\t\tcaller2 := std.GetCallerAt(2).String()\n\t\ti.djb2String(caller2)\n\t}\n\n\t// height\n\t{\n\t\theight := std.GetHeight()\n\t\tif height \u003e= math.MaxUint32 {\n\t\t\theight -= math.MaxUint32\n\t\t}\n\t\ti.djb2Uint32(uint32(height))\n\t}\n\n\t// time\n\t{\n\t\tsecs := time.Now().Second()\n\t\ti.djb2Uint32(uint32(secs))\n\t\tnsecs := time.Now().Nanosecond()\n\t\ti.djb2Uint32(uint32(nsecs))\n\t}\n\n\t// FIXME: compute other hard-to-guess but deterministic variables, like real gas?\n}\n\nfunc (i *Instance) Value() uint32 {\n\ti.addEntropy()\n\treturn i.value\n}\n" + }, + { + "name": "entropy_test.gno", + "body": "package entropy\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestInstance(t *testing.T) {\n\tinstance := New()\n\tif instance == nil {\n\t\tt.Errorf(\"instance should not be nil\")\n\t}\n}\n\nfunc TestInstanceValue(t *testing.T) {\n\tbaseEntropy := New()\n\tbaseResult := computeValue(t, baseEntropy)\n\n\tsameHeightEntropy := New()\n\tsameHeightResult := computeValue(t, sameHeightEntropy)\n\n\tif baseResult != sameHeightResult {\n\t\tt.Errorf(\"should have the same result: new=%s, base=%s\", sameHeightResult, baseResult)\n\t}\n\n\tstd.TestSkipHeights(1)\n\tdifferentHeightEntropy := New()\n\tdifferentHeightResult := computeValue(t, differentHeightEntropy)\n\n\tif baseResult == differentHeightResult {\n\t\tt.Errorf(\"should have different result: new=%s, base=%s\", differentHeightResult, baseResult)\n\t}\n}\n\nfunc computeValue(t *testing.T, r *Instance) string {\n\tt.Helper()\n\n\tout := \"\"\n\tfor i := 0; i \u003c 10; i++ {\n\t\tval := int(r.Value())\n\t\tout += strconv.Itoa(val) + \" \"\n\t}\n\n\treturn out\n}\n" + }, + { + "name": "z_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/entropy\"\n)\n\nfunc main() {\n\t// initial\n\tprintln(\"---\")\n\tr := entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\n\t// should be the same\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\n\tstd.TestSkipHeights(1)\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n}\n\n// Output:\n// ---\n// 4129293727\n// 2141104956\n// 1950222777\n// 3348280598\n// 438354259\n// ---\n// 4129293727\n// 2141104956\n// 1950222777\n// 3348280598\n// 438354259\n// ---\n// 49506731\n// 1539580078\n// 2695928529\n// 1895482388\n// 3462727799\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "flow", + "path": "gno.land/p/demo/flow", + "files": [ + { + "name": "LICENSE", + "body": "https://github.com/mxk/go-flowrate/blob/master/LICENSE\nBSD 3-Clause \"New\" or \"Revised\" License\n\nCopyright (c) 2014 The Go-FlowRate Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the\n distribution.\n\n * Neither the name of the go-flowrate project nor the names of its\n contributors may be used to endorse or promote products derived\n from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "README.md", + "body": "Data Flow Rate Control\n======================\n\nTo download and install this package run:\n\ngo get github.com/mxk/go-flowrate/flowrate\n\nThe documentation is available at:\n\nhttp://godoc.org/github.com/mxk/go-flowrate/flowrate\n" + }, + { + "name": "flow.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n// XXX modified to disable blocking, time.Sleep().\n\n// Package flow provides the tools for monitoring and limiting the flow rate\n// of an arbitrary data stream.\npackage flow\n\nimport (\n\t\"math\"\n\t// \"sync\"\n\t\"time\"\n)\n\n// Monitor monitors and limits the transfer rate of a data stream.\ntype Monitor struct {\n\t// mu sync.Mutex // Mutex guarding access to all internal fields\n\tactive bool // Flag indicating an active transfer\n\tstart time.Duration // Transfer start time (clock() value)\n\tbytes int64 // Total number of bytes transferred\n\tsamples int64 // Total number of samples taken\n\n\trSample float64 // Most recent transfer rate sample (bytes per second)\n\trEMA float64 // Exponential moving average of rSample\n\trPeak float64 // Peak transfer rate (max of all rSamples)\n\trWindow float64 // rEMA window (seconds)\n\n\tsBytes int64 // Number of bytes transferred since sLast\n\tsLast time.Duration // Most recent sample time (stop time when inactive)\n\tsRate time.Duration // Sampling rate\n\n\ttBytes int64 // Number of bytes expected in the current transfer\n\ttLast time.Duration // Time of the most recent transfer of at least 1 byte\n}\n\n// New creates a new flow control monitor. Instantaneous transfer rate is\n// measured and updated for each sampleRate interval. windowSize determines the\n// weight of each sample in the exponential moving average (EMA) calculation.\n// The exact formulas are:\n//\n//\tsampleTime = currentTime - prevSampleTime\n//\tsampleRate = byteCount / sampleTime\n//\tweight = 1 - exp(-sampleTime/windowSize)\n//\tnewRate = weight*sampleRate + (1-weight)*oldRate\n//\n// The default values for sampleRate and windowSize (if \u003c= 0) are 100ms and 1s,\n// respectively.\nfunc New(sampleRate, windowSize time.Duration) *Monitor {\n\tif sampleRate = clockRound(sampleRate); sampleRate \u003c= 0 {\n\t\tsampleRate = 5 * clockRate\n\t}\n\tif windowSize \u003c= 0 {\n\t\twindowSize = 1 * time.Second\n\t}\n\tnow := clock()\n\treturn \u0026Monitor{\n\t\tactive: true,\n\t\tstart: now,\n\t\trWindow: windowSize.Seconds(),\n\t\tsLast: now,\n\t\tsRate: sampleRate,\n\t\ttLast: now,\n\t}\n}\n\n// Update records the transfer of n bytes and returns n. It should be called\n// after each Read/Write operation, even if n is 0.\nfunc (m *Monitor) Update(n int) int {\n\t// m.mu.Lock()\n\tm.update(n)\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// Hack to set the current rEMA.\nfunc (m *Monitor) SetREMA(rEMA float64) {\n\t// m.mu.Lock()\n\tm.rEMA = rEMA\n\tm.samples++\n\t// m.mu.Unlock()\n}\n\n// IO is a convenience method intended to wrap io.Reader and io.Writer method\n// execution. It calls m.Update(n) and then returns (n, err) unmodified.\nfunc (m *Monitor) IO(n int, err error) (int, error) {\n\treturn m.Update(n), err\n}\n\n// Done marks the transfer as finished and prevents any further updates or\n// limiting. Instantaneous and current transfer rates drop to 0. Update, IO, and\n// Limit methods become NOOPs. It returns the total number of bytes transferred.\nfunc (m *Monitor) Done() int64 {\n\t// m.mu.Lock()\n\tif now := m.update(0); m.sBytes \u003e 0 {\n\t\tm.reset(now)\n\t}\n\tm.active = false\n\tm.tLast = 0\n\tn := m.bytes\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// timeRemLimit is the maximum Status.TimeRem value.\nconst timeRemLimit = 999*time.Hour + 59*time.Minute + 59*time.Second\n\n// Status represents the current Monitor status. All transfer rates are in bytes\n// per second rounded to the nearest byte.\ntype Status struct {\n\tActive bool // Flag indicating an active transfer\n\tStart time.Time // Transfer start time\n\tDuration time.Duration // Time period covered by the statistics\n\tIdle time.Duration // Time since the last transfer of at least 1 byte\n\tBytes int64 // Total number of bytes transferred\n\tSamples int64 // Total number of samples taken\n\tInstRate int64 // Instantaneous transfer rate\n\tCurRate int64 // Current transfer rate (EMA of InstRate)\n\tAvgRate int64 // Average transfer rate (Bytes / Duration)\n\tPeakRate int64 // Maximum instantaneous transfer rate\n\tBytesRem int64 // Number of bytes remaining in the transfer\n\tTimeRem time.Duration // Estimated time to completion\n\tProgress Percent // Overall transfer progress\n}\n\nfunc (s Status) String() string {\n\treturn \"STATUS{}\"\n}\n\n// Status returns current transfer status information. The returned value\n// becomes static after a call to Done.\nfunc (m *Monitor) Status() Status {\n\t// m.mu.Lock()\n\tnow := m.update(0)\n\ts := Status{\n\t\tActive: m.active,\n\t\tStart: clockToTime(m.start),\n\t\tDuration: m.sLast - m.start,\n\t\tIdle: now - m.tLast,\n\t\tBytes: m.bytes,\n\t\tSamples: m.samples,\n\t\tPeakRate: round(m.rPeak),\n\t\tBytesRem: m.tBytes - m.bytes,\n\t\tProgress: percentOf(float64(m.bytes), float64(m.tBytes)),\n\t}\n\tif s.BytesRem \u003c 0 {\n\t\ts.BytesRem = 0\n\t}\n\tif s.Duration \u003e 0 {\n\t\trAvg := float64(s.Bytes) / s.Duration.Seconds()\n\t\ts.AvgRate = round(rAvg)\n\t\tif s.Active {\n\t\t\ts.InstRate = round(m.rSample)\n\t\t\ts.CurRate = round(m.rEMA)\n\t\t\tif s.BytesRem \u003e 0 {\n\t\t\t\tif tRate := 0.8*m.rEMA + 0.2*rAvg; tRate \u003e 0 {\n\t\t\t\t\tns := float64(s.BytesRem) / tRate * 1e9\n\t\t\t\t\tif ns \u003e float64(timeRemLimit) {\n\t\t\t\t\t\tns = float64(timeRemLimit)\n\t\t\t\t\t}\n\t\t\t\t\ts.TimeRem = clockRound(time.Duration(ns))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t// m.mu.Unlock()\n\treturn s\n}\n\n// Limit restricts the instantaneous (per-sample) data flow to rate bytes per\n// second. It returns the maximum number of bytes (0 \u003c= n \u003c= want) that may be\n// transferred immediately without exceeding the limit. If block == true, the\n// call blocks until n \u003e 0. want is returned unmodified if want \u003c 1, rate \u003c 1,\n// or the transfer is inactive (after a call to Done).\n//\n// At least one byte is always allowed to be transferred in any given sampling\n// period. Thus, if the sampling rate is 100ms, the lowest achievable flow rate\n// is 10 bytes per second.\n//\n// For usage examples, see the implementation of Reader and Writer in io.go.\nfunc (m *Monitor) Limit(want int, rate int64, block bool) (n int) {\n\tif block {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\tif want \u003c 1 || rate \u003c 1 {\n\t\treturn want\n\t}\n\t// m.mu.Lock()\n\n\t// Determine the maximum number of bytes that can be sent in one sample\n\tlimit := round(float64(rate) * m.sRate.Seconds())\n\tif limit \u003c= 0 {\n\t\tlimit = 1\n\t}\n\n\t_ = m.update(0)\n\t/* XXX\n\t// If block == true, wait until m.sBytes \u003c limit\n\tif now := m.update(0); block {\n\t\tfor m.sBytes \u003e= limit \u0026\u0026 m.active {\n\t\t\tnow = m.waitNextSample(now)\n\t\t}\n\t}\n\t*/\n\n\t// Make limit \u003c= want (unlimited if the transfer is no longer active)\n\tif limit -= m.sBytes; limit \u003e int64(want) || !m.active {\n\t\tlimit = int64(want)\n\t}\n\t// m.mu.Unlock()\n\n\tif limit \u003c 0 {\n\t\tlimit = 0\n\t}\n\treturn int(limit)\n}\n\n// SetTransferSize specifies the total size of the data transfer, which allows\n// the Monitor to calculate the overall progress and time to completion.\nfunc (m *Monitor) SetTransferSize(bytes int64) {\n\tif bytes \u003c 0 {\n\t\tbytes = 0\n\t}\n\t// m.mu.Lock()\n\tm.tBytes = bytes\n\t// m.mu.Unlock()\n}\n\n// update accumulates the transferred byte count for the current sample until\n// clock() - m.sLast \u003e= m.sRate. The monitor status is updated once the current\n// sample is done.\nfunc (m *Monitor) update(n int) (now time.Duration) {\n\tif !m.active {\n\t\treturn\n\t}\n\tif now = clock(); n \u003e 0 {\n\t\tm.tLast = now\n\t}\n\tm.sBytes += int64(n)\n\tif sTime := now - m.sLast; sTime \u003e= m.sRate {\n\t\tt := sTime.Seconds()\n\t\tif m.rSample = float64(m.sBytes) / t; m.rSample \u003e m.rPeak {\n\t\t\tm.rPeak = m.rSample\n\t\t}\n\n\t\t// Exponential moving average using a method similar to *nix load\n\t\t// average calculation. Longer sampling periods carry greater weight.\n\t\tif m.samples \u003e 0 {\n\t\t\tw := math.Exp(-t / m.rWindow)\n\t\t\tm.rEMA = m.rSample + w*(m.rEMA-m.rSample)\n\t\t} else {\n\t\t\tm.rEMA = m.rSample\n\t\t}\n\t\tm.reset(now)\n\t}\n\treturn\n}\n\n// reset clears the current sample state in preparation for the next sample.\nfunc (m *Monitor) reset(sampleTime time.Duration) {\n\tm.bytes += m.sBytes\n\tm.samples++\n\tm.sBytes = 0\n\tm.sLast = sampleTime\n}\n\n/*\n// waitNextSample sleeps for the remainder of the current sample. The lock is\n// released and reacquired during the actual sleep period, so it's possible for\n// the transfer to be inactive when this method returns.\nfunc (m *Monitor) waitNextSample(now time.Duration) time.Duration {\n\tconst minWait = 5 * time.Millisecond\n\tcurrent := m.sLast\n\n\t// sleep until the last sample time changes (ideally, just one iteration)\n\tfor m.sLast == current \u0026\u0026 m.active {\n\t\td := current + m.sRate - now\n\t\t// m.mu.Unlock()\n\t\tif d \u003c minWait {\n\t\t\td = minWait\n\t\t}\n\t\ttime.Sleep(d)\n\t\t// m.mu.Lock()\n\t\tnow = m.update(0)\n\t}\n\treturn now\n}\n*/\n" + }, + { + "name": "io.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// ErrLimit is returned by the Writer when a non-blocking write is short due to\n// the transfer rate limit.\nvar ErrLimit = errors.New(\"flowrate: flow rate limit exceeded\")\n\n// Limiter is implemented by the Reader and Writer to provide a consistent\n// interface for monitoring and controlling data transfer.\ntype Limiter interface {\n\tDone() int64\n\tStatus() Status\n\tSetTransferSize(bytes int64)\n\tSetLimit(new int64) (old int64)\n\tSetBlocking(new bool) (old bool)\n}\n\n// Reader implements io.ReadCloser with a restriction on the rate of data\n// transfer.\ntype Reader struct {\n\tio.Reader // Data source\n\t*Monitor // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool // What to do when no new bytes can be read due to the limit\n}\n\n// NewReader restricts all Read operations on r to limit bytes per second.\nfunc NewReader(r io.Reader, limit int64) *Reader {\n\treturn \u0026Reader{r, New(0, 0), limit, false} // XXX default false\n}\n\n// Read reads up to len(p) bytes into p without exceeding the current transfer\n// rate limit. It returns (0, nil) immediately if r is non-blocking and no new\n// bytes can be read at this time.\nfunc (r *Reader) Read(p []byte) (n int, err error) {\n\tp = p[:r.Limit(len(p), r.limit, r.block)]\n\tif len(p) \u003e 0 {\n\t\tn, err = r.IO(r.Reader.Read(p))\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (r *Reader) SetLimit(new int64) (old int64) {\n\told, r.limit = r.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Read call on a non-blocking reader returns immediately if no additional bytes\n// may be read at this time due to the rate limit.\nfunc (r *Reader) SetBlocking(new bool) (old bool) {\n\tif new == true {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\told, r.block = r.block, new\n\treturn\n}\n\n// Close closes the underlying reader if it implements the io.Closer interface.\nfunc (r *Reader) Close() error {\n\tdefer r.Done()\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\n// Writer implements io.WriteCloser with a restriction on the rate of data\n// transfer.\ntype Writer struct {\n\tio.Writer // Data destination\n\t*Monitor // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool // What to do when no new bytes can be written due to the limit\n}\n\n// NewWriter restricts all Write operations on w to limit bytes per second. The\n// transfer rate and the default blocking behavior (true) can be changed\n// directly on the returned *Writer.\nfunc NewWriter(w io.Writer, limit int64) *Writer {\n\treturn \u0026Writer{w, New(0, 0), limit, false} // XXX default false\n}\n\n// Write writes len(p) bytes from p to the underlying data stream without\n// exceeding the current transfer rate limit. It returns (n, ErrLimit) if w is\n// non-blocking and no additional bytes can be written at this time.\nfunc (w *Writer) Write(p []byte) (n int, err error) {\n\tvar c int\n\tfor len(p) \u003e 0 \u0026\u0026 err == nil {\n\t\ts := p[:w.Limit(len(p), w.limit, w.block)]\n\t\tif len(s) \u003e 0 {\n\t\t\tc, err = w.IO(w.Writer.Write(s))\n\t\t} else {\n\t\t\treturn n, ErrLimit\n\t\t}\n\t\tp = p[c:]\n\t\tn += c\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (w *Writer) SetLimit(new int64) (old int64) {\n\told, w.limit = w.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Write call on a non-blocking writer returns as soon as no additional bytes\n// may be written at this time due to the rate limit.\nfunc (w *Writer) SetBlocking(new bool) (old bool) {\n\told, w.block = w.block, new\n\treturn\n}\n\n// Close closes the underlying writer if it implements the io.Closer interface.\nfunc (w *Writer) Close() error {\n\tdefer w.Done()\n\tif c, ok := w.Writer.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n" + }, + { + "name": "io_test.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\tios_test \"internal/os_test\"\n)\n\n// XXX ugh, I can't even sleep milliseconds.\n// XXX\n\nconst (\n\t_50ms = 50 * time.Millisecond\n\t_100ms = 100 * time.Millisecond\n\t_200ms = 200 * time.Millisecond\n\t_300ms = 300 * time.Millisecond\n\t_400ms = 400 * time.Millisecond\n\t_500ms = 500 * time.Millisecond\n)\n\nfunc nextStatus(m *Monitor) Status {\n\tsamples := m.samples\n\tfor i := 0; i \u003c 30; i++ {\n\t\tif s := m.Status(); s.Samples != samples {\n\t\t\treturn s\n\t\t}\n\t\tios_test.Sleep(5 * time.Millisecond)\n\t}\n\treturn m.Status()\n}\n\nfunc TestReader(t *testing.T) {\n\tin := make([]byte, 100)\n\tfor i := range in {\n\t\tin[i] = byte(i)\n\t}\n\tb := make([]byte, 100)\n\tr := NewReader(bytes.NewReader(in), 100)\n\tstart := time.Now()\n\n\t// Make sure r implements Limiter\n\t_ = Limiter(r)\n\n\t// 1st read of 10 bytes is performed immediately\n\tif n, err := r.Read(b); n != 10 {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\t// No new Reads allowed in the current sample\n\tr.SetBlocking(false)\n\tif n, err := r.Read(b); n != 0 {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\tstatus := [6]Status{0: r.Status()} // No samples in the first status\n\n\t// 2nd read of 10 bytes blocks until the next sample\n\t// r.SetBlocking(true)\n\tios_test.Sleep(100 * time.Millisecond)\n\tif n, err := r.Read(b[10:]); n != 10 {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _100ms {\n\t\tt.Fatalf(\"r.Read(b[10:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tstatus[1] = r.Status() // 1st sample\n\tstatus[2] = nextStatus(r.Monitor) // 2nd sample\n\tstatus[3] = nextStatus(r.Monitor) // No activity for the 3rd sample\n\n\tif n := r.Done(); n != 20 {\n\t\tt.Fatalf(\"r.Done() expected 20; got %v\", n)\n\t}\n\n\tstatus[4] = r.Status()\n\tstatus[5] = nextStatus(r.Monitor) // Timeout\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n\t\t{true, start, _100ms, 0, 10, 1, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _200ms, _100ms, 20, 2, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _300ms, _200ms, 20, 3, 0, 90, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"r.Status(%v)\\nexpected: %v\\ngot : %v\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b[:20], in[:20]) {\n\t\tt.Errorf(\"r.Read() input doesn't match output\")\n\t}\n}\n\n// XXX blocking writer test doesn't work.\nfunc _TestWriter(t *testing.T) {\n\tb := make([]byte, 100)\n\tfor i := range b {\n\t\tb[i] = byte(i)\n\t}\n\tw := NewWriter(\u0026bytes.Buffer{}, 200)\n\tstart := time.Now()\n\n\t// Make sure w implements Limiter\n\t_ = Limiter(w)\n\n\t// Non-blocking 20-byte write for the first sample returns ErrLimit\n\tw.SetBlocking(false)\n\tif n, err := w.Write(b); n != 20 || err != ErrLimit {\n\t\tt.Fatalf(\"w.Write(b) expected 20 (ErrLimit); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"w.Write(b) took too long (%v)\", rt)\n\t}\n\n\t// Blocking 80-byte write\n\t// w.SetBlocking(true)\n\t// XXX This test doesn't work, because w.Write calls w.Limit(block=false),\n\t// XXX and it returns ErrLimit after 20. What we want is to keep waiting until 80 is returned,\n\t// XXX but blocking isn't supported. Sleeping 800 shouldn't be sufficient either (its a burst).\n\t// XXX This limits the usage of Limiter and m.Limit().\n\tios_test.Sleep(800 * time.Millisecond)\n\tif n, err := w.Write(b[20:]); n \u003c 80 {\n\t} else if n != 80 || err != nil {\n\t\tt.Fatalf(\"w.Write(b[20:]) expected 80 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _300ms {\n\t\t// Explanation for `rt \u003c _300ms` (as opposed to `\u003c _400ms`)\n\t\t//\n\t\t// |\u003c-- start | |\n\t\t// epochs: -----0ms|---100ms|---200ms|---300ms|---400ms\n\t\t// sends: 20|20 |20 |20 |20#\n\t\t//\n\t\t// NOTE: The '#' symbol can thus happen before 400ms is up.\n\t\t// Thus, we can only panic if rt \u003c _300ms.\n\t\tt.Fatalf(\"w.Write(b[20:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tw.SetTransferSize(100)\n\tstatus := []Status{w.Status(), nextStatus(w.Monitor)}\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, _400ms, 0, 80, 4, 200, 200, 200, 200, 20, _100ms, 80000},\n\t\t{true, start, _500ms, _100ms, 100, 5, 200, 200, 200, 200, 0, 0, 100000},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"w.Status(%v)\\nexpected: %v\\ngot : %v\\n\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b, w.Writer.(*bytes.Buffer).Bytes()) {\n\t\tt.Errorf(\"w.Write() input doesn't match output\")\n\t}\n}\n\nconst (\n\tmaxDeviationForDuration = 50 * time.Millisecond\n\tmaxDeviationForRate int64 = 50\n)\n\n// statusesAreEqual returns true if s1 is equal to s2. Equality here means\n// general equality of fields except for the duration and rates, which can\n// drift due to unpredictable delays (e.g. thread wakes up 25ms after\n// `time.Sleep` has ended).\nfunc statusesAreEqual(s1 *Status, s2 *Status) bool {\n\tif s1.Active == s2.Active \u0026\u0026\n\t\ts1.Start == s2.Start \u0026\u0026\n\t\tdurationsAreEqual(s1.Duration, s2.Duration, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Idle == s2.Idle \u0026\u0026\n\t\ts1.Bytes == s2.Bytes \u0026\u0026\n\t\ts1.Samples == s2.Samples \u0026\u0026\n\t\tratesAreEqual(s1.InstRate, s2.InstRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.CurRate, s2.CurRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.AvgRate, s2.AvgRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.PeakRate, s2.PeakRate, maxDeviationForRate) \u0026\u0026\n\t\ts1.BytesRem == s2.BytesRem \u0026\u0026\n\t\tdurationsAreEqual(s1.TimeRem, s2.TimeRem, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Progress == s2.Progress {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc durationsAreEqual(d1 time.Duration, d2 time.Duration, maxDeviation time.Duration) bool {\n\treturn d2-d1 \u003c= maxDeviation\n}\n\nfunc ratesAreEqual(r1 int64, r2 int64, maxDeviation int64) bool {\n\tsub := r1 - r2\n\tif sub \u003c 0 {\n\t\tsub = -sub\n\t}\n\tif sub \u003c= maxDeviation {\n\t\treturn true\n\t}\n\treturn false\n}\n" + }, + { + "name": "util.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// clockRate is the resolution and precision of clock().\nconst clockRate = 20 * time.Millisecond\n\n// czero is the process start time rounded down to the nearest clockRate\n// increment.\nvar czero = time.Now().Round(clockRate)\n\n// clock returns a low resolution timestamp relative to the process start time.\nfunc clock() time.Duration {\n\treturn time.Now().Round(clockRate).Sub(czero)\n}\n\n// clockToTime converts a clock() timestamp to an absolute time.Time value.\nfunc clockToTime(c time.Duration) time.Time {\n\treturn czero.Add(c)\n}\n\n// clockRound returns d rounded to the nearest clockRate increment.\nfunc clockRound(d time.Duration) time.Duration {\n\treturn (d + clockRate\u003e\u003e1) / clockRate * clockRate\n}\n\n// round returns x rounded to the nearest int64 (non-negative values only).\nfunc round(x float64) int64 {\n\tif _, frac := math.Modf(x); frac \u003e= 0.5 {\n\t\treturn int64(math.Ceil(x))\n\t}\n\treturn int64(math.Floor(x))\n}\n\n// Percent represents a percentage in increments of 1/1000th of a percent.\ntype Percent uint32\n\n// percentOf calculates what percent of the total is x.\nfunc percentOf(x, total float64) Percent {\n\tif x \u003c 0 || total \u003c= 0 {\n\t\treturn 0\n\t} else if p := round(x / total * 1e5); p \u003c= math.MaxUint32 {\n\t\treturn Percent(p)\n\t}\n\treturn Percent(math.MaxUint32)\n}\n\nfunc (p Percent) Float() float64 {\n\treturn float64(p) * 1e-3\n}\n\nfunc (p Percent) String() string {\n\tvar buf [12]byte\n\tb := strconv.AppendUint(buf[:0], uint64(p)/1000, 10)\n\tn := len(b)\n\tb = strconv.AppendUint(b, 1000+uint64(p)%1000, 10)\n\tb[n] = '.'\n\treturn string(append(b, '%'))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "fqname", + "path": "gno.land/p/demo/fqname", + "files": [ + { + "name": "fqname.gno", + "body": "// Package fqname provides utilities for handling fully qualified identifiers in\n// Gno. A fully qualified identifier typically includes a package path followed\n// by a dot (.) and then the name of a variable, function, type, or other\n// package-level declaration.\npackage fqname\n\nimport \"strings\"\n\n// Parse splits a fully qualified identifier into its package path and name\n// components. It handles cases with and without slashes in the package path.\n//\n//\tpkgpath, name := fqname.Parse(\"gno.land/p/demo/avl.Tree\")\n//\tufmt.Sprintf(\"Package: %s, Name: %s\\n\", id.Package, id.Name)\n//\t// Output: Package: gno.land/p/demo/avl, Name: Tree\nfunc Parse(fqname string) (pkgpath, name string) {\n\t// Find the index of the last slash.\n\tlastSlashIndex := strings.LastIndex(fqname, \"/\")\n\tif lastSlashIndex == -1 {\n\t\t// No slash found, handle it as a simple package name with dot notation.\n\t\tdotIndex := strings.LastIndex(fqname, \".\")\n\t\tif dotIndex == -1 {\n\t\t\treturn fqname, \"\"\n\t\t}\n\t\treturn fqname[:dotIndex], fqname[dotIndex+1:]\n\t}\n\n\t// Get the part after the last slash.\n\tafterSlash := fqname[lastSlashIndex+1:]\n\n\t// Check for a dot in the substring after the last slash.\n\tdotIndex := strings.Index(afterSlash, \".\")\n\tif dotIndex == -1 {\n\t\t// No dot found after the last slash\n\t\treturn fqname, \"\"\n\t}\n\n\t// Split at the dot to separate the base and the suffix.\n\tbase := fqname[:lastSlashIndex+1+dotIndex]\n\tsuffix := afterSlash[dotIndex+1:]\n\n\treturn base, suffix\n}\n\n// Construct a qualified identifier.\n//\n//\tfqName := fqname.Construct(\"gno.land/r/demo/foo20\", \"GRC20\")\n//\tfmt.Println(\"Fully Qualified Name:\", fqName)\n//\t// Output: gno.land/r/demo/foo20.GRC20\nfunc Construct(pkgpath, name string) string {\n\t// TODO: ensure pkgpath is valid - and as such last part does not contain a dot.\n\tif name == \"\" {\n\t\treturn pkgpath\n\t}\n\treturn pkgpath + \".\" + name\n}\n\n// RenderLink creates a formatted link for a fully qualified identifier.\n// If the package path starts with \"gno.land\", it converts it to a markdown link.\n// If the domain is different or missing, it returns the input as is.\nfunc RenderLink(pkgPath, slug string) string {\n\tif strings.HasPrefix(pkgPath, \"gno.land\") {\n\t\tpkgLink := strings.TrimPrefix(pkgPath, \"gno.land\")\n\t\tif slug != \"\" {\n\t\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \").\" + slug\n\t\t}\n\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \")\"\n\t}\n\tif slug != \"\" {\n\t\treturn pkgPath + \".\" + slug\n\t}\n\treturn pkgPath\n}\n" + }, + { + "name": "fqname_test.gno", + "body": "package fqname\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpectedPkgPath string\n\t\texpectedName string\n\t}{\n\t\t{\"gno.land/p/demo/avl.Tree\", \"gno.land/p/demo/avl\", \"Tree\"},\n\t\t{\"gno.land/p/demo/avl\", \"gno.land/p/demo/avl\", \"\"},\n\t\t{\"gno.land/p/demo/avl.Tree.Node\", \"gno.land/p/demo/avl\", \"Tree.Node\"},\n\t\t{\"gno.land/p/demo/avl/nested.Package.Func\", \"gno.land/p/demo/avl/nested\", \"Package.Func\"},\n\t\t{\"path/filepath.Split\", \"path/filepath\", \"Split\"},\n\t\t{\"path.Split\", \"path\", \"Split\"},\n\t\t{\"path/filepath\", \"path/filepath\", \"\"},\n\t\t{\"path\", \"path\", \"\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpkgpath, name := Parse(tt.input)\n\t\tuassert.Equal(t, tt.expectedPkgPath, pkgpath, \"Package path did not match\")\n\t\tuassert.Equal(t, tt.expectedName, name, \"Name did not match\")\n\t}\n}\n\nfunc TestConstruct(t *testing.T) {\n\ttests := []struct {\n\t\tpkgpath string\n\t\tname string\n\t\texpected string\n\t}{\n\t\t{\"gno.land/r/demo/foo20\", \"GRC20\", \"gno.land/r/demo/foo20.GRC20\"},\n\t\t{\"gno.land/r/demo/foo20\", \"\", \"gno.land/r/demo/foo20\"},\n\t\t{\"path\", \"\", \"path\"},\n\t\t{\"path\", \"Split\", \"path.Split\"},\n\t\t{\"path/filepath\", \"\", \"path/filepath\"},\n\t\t{\"path/filepath\", \"Split\", \"path/filepath.Split\"},\n\t\t{\"\", \"JustName\", \".JustName\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := Construct(tt.pkgpath, tt.name)\n\t\tuassert.Equal(t, tt.expected, result, \"Constructed FQName did not match expected\")\n\t}\n}\n\nfunc TestRenderLink(t *testing.T) {\n\ttests := []struct {\n\t\tpkgPath string\n\t\tslug string\n\t\texpected string\n\t}{\n\t\t{\"gno.land/p/demo/avl\", \"Tree\", \"[gno.land/p/demo/avl](/p/demo/avl).Tree\"},\n\t\t{\"gno.land/p/demo/avl\", \"\", \"[gno.land/p/demo/avl](/p/demo/avl)\"},\n\t\t{\"github.com/a/b\", \"C\", \"github.com/a/b.C\"},\n\t\t{\"example.com/pkg\", \"Func\", \"example.com/pkg.Func\"},\n\t\t{\"gno.land/r/demo/foo20\", \"GRC20\", \"[gno.land/r/demo/foo20](/r/demo/foo20).GRC20\"},\n\t\t{\"gno.land/r/demo/foo20\", \"\", \"[gno.land/r/demo/foo20](/r/demo/foo20)\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := RenderLink(tt.pkgPath, tt.slug)\n\t\tuassert.Equal(t, tt.expected, result, \"Rendered link did not match expected\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "gnode", + "path": "gno.land/p/demo/gnode", + "files": [ + { + "name": "gnode.gno", + "body": "package gnode\n\n// XXX what about Gnodes signing on behalf of others?\n// XXX like a multi-sig of Gnodes?\n\ntype Name string\n\ntype Gnode interface {\n\t//----------------------------------------\n\t// Basic properties\n\tGetName() Name\n\n\t//----------------------------------------\n\t// Affiliate Gnodes\n\tNumAffiliates() int\n\tGetAffiliates(Name) Affiliate\n\tAddAffiliate(Affiliate) error // must be affiliated\n\tRemAffiliate(Name) error // must have become unaffiliated\n\n\t//----------------------------------------\n\t// Signing\n\tNumSignedDocuments() int\n\tGetSignedDocument(idx int) Document\n\tSignDocument(doc Document) (int, error) // index relative to signer\n\n\t//----------------------------------------\n\t// Rendering\n\tRenderLines() []string\n}\n\ntype Affiliate struct {\n\tType string\n\tGnode Gnode\n\tTags []string\n}\n\ntype MyGnode struct {\n\tName\n\t// Owners // voting set, something that gives authority of action.\n\t// Treasury //\n\t// Affiliates //\n\t// Board // discussions\n\t// Data // XXX ?\n}\n\ntype Affiliates []*Affiliate\n\n// Documents are equal if they compare equal.\n// NOTE: requires all fields to be comparable.\ntype Document struct {\n\tAuthors string\n\t// Timestamp\n\t// Body\n\t// Attachments\n}\n\n// ACTIONS\n\n// * Lend tokens\n// * Pay tokens\n// * Administrate transferrable and non-transferrable tokens\n// * Sum tokens\n// * Passthrough dependencies\n// * Code\n// * ...\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "agent", + "path": "gno.land/p/demo/gnorkle/agent", + "files": [ + { + "name": "whitelist.gno", + "body": "package agent\n\nimport \"gno.land/p/demo/avl\"\n\n// Whitelist manages whitelisted agent addresses.\ntype Whitelist struct {\n\tstore *avl.Tree\n}\n\n// ClearAddresses removes all addresses from the whitelist and puts into a state\n// that indicates it is moot and has no whitelist defined.\nfunc (m *Whitelist) ClearAddresses() {\n\tm.store = nil\n}\n\n// AddAddresses adds the given addresses to the whitelist.\nfunc (m *Whitelist) AddAddresses(addresses []string) {\n\tif m.store == nil {\n\t\tm.store = avl.NewTree()\n\t}\n\n\tfor _, address := range addresses {\n\t\tm.store.Set(address, struct{}{})\n\t}\n}\n\n// RemoveAddress removes the given address from the whitelist if it exists.\nfunc (m *Whitelist) RemoveAddress(address string) {\n\tif m.store == nil {\n\t\treturn\n\t}\n\n\tm.store.Remove(address)\n}\n\n// HasDefinition returns true if the whitelist has a definition. It retuns false if\n// `ClearAddresses` has been called without any subsequent `AddAddresses` calls, or\n// if `AddAddresses` has never been called.\nfunc (m Whitelist) HasDefinition() bool {\n\treturn m.store != nil\n}\n\n// HasAddress returns true if the given address is in the whitelist.\nfunc (m Whitelist) HasAddress(address string) bool {\n\tif m.store == nil {\n\t\treturn false\n\t}\n\n\treturn m.store.Has(address)\n}\n" + }, + { + "name": "whitelist_test.gno", + "body": "package agent_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestWhitelist(t *testing.T) {\n\tvar whitelist agent.Whitelist\n\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist should not be defined initially\")\n\n\twhitelist.AddAddresses([]string{\"a\", \"b\"})\n\tuassert.True(t, whitelist.HasAddress(\"a\"), `whitelist should have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should have address \"b\"`)\n\tuassert.True(t, whitelist.HasDefinition(), \"whitelist should be defined after adding addresses\")\n\n\twhitelist.RemoveAddress(\"a\")\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist should not have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should still have address \"b\"`)\n\n\twhitelist.ClearAddresses()\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist cleared; should not have address \"a\"`)\n\tuassert.False(t, whitelist.HasAddress(\"b\"), `whitelist cleared; should still have address \"b\"`)\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist cleared; should not be defined\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "feed", + "path": "gno.land/p/demo/gnorkle/feed", + "files": [ + { + "name": "errors.gno", + "body": "package feed\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined feed\")\n" + }, + { + "name": "task.gno", + "body": "package feed\n\n// Task is a unit of work that can be part of a `Feed` definition. Tasks\n// are executed by agents.\ntype Task interface {\n\tMarshalJSON() ([]byte, error)\n}\n" + }, + { + "name": "type.gno", + "body": "package feed\n\n// Type indicates the type of a feed.\ntype Type int\n\nconst (\n\t// TypeStatic indicates a feed cannot be changed once the first value is committed.\n\tTypeStatic Type = iota\n\t// TypeContinuous indicates a feed can continuously ingest values and will publish\n\t// a new value on request using the values it has ingested.\n\tTypeContinuous\n\t// TypePeriodic indicates a feed can accept one or more values within a certain period\n\t// and will proceed to commit these values at the end up each period to produce an\n\t// aggregate value before starting a new period.\n\tTypePeriodic\n)\n" + }, + { + "name": "value.gno", + "body": "package feed\n\nimport \"time\"\n\n// Value represents a value published by a feed. The `Time` is when the value was published.\ntype Value struct {\n\tString string\n\tTime time.Time\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "ingester", + "path": "gno.land/p/demo/gnorkle/ingester", + "files": [ + { + "name": "errors.gno", + "body": "package ingester\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"ingester undefined\")\n" + }, + { + "name": "type.gno", + "body": "package ingester\n\n// Type indicates an ingester type.\ntype Type int\n\nconst (\n\t// TypeSingle indicates an ingester that can only ingest a single within a given period or no period.\n\tTypeSingle Type = iota\n\t// TypeMulti indicates an ingester that can ingest multiple within a given period or no period\n\tTypeMulti\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "message", + "path": "gno.land/p/demo/gnorkle/message", + "files": [ + { + "name": "parse.gno", + "body": "package message\n\nimport \"strings\"\n\n// ParseFunc parses a raw message and returns the message function\n// type extracted from the remainder of the message.\nfunc ParseFunc(rawMsg string) (FuncType, string) {\n\tfuncType, remainder := parseFirstToken(rawMsg)\n\treturn FuncType(funcType), remainder\n}\n\n// ParseID parses a raw message and returns the ID extracted from\n// the remainder of the message.\nfunc ParseID(rawMsg string) (string, string) {\n\treturn parseFirstToken(rawMsg)\n}\n\nfunc parseFirstToken(rawMsg string) (string, string) {\n\tmsgParts := strings.SplitN(rawMsg, \",\", 2)\n\tif len(msgParts) \u003c 2 {\n\t\treturn msgParts[0], \"\"\n\t}\n\n\treturn msgParts[0], msgParts[1]\n}\n" + }, + { + "name": "parse_test.gno", + "body": "package message_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestParseFunc(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\texpFuncType message.FuncType\n\t\texpRemainder string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t},\n\t\t{\n\t\t\tname: \"func only\",\n\t\t\tinput: \"ingest\",\n\t\t\texpFuncType: message.FuncTypeIngest,\n\t\t},\n\t\t{\n\t\t\tname: \"func with short remainder\",\n\t\t\tinput: \"commit,asdf\",\n\t\t\texpFuncType: message.FuncTypeCommit,\n\t\t\texpRemainder: \"asdf\",\n\t\t},\n\t\t{\n\t\t\tname: \"func with long remainder\",\n\t\t\tinput: \"request,hello,world,goodbye\",\n\t\t\texpFuncType: message.FuncTypeRequest,\n\t\t\texpRemainder: \"hello,world,goodbye\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfuncType, remainder := message.ParseFunc(tt.input)\n\n\t\t\tuassert.Equal(t, string(tt.expFuncType), string(funcType))\n\t\t\tuassert.Equal(t, tt.expRemainder, remainder)\n\t\t})\n\t}\n}\n" + }, + { + "name": "type.gno", + "body": "package message\n\n// FuncType is the type of function that is being called by the agent.\ntype FuncType string\n\nconst (\n\t// FuncTypeIngest means the agent is sending data for ingestion.\n\tFuncTypeIngest FuncType = \"ingest\"\n\t// FuncTypeCommit means the agent is requesting a feed commit the transitive data\n\t// being held by its ingester.\n\tFuncTypeCommit FuncType = \"commit\"\n\t// FuncTypeRequest means the agent is requesting feed definitions for all those\n\t// that it is whitelisted to provide data for.\n\tFuncTypeRequest FuncType = \"request\"\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "gnorkle", + "path": "gno.land/p/demo/gnorkle/gnorkle", + "files": [ + { + "name": "feed.gno", + "body": "package gnorkle\n\nimport (\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Feed is an abstraction used by a gnorkle `Instance` to ingest data from\n// agents and provide data feeds to consumers.\ntype Feed interface {\n\tID() string\n\tType() feed.Type\n\tValue() (value feed.Value, dataType string, consumable bool)\n\tIngest(funcType message.FuncType, rawMessage, providerAddress string) error\n\tMarshalJSON() ([]byte, error)\n\tTasks() []feed.Task\n\tIsActive() bool\n}\n\n// FeedWithWhitelist associates a `Whitelist` with a `Feed`.\ntype FeedWithWhitelist struct {\n\tFeed\n\tWhitelist\n}\n" + }, + { + "name": "ingester.gno", + "body": "package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/ingester\"\n\n// Ingester is the abstraction that allows a `Feed` to ingest data from agents\n// and commit it to storage using zero or more intermediate aggregation steps.\ntype Ingester interface {\n\tType() ingester.Type\n\tIngest(value, providerAddress string) (canAutoCommit bool, err error)\n\tCommitValue(storage Storage, providerAddress string) error\n}\n" + }, + { + "name": "instance.gno", + "body": "package gnorkle\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Instance is a single instance of an oracle.\ntype Instance struct {\n\tfeeds *avl.Tree\n\twhitelist agent.Whitelist\n}\n\n// NewInstance creates a new instance of an oracle.\nfunc NewInstance() *Instance {\n\treturn \u0026Instance{\n\t\tfeeds: avl.NewTree(),\n\t}\n}\n\nfunc assertValidID(id string) error {\n\tif len(id) == 0 {\n\t\treturn errors.New(\"feed ids cannot be empty\")\n\t}\n\n\tif strings.Contains(id, \",\") {\n\t\treturn errors.New(\"feed ids cannot contain commas\")\n\t}\n\n\treturn nil\n}\n\nfunc (i *Instance) assertFeedDoesNotExist(id string) error {\n\tif i.feeds.Has(id) {\n\t\treturn errors.New(\"feed already exists\")\n\t}\n\n\treturn nil\n}\n\n// AddFeeds adds feeds to the instance with empty whitelists.\nfunc (i *Instance) AddFeeds(feeds ...Feed) error {\n\tfor _, feed := range feeds {\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: new(agent.Whitelist),\n\t\t\t\tFeed: feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// AddFeedsWithWhitelists adds feeds to the instance with the given whitelists.\nfunc (i *Instance) AddFeedsWithWhitelists(feeds ...FeedWithWhitelist) error {\n\tfor _, feed := range feeds {\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: feed.Whitelist,\n\t\t\t\tFeed: feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// RemoveFeed removes a feed from the instance.\nfunc (i *Instance) RemoveFeed(id string) {\n\ti.feeds.Remove(id)\n}\n\n// PostMessageHandler is a type that allows for post-processing of feed state after a feed\n// ingests a message from an agent.\ntype PostMessageHandler interface {\n\tHandle(i *Instance, funcType message.FuncType, feed Feed) error\n}\n\n// HandleMessage handles a message from an agent and routes to either the logic that returns\n// feed definitions or the logic that allows a feed to ingest a message.\n//\n// TODO: Consider further message types that could allow administrative action such as modifying\n// a feed's whitelist without the owner of this oracle having to maintain a reference to it.\nfunc (i *Instance) HandleMessage(msg string, postHandler PostMessageHandler) (string, error) {\n\tcaller := string(std.GetOrigCaller())\n\n\tfuncType, msg := message.ParseFunc(msg)\n\n\tswitch funcType {\n\tcase message.FuncTypeRequest:\n\t\treturn i.GetFeedDefinitions(caller)\n\n\tdefault:\n\t\tid, msg := message.ParseID(msg)\n\t\tif err := assertValidID(id); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tfeedWithWhitelist, err := i.getFeedWithWhitelist(id)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, caller, nil) {\n\t\t\treturn \"\", errors.New(\"caller not whitelisted\")\n\t\t}\n\n\t\tif err := feedWithWhitelist.Ingest(funcType, msg, caller); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif postHandler != nil {\n\t\t\tpostHandler.Handle(i, funcType, feedWithWhitelist)\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (i *Instance) getFeed(id string) (Feed, error) {\n\tuntypedFeed, ok := i.feeds.Get(id)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeed, ok := untypedFeed.(Feed)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid feed type\")\n\t}\n\n\treturn feed, nil\n}\n\nfunc (i *Instance) getFeedWithWhitelist(id string) (FeedWithWhitelist, error) {\n\tuntypedFeedWithWhitelist, ok := i.feeds.Get(id)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeedWithWhitelist, ok := untypedFeedWithWhitelist.(FeedWithWhitelist)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid feed with whitelist type\")\n\t}\n\n\treturn feedWithWhitelist, nil\n}\n\n// GetFeedValue returns the most recently published value of a feed along with a string\n// representation of the value's type and boolean indicating whether the value is\n// okay for consumption.\nfunc (i *Instance) GetFeedValue(id string) (feed.Value, string, bool, error) {\n\tfoundFeed, err := i.getFeed(id)\n\tif err != nil {\n\t\treturn feed.Value{}, \"\", false, err\n\t}\n\n\tvalue, valueType, consumable := foundFeed.Value()\n\treturn value, valueType, consumable, nil\n}\n\n// GetFeedDefinitions returns a JSON string representing the feed definitions for which the given\n// agent address is whitelisted to provide values for ingestion.\nfunc (i *Instance) GetFeedDefinitions(forAddress string) (string, error) {\n\tinstanceHasAddressWhitelisted := !i.whitelist.HasDefinition() || i.whitelist.HasAddress(forAddress)\n\n\tbuf := new(strings.Builder)\n\tbuf.WriteString(\"[\")\n\tfirst := true\n\tvar err error\n\n\t// The boolean value returned by this callback function indicates whether to stop iterating.\n\ti.feeds.Iterate(\"\", \"\", func(_ string, value interface{}) bool {\n\t\tfeedWithWhitelist, ok := value.(FeedWithWhitelist)\n\t\tif !ok {\n\t\t\terr = errors.New(\"invalid feed type\")\n\t\t\treturn true\n\t\t}\n\n\t\t// Don't give agents the ability to try to publish to inactive feeds.\n\t\tif !feedWithWhitelist.IsActive() {\n\t\t\treturn false\n\t\t}\n\n\t\t// Skip feeds the address is not whitelisted for.\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, forAddress, \u0026instanceHasAddressWhitelisted) {\n\t\t\treturn false\n\t\t}\n\n\t\tvar taskBytes []byte\n\t\tif taskBytes, err = feedWithWhitelist.Feed.MarshalJSON(); err != nil {\n\t\t\treturn true\n\t\t}\n\n\t\t// Guard against any tasks that shouldn't be returned; maybe they are not active because they have\n\t\t// already been completed.\n\t\tif len(taskBytes) == 0 {\n\t\t\treturn false\n\t\t}\n\n\t\tif !first {\n\t\t\tbuf.WriteString(\",\")\n\t\t}\n\n\t\tfirst = false\n\t\tbuf.Write(taskBytes)\n\t\treturn false\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf.WriteString(\"]\")\n\treturn buf.String(), nil\n}\n" + }, + { + "name": "storage.gno", + "body": "package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/feed\"\n\n// Storage defines how published feed values should be read\n// and written.\ntype Storage interface {\n\tPut(value string) error\n\tGetLatest() feed.Value\n\tGetHistory() []feed.Value\n}\n" + }, + { + "name": "whitelist.gno", + "body": "package gnorkle\n\n// Whitelist is used to manage which agents are allowed to interact.\ntype Whitelist interface {\n\tClearAddresses()\n\tAddAddresses(addresses []string)\n\tRemoveAddress(address string)\n\tHasDefinition() bool\n\tHasAddress(address string) bool\n}\n\n// ClearWhitelist clears the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) ClearWhitelist(feedID string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.ClearAddresses()\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.ClearAddresses()\n\treturn nil\n}\n\n// AddToWhitelist adds the given addresses to the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) AddToWhitelist(feedID string, addresses []string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.AddAddresses(addresses)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.AddAddresses(addresses)\n\treturn nil\n}\n\n// RemoveFromWhitelist removes the given address from the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) RemoveFromWhitelist(feedID string, address string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.RemoveAddress(address)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.RemoveAddress(address)\n\treturn nil\n}\n\n// addressWhiteListed returns true if:\n// - the feed has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has no white list.\nfunc addressIsWhitelisted(instanceWhitelist, feedWhitelist Whitelist, address string, instanceWhitelistedOverride *bool) bool {\n\t// A feed whitelist takes priority, so it will return false if the feed has a whitelist and the caller is\n\t// not a part of it. An empty whitelist defers to the instance whitelist.\n\tif feedWhitelist != nil {\n\t\tif feedWhitelist.HasDefinition() \u0026\u0026 !feedWhitelist.HasAddress(address) {\n\t\t\treturn false\n\t\t}\n\n\t\t// Getting to this point means that one of the following is true:\n\t\t// - the feed has no defined whitelist (so it can't possibly have the address whitelisted)\n\t\t// - the feed has a defined whitelist and the caller is a part of it\n\t\t//\n\t\t// In this case, we can be sure that the boolean indicating whether the feed has this address whitelisted\n\t\t// is equivalent to the boolean indicating whether the feed has a defined whitelist.\n\t\tif feedWhitelist.HasDefinition() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif instanceWhitelistedOverride != nil {\n\t\treturn *instanceWhitelistedOverride\n\t}\n\n\t// We were unable able to determine whether this address is allowed after looking at the feed whitelist,\n\t// so fall back to the instance whitelist. A complete absence of values in the instance whitelist means\n\t// that the instance has no whitelist so we can return true because everything is allowed by default.\n\tif instanceWhitelist == nil || !instanceWhitelist.HasDefinition() {\n\t\treturn true\n\t}\n\n\t// The instance whitelist is defined so if the address is present then it is allowed.\n\treturn instanceWhitelist.HasAddress(address)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "storage", + "path": "gno.land/p/demo/gnorkle/storage", + "files": [ + { + "name": "errors.gno", + "body": "package storage\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined storage\")\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "simple", + "path": "gno.land/p/demo/gnorkle/storage/simple", + "files": [ + { + "name": "storage.gno", + "body": "package simple\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/storage\"\n)\n\n// Storage is simple, bounded storage for published feed values.\ntype Storage struct {\n\tvalues []feed.Value\n\tmaxValues uint\n}\n\n// NewStorage creates a new Storage with the given maximum number of values.\n// If maxValues is 0, the storage is bounded to a size of one. If this is not desirable,\n// then don't provide a value of 0.\nfunc NewStorage(maxValues uint) *Storage {\n\tif maxValues == 0 {\n\t\tmaxValues = 1\n\t}\n\n\treturn \u0026Storage{\n\t\tmaxValues: maxValues,\n\t}\n}\n\n// Put adds a new value to the storage. If the storage is full, the oldest value\n// is removed. If maxValues is 0, the storage is bounded to a size of one.\nfunc (s *Storage) Put(value string) error {\n\tif s == nil {\n\t\treturn storage.ErrUndefined\n\t}\n\n\ts.values = append(s.values, feed.Value{String: value, Time: time.Now()})\n\tif uint(len(s.values)) \u003e s.maxValues {\n\t\ts.values = s.values[1:]\n\t}\n\n\treturn nil\n}\n\n// GetLatest returns the most recently added value, or an empty value if none exist.\nfunc (s Storage) GetLatest() feed.Value {\n\tif len(s.values) == 0 {\n\t\treturn feed.Value{}\n\t}\n\n\treturn s.values[len(s.values)-1]\n}\n\n// GetHistory returns all values in the storage, from oldest to newest.\nfunc (s Storage) GetHistory() []feed.Value {\n\treturn s.values\n}\n" + }, + { + "name": "storage_test.gno", + "body": "package simple_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/storage\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestStorage(t *testing.T) {\n\tvar undefinedStorage *simple.Storage\n\terr := undefinedStorage.Put(\"\")\n\tuassert.ErrorIs(t, err, storage.ErrUndefined, \"expected storage.ErrUndefined on undefined storage\")\n\n\ttests := []struct {\n\t\tname string\n\t\tvaluesToPut []string\n\t\texpLatestValueString string\n\t\texpLatestValueTimeIsZero bool\n\t\texpHistoricalValueStrings []string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\texpLatestValueTimeIsZero: true,\n\t\t},\n\t\t{\n\t\t\tname: \"one value\",\n\t\t\tvaluesToPut: []string{\"one\"},\n\t\t\texpLatestValueString: \"one\",\n\t\t\texpHistoricalValueStrings: []string{\"one\"},\n\t\t},\n\t\t{\n\t\t\tname: \"two values\",\n\t\t\tvaluesToPut: []string{\"one\", \"two\"},\n\t\t\texpLatestValueString: \"two\",\n\t\t\texpHistoricalValueStrings: []string{\"one\", \"two\"},\n\t\t},\n\t\t{\n\t\t\tname: \"three values\",\n\t\t\tvaluesToPut: []string{\"one\", \"two\", \"three\"},\n\t\t\texpLatestValueString: \"three\",\n\t\t\texpHistoricalValueStrings: []string{\"two\", \"three\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsimpleStorage := simple.NewStorage(2)\n\t\t\tfor _, value := range tt.valuesToPut {\n\t\t\t\terr := simpleStorage.Put(value)\n\t\t\t\turequire.NoError(t, err, \"unexpected error putting value in storage\")\n\t\t\t}\n\n\t\t\tlatestValue := simpleStorage.GetLatest()\n\t\t\tuassert.Equal(t, tt.expLatestValueString, latestValue.String)\n\t\t\tuassert.Equal(t, tt.expLatestValueTimeIsZero, latestValue.Time.IsZero())\n\n\t\t\thistoricalValues := simpleStorage.GetHistory()\n\t\t\turequire.Equal(t, len(tt.expHistoricalValueStrings), len(historicalValues), \"historical values length does not match\")\n\n\t\t\tfor i, expValue := range tt.expHistoricalValueStrings {\n\t\t\t\tuassert.Equal(t, historicalValues[i].String, expValue)\n\t\t\t\turequire.False(t, historicalValues[i].Time.IsZero(), ufmt.Sprintf(\"unexpeced zero time for historical value at index %d\", i))\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "single", + "path": "gno.land/p/demo/gnorkle/ingesters/single", + "files": [ + { + "name": "ingester.gno", + "body": "package single\n\nimport (\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n)\n\n// ValueIngester is an ingester that ingests a single value.\ntype ValueIngester struct {\n\tvalue string\n}\n\n// Type returns the type of the ingester.\nfunc (i *ValueIngester) Type() ingester.Type {\n\treturn ingester.TypeSingle\n}\n\n// Ingest ingests a value provided by the given agent address.\nfunc (i *ValueIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i == nil {\n\t\treturn false, ingester.ErrUndefined\n\t}\n\n\ti.value = value\n\treturn true, nil\n}\n\n// CommitValue commits the ingested value to the given storage instance.\nfunc (i *ValueIngester) CommitValue(valueStorer gnorkle.Storage, providerAddress string) error {\n\tif i == nil {\n\t\treturn ingester.ErrUndefined\n\t}\n\n\treturn valueStorer.Put(i.value)\n}\n" + }, + { + "name": "ingester_test.gno", + "body": "package single_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/ingester\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestValueIngester(t *testing.T) {\n\tstorage := simple.NewStorage(1)\n\n\tvar undefinedIngester *single.ValueIngester\n\t_, err := undefinedIngester.Ingest(\"asdf\", \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to Ingest should return ingester.ErrUndefined\")\n\n\terr = undefinedIngester.CommitValue(storage, \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to CommitValue should return ingester.ErrUndefined\")\n\n\tvar valueIngester single.ValueIngester\n\ttyp := valueIngester.Type()\n\tuassert.Equal(t, int(ingester.TypeSingle), int(typ), \"single value ingester should return type ingester.TypeSingle\")\n\n\tingestValue := \"value\"\n\tautocommit, err := valueIngester.Ingest(ingestValue, \"gno11111\")\n\tuassert.True(t, autocommit, \"single value ingester should return autocommit true\")\n\tuassert.NoError(t, err)\n\n\terr = valueIngester.CommitValue(storage, \"gno11111\")\n\tuassert.NoError(t, err)\n\n\tlatestValue := storage.GetLatest()\n\tuassert.Equal(t, ingestValue, latestValue.String)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "static", + "path": "gno.land/p/demo/gnorkle/feeds/static", + "files": [ + { + "name": "feed.gno", + "body": "package static\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Feed is a static feed.\ntype Feed struct {\n\tid string\n\tisLocked bool\n\tvalueDataType string\n\tingester gnorkle.Ingester\n\tstorage gnorkle.Storage\n\ttasks []feed.Task\n}\n\n// NewFeed creates a new static feed.\nfunc NewFeed(\n\tid string,\n\tvalueDataType string,\n\tingester gnorkle.Ingester,\n\tstorage gnorkle.Storage,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn \u0026Feed{\n\t\tid: id,\n\t\tvalueDataType: valueDataType,\n\t\tingester: ingester,\n\t\tstorage: storage,\n\t\ttasks: tasks,\n\t}\n}\n\n// NewSingleValueFeed is a convenience function for creating a static feed\n// that autocommits a value after a single ingestion.\nfunc NewSingleValueFeed(\n\tid string,\n\tvalueDataType string,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn NewFeed(\n\t\tid,\n\t\tvalueDataType,\n\t\t\u0026single.ValueIngester{},\n\t\tsimple.NewStorage(1),\n\t\ttasks...,\n\t)\n}\n\n// ID returns the feed's ID.\nfunc (f Feed) ID() string {\n\treturn f.id\n}\n\n// Type returns the feed's type.\nfunc (f Feed) Type() feed.Type {\n\treturn feed.TypeStatic\n}\n\n// Ingest ingests a message into the feed. It either adds the value to the ingester's\n// pending values or commits the value to the storage.\nfunc (f *Feed) Ingest(funcType message.FuncType, msg, providerAddress string) error {\n\tif f == nil {\n\t\treturn feed.ErrUndefined\n\t}\n\n\tif f.isLocked {\n\t\treturn errors.New(\"feed locked\")\n\t}\n\n\tswitch funcType {\n\tcase message.FuncTypeIngest:\n\t\t// Autocommit the ingester's value if it's a single value ingester\n\t\t// because this is a static feed and this is the only value it will ever have.\n\t\tif canAutoCommit, err := f.ingester.Ingest(msg, providerAddress); canAutoCommit \u0026\u0026 err == nil {\n\t\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tf.isLocked = true\n\t\t} else if err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase message.FuncTypeCommit:\n\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tf.isLocked = true\n\n\tdefault:\n\t\treturn errors.New(\"invalid message function \" + string(funcType))\n\t}\n\n\treturn nil\n}\n\n// Value returns the feed's latest value, it's data type, and whether or not it can\n// be safely consumed. In this case it uses `f.isLocked` because, this being a static\n// feed, it will only ever have one value; once that value is committed the feed is locked\n// and there is a valid, non-empty value to consume.\nfunc (f Feed) Value() (feed.Value, string, bool) {\n\treturn f.storage.GetLatest(), f.valueDataType, f.isLocked\n}\n\n// MarshalJSON marshals the components of the feed that are needed for\n// an agent to execute tasks and send values for ingestion.\nfunc (f Feed) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write([]byte(\n\t\t`{\"id\":\"` + f.id +\n\t\t\t`\",\"type\":\"` + ufmt.Sprintf(\"%d\", int(f.Type())) +\n\t\t\t`\",\"value_type\":\"` + f.valueDataType +\n\t\t\t`\",\"tasks\":[`),\n\t)\n\n\tfirst := true\n\tfor _, task := range f.tasks {\n\t\tif !first {\n\t\t\tw.WriteString(\",\")\n\t\t}\n\n\t\ttaskJSON, err := task.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.Write(taskJSON)\n\t\tfirst = false\n\t}\n\n\tw.Write([]byte(\"]}\"))\n\tw.Flush()\n\n\treturn buf.Bytes(), nil\n}\n\n// Tasks returns the feed's tasks. This allows task consumers to extract task\n// contents without having to marshal the entire feed.\nfunc (f Feed) Tasks() []feed.Task {\n\treturn f.tasks\n}\n\n// IsActive returns true if the feed is accepting ingestion requests from agents.\nfunc (f Feed) IsActive() bool {\n\treturn !f.isLocked\n}\n" + }, + { + "name": "feed_test.gno", + "body": "package static_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\ntype mockIngester struct {\n\tcanAutoCommit bool\n\tingestErr error\n\tcommitErr error\n\tvalue string\n\tproviderAddress string\n}\n\nfunc (i mockIngester) Type() ingester.Type {\n\treturn ingester.Type(0)\n}\n\nfunc (i *mockIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i.ingestErr != nil {\n\t\treturn false, i.ingestErr\n\t}\n\n\ti.value = value\n\ti.providerAddress = providerAddress\n\treturn i.canAutoCommit, nil\n}\n\nfunc (i *mockIngester) CommitValue(storage gnorkle.Storage, providerAddress string) error {\n\tif i.commitErr != nil {\n\t\treturn i.commitErr\n\t}\n\n\treturn storage.Put(i.value)\n}\n\nfunc TestNewSingleValueFeed(t *testing.T) {\n\tstaticFeed := static.NewSingleValueFeed(\"1\", \"\")\n\n\tuassert.Equal(t, \"1\", staticFeed.ID())\n\tuassert.Equal(t, int(feed.TypeStatic), int(staticFeed.Type()))\n}\n\nfunc TestFeed_Ingest(t *testing.T) {\n\tvar undefinedFeed *static.Feed\n\terr := undefinedFeed.Ingest(\"\", \"\", \"\")\n\tuassert.ErrorIs(t, err, feed.ErrUndefined)\n\n\ttests := []struct {\n\t\tname string\n\t\tingester *mockIngester\n\t\tverifyIsLocked bool\n\t\tdoCommit bool\n\t\tfuncType message.FuncType\n\t\tmsg string\n\t\tproviderAddress string\n\t\texpFeedValueString string\n\t\texpErrText string\n\t\texpIsActive bool\n\t}{\n\t\t{\n\t\t\tname: \"func invalid error\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncType(\"derp\"),\n\t\t\texpErrText: \"invalid message function derp\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest ingest error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tingestErr: errors.New(\"ingest error\"),\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\texpErrText: \"ingest error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr: errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\texpErrText: \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func commit commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr: errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeCommit,\n\t\t\texpErrText: \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"only ingest\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ingest autocommit\",\n\t\t\tingester: \u0026mockIngester{canAutoCommit: true},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpFeedValueString: \"still active feed\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname: \"commit no value\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeCommit,\n\t\t\tmsg: \"shouldn't be stored\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ingest then commmit\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"blahblah\",\n\t\t\tdoCommit: true,\n\t\t\texpFeedValueString: \"blahblah\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewFeed(\n\t\t\t\t\"1\",\n\t\t\t\t\"string\",\n\t\t\t\ttt.ingester,\n\t\t\t\tsimple.NewStorage(1),\n\t\t\t\tnil,\n\t\t\t)\n\n\t\t\tvar errText string\n\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\n\t\t\tif tt.doCommit {\n\t\t\t\terr := staticFeed.Ingest(message.FuncTypeCommit, \"\", \"\")\n\t\t\t\turequire.NoError(t, err, \"follow up commit failed\")\n\t\t\t}\n\n\t\t\tif tt.verifyIsLocked {\n\t\t\t\terrText = \"\"\n\t\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\t\terrText = err.Error()\n\t\t\t\t}\n\n\t\t\t\turequire.Equal(t, \"feed locked\", errText)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tt.providerAddress, tt.ingester.providerAddress)\n\n\t\t\tfeedValue, dataType, isLocked := staticFeed.Value()\n\t\t\tuassert.Equal(t, tt.expFeedValueString, feedValue.String)\n\t\t\tuassert.Equal(t, \"string\", dataType)\n\t\t\tuassert.Equal(t, tt.verifyIsLocked, isLocked)\n\t\t\tuassert.Equal(t, tt.expIsActive, staticFeed.IsActive())\n\t\t})\n\t}\n}\n\ntype mockTask struct {\n\terr error\n\tvalue string\n}\n\nfunc (t mockTask) MarshalJSON() ([]byte, error) {\n\tif t.err != nil {\n\t\treturn nil, t.err\n\t}\n\n\treturn []byte(`{\"value\":\"` + t.value + `\"}`), nil\n}\n\nfunc TestFeed_Tasks(t *testing.T) {\n\tid := \"99\"\n\tvalueDataType := \"int\"\n\n\ttests := []struct {\n\t\tname string\n\t\ttasks []feed.Task\n\t\texpErrText string\n\t\texpJSON string\n\t}{\n\t\t{\n\t\t\tname: \"no tasks\",\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"marshal error\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{err: errors.New(\"marshal error\")},\n\t\t\t},\n\t\t\texpErrText: \"marshal error\",\n\t\t},\n\t\t{\n\t\t\tname: \"one task\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"single\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"single\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"two tasks\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"first\"},\n\t\t\t\tmockTask{value: \"second\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"first\"},{\"value\":\"second\"}]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewSingleValueFeed(\n\t\t\t\tid,\n\t\t\t\tvalueDataType,\n\t\t\t\ttt.tasks...,\n\t\t\t)\n\n\t\t\turequire.Equal(t, len(tt.tasks), len(staticFeed.Tasks()))\n\n\t\t\tvar errText string\n\t\t\tjson, err := staticFeed.MarshalJSON()\n\t\t\tif err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\t\t\turequire.Equal(t, tt.expJSON, string(json))\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "exts", + "path": "gno.land/p/demo/grc/exts", + "files": [ + { + "name": "token_metadata.gno", + "body": "package exts\n\ntype TokenMetadata interface {\n\t// Returns the name of the token.\n\tGetName() string\n\n\t// Returns the symbol of the token, usually a shorter version of the\n\t// name.\n\tGetSymbol() string\n\n\t// Returns the decimals places of the token.\n\tGetDecimals() uint\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "grc1155", + "path": "gno.land/p/demo/grc/grc1155", + "files": [ + { + "name": "README.md", + "body": "# GRC-1155 Spec: Multi Token Standard\n\nGRC1155 is a specification for managing multiple tokens based on Gnoland. The name and design is based on Ethereum's ERC1155 standard.\n\n## See also:\n\n[ERC-1155 Spec][erc-1155]\n\n[erc-1155]: https://eips.ethereum.org/EIPS/eip-1155" + }, + { + "name": "basic_grc1155_token.gno", + "body": "package grc1155\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype basicGRC1155Token struct {\n\turi string\n\tbalances avl.Tree // \"TokenId:Address\" -\u003e uint64\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\nvar _ IGRC1155 = (*basicGRC1155Token)(nil)\n\n// Returns new basic GRC1155 token\nfunc NewBasicGRC1155Token(uri string) *basicGRC1155Token {\n\treturn \u0026basicGRC1155Token{\n\t\turi: uri,\n\t\tbalances: avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicGRC1155Token) Uri() string { return s.uri }\n\n// BalanceOf returns the input address's balance of the token type requested\nfunc (s *basicGRC1155Token) BalanceOf(addr std.Address, tid TokenID) (uint64, error) {\n\tif !isValidAddress(addr) {\n\t\treturn 0, ErrInvalidAddress\n\t}\n\n\tkey := string(tid) + \":\" + addr.String()\n\tbalance, found := s.balances.Get(key)\n\tif !found {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(uint64), nil\n}\n\n// BalanceOfBatch returns the balance of multiple account/token pairs\nfunc (s *basicGRC1155Token) BalanceOfBatch(owners []std.Address, batch []TokenID) ([]uint64, error) {\n\tif len(owners) != len(batch) {\n\t\treturn nil, ErrMismatchLength\n\t}\n\n\tbalanceOfBatch := make([]uint64, len(owners))\n\n\tfor i := 0; i \u003c len(owners); i++ {\n\t\tbalanceOfBatch[i], _ = s.BalanceOf(owners[i], batch[i])\n\t}\n\n\treturn balanceOfBatch, nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicGRC1155Token) SetApprovalForAll(operator std.Address, approved bool) error {\n\tif !isValidAddress(operator) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.GetOrigCaller()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// IsApprovedForAll returns true if operator is the owner or is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicGRC1155Token) IsApprovedForAll(owner, operator std.Address) bool {\n\tif operator == owner {\n\t\treturn true\n\t}\n\tkey := owner.String() + \":\" + operator.String()\n\t_, found := s.operatorApprovals.Get(key)\n\tif !found {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeTransferFrom(from, to std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.GetOrigCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, from, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, to, tid, amount})\n\n\treturn nil\n}\n\n// Safely transfers a `batch` of tokens from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.GetOrigCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, from, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, to, batch, amounts})\n\n\treturn nil\n}\n\n// Creates `amount` tokens of token type `id`, and assigns them to `to`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeMint(to std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.mintBatch(to, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, zeroAddress, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, zeroAddress, to, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `SafeMint()`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeBatchMint(to std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.mintBatch(to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, zeroAddress, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, zeroAddress, to, batch, amounts})\n\n\treturn nil\n}\n\n// Destroys `amount` tokens of token type `id` from `from`.\nfunc (s *basicGRC1155Token) Burn(from std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.burnBatch(from, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, zeroAddress, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `Burn()`\nfunc (s *basicGRC1155Token) BatchBurn(from std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.burnBatch(from, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, zeroAddress, batch, amounts})\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll(): approve `operator` to operate on all of `owner` tokens\nfunc (s *basicGRC1155Token) setApprovalForAll(owner, operator std.Address, approved bool) error {\n\tif owner == operator {\n\t\treturn nil\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\tif approved {\n\t\ts.operatorApprovals.Set(key, approved)\n\t} else {\n\t\ts.operatorApprovals.Remove(key)\n\t}\n\n\temit(\u0026ApprovalForAllEvent{owner, operator, approved})\n\n\treturn nil\n}\n\n// Helper for SafeTransferFrom() and SafeBatchTransferFrom()\nfunc (s *basicGRC1155Token) safeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) || !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\tcaller := std.GetOrigCaller()\n\ts.beforeTokenTransfer(caller, from, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrInsufficientBalance\n\t\t}\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfromBalance -= amount\n\t\ttoBalance += amount\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for SafeMint() and SafeBatchMint()\nfunc (s *basicGRC1155Token) mintBatch(to std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.GetOrigCaller()\n\ts.beforeTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoBalance += amount\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for Burn() and BurnBatch()\nfunc (s *basicGRC1155Token) burnBatch(from std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.GetOrigCaller()\n\ts.beforeTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrBurnAmountExceedsBalance\n\t\t}\n\t\tfromBalance -= amount\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\treturn nil\n}\n\nfunc (s *basicGRC1155Token) setUri(newUri string) {\n\ts.uri = newUri\n\temit(\u0026UpdateURIEvent{newUri})\n}\n\nfunc (s *basicGRC1155Token) beforeTokenTransfer(operator, from, to std.Address, batch []TokenID, amounts []uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) afterTokenTransfer(operator, from, to std.Address, batch []TokenID, amounts []uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) doSafeTransferAcceptanceCheck(operator, from, to std.Address, tid TokenID, amount uint64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) doSafeBatchTransferAcceptanceCheck(operator, from, to std.Address, batch []TokenID, amounts []uint64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# URI:%s\\n\", s.uri)\n\n\treturn\n}\n" + }, + { + "name": "basic_grc1155_token_test.gno", + "body": "package grc1155\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nconst dummyURI = \"ipfs://xyz\"\n\nfunc TestNewBasicGRC1155Token(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestUri(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\tuassert.Equal(t, dummyURI, dummy.Uri())\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tbalanceZeroAddressOfToken1, err := dummy.BalanceOf(zeroAddress, tid1)\n\tuassert.Error(t, err, \"should result in error\")\n\n\tbalanceAddr1OfToken1, err := dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceAddr1OfToken1)\n\n\tdummy.mintBatch(addr1, []TokenID{tid1, tid2}, []uint64{10, 100})\n\tdummy.mintBatch(addr2, []TokenID{tid1}, []uint64{20})\n\n\tbalanceAddr1OfToken1, err = dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr1OfToken2, err := dummy.BalanceOf(addr1, tid2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr2OfToken1, err := dummy.BalanceOf(addr2, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, uint64(10), balanceAddr1OfToken1)\n\tuassert.Equal(t, uint64(100), balanceAddr1OfToken2)\n\tuassert.Equal(t, uint64(20), balanceAddr2OfToken1)\n}\n\nfunc TestBalanceOfBatch(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceBatch[0])\n\tuassert.Equal(t, uint64(0), balanceBatch[1])\n\n\tdummy.mintBatch(addr1, []TokenID{tid1}, []uint64{10})\n\tdummy.mintBatch(addr2, []TokenID{tid2}, []uint64{20})\n\n\tbalanceBatch, err = dummy.BalanceOfBatch([]std.Address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(10), balanceBatch[0])\n\tuassert.Equal(t, uint64(20), balanceBatch[1])\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.GetOrigCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n\n\terr = dummy.SetApprovalForAll(addr, false)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.GetOrigCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid := TokenID(\"1\")\n\n\tdummy.mintBatch(caller, []TokenID{tid}, []uint64{100})\n\n\terr := dummy.SafeTransferFrom(caller, zeroAddress, tid, 10)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 160)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 60)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(40), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(60), balanceOfAddr)\n}\n\nfunc TestSafeBatchTransferFrom(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.GetOrigCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(caller, []TokenID{tid1, tid2}, []uint64{10, 100})\n\n\terr := dummy.SafeBatchTransferFrom(caller, zeroAddress, []TokenID{tid1, tid2}, []uint64{4, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []uint64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1}, []uint64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []uint64{4, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{caller, addr, caller, addr}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of caller after batch transfer\n\tuassert.Equal(t, uint64(6), balanceBatch[0])\n\n\t// Check token1's balance of addr after batch transfer\n\tuassert.Equal(t, uint64(4), balanceBatch[1])\n\n\t// Check token2's balance of caller after batch transfer\n\tuassert.Equal(t, uint64(40), balanceBatch[2])\n\n\t// Check token2's balance of addr after batch transfer\n\tuassert.Equal(t, uint64(60), balanceBatch[3])\n}\n\nfunc TestSafeMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeMint(zeroAddress, tid1, 100)\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeMint(addr1, tid1, 100)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr1, tid2, 200)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr2, tid1, 50)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2, addr1}, []TokenID{tid1, tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after mint\n\tuassert.Equal(t, uint64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after mint\n\tuassert.Equal(t, uint64(50), balanceBatch[1])\n\t// Check token2's balance of addr1 after mint\n\tuassert.Equal(t, uint64(200), balanceBatch[2])\n}\n\nfunc TestSafeBatchMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeBatchMint(zeroAddress, []TokenID{tid1, tid2}, []uint64{100, 200})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchMint(addr1, []TokenID{tid1, tid2}, []uint64{100, 200})\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeBatchMint(addr2, []TokenID{tid1, tid2}, []uint64{300, 400})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2, addr1, addr2}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after batch mint\n\tuassert.Equal(t, uint64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after batch mint\n\tuassert.Equal(t, uint64(300), balanceBatch[1])\n\t// Check token2's balance of addr1 after batch mint\n\tuassert.Equal(t, uint64(200), balanceBatch[2])\n\t// Check token2's balance of addr2 after batch mint\n\tuassert.Equal(t, uint64(400), balanceBatch[3])\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []uint64{100, 200})\n\terr := dummy.Burn(zeroAddress, tid1, uint64(60))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, uint64(160))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, uint64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Burn(addr, tid2, uint64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after burn\n\tuassert.Equal(t, uint64(40), balanceBatch[0])\n\t// Check token2's balance of addr after burn\n\tuassert.Equal(t, uint64(140), balanceBatch[1])\n}\n\nfunc TestBatchBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []uint64{100, 200})\n\terr := dummy.BatchBurn(zeroAddress, []TokenID{tid1, tid2}, []uint64{60, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []uint64{160, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []uint64{60, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after batch burn\n\tuassert.Equal(t, uint64(40), balanceBatch[0])\n\t// Check token2's balance of addr after batch burn\n\tuassert.Equal(t, uint64(140), balanceBatch[1])\n}\n" + }, + { + "name": "errors.gno", + "body": "package grc1155\n\nimport \"errors\"\n\nvar (\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrMismatchLength = errors.New(\"accounts and ids length mismatch\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n\tErrTransferToRejectedOrNonGRC1155Receiver = errors.New(\"transfer to rejected or non GRC1155Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrInsufficientBalance = errors.New(\"insufficient balance for transfer\")\n\tErrBurnAmountExceedsBalance = errors.New(\"burn amount exceeds balance\")\n)\n" + }, + { + "name": "igrc1155.gno", + "body": "package grc1155\n\nimport \"std\"\n\ntype IGRC1155 interface {\n\tSafeTransferFrom(from, to std.Address, tid TokenID, amount uint64) error\n\tSafeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error\n\tBalanceOf(owner std.Address, tid TokenID) (uint64, error)\n\tBalanceOfBatch(owners []std.Address, batch []TokenID) ([]uint64, error)\n\tSetApprovalForAll(operator std.Address, approved bool) error\n\tIsApprovedForAll(owner, operator std.Address) bool\n}\n\ntype TokenID string\n\ntype TransferSingleEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tTokenID TokenID\n\tAmount uint64\n}\n\ntype TransferBatchEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tBatch []TokenID\n\tAmounts []uint64\n}\n\ntype ApprovalForAllEvent struct {\n\tOwner std.Address\n\tOperator std.Address\n\tApproved bool\n}\n\ntype UpdateURIEvent struct {\n\tURI string\n}\n" + }, + { + "name": "util.gno", + "body": "package grc1155\n\nimport (\n\t\"std\"\n)\n\nconst zeroAddress std.Address = \"\"\n\nfunc isValidAddress(addr std.Address) bool {\n\tif !addr.IsValid() {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc emit(event interface{}) {\n\t// TODO: setup a pubsub system here?\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "grc20", + "path": "gno.land/p/demo/grc/grc20", + "files": [ + { + "name": "banker.gno", + "body": "package grc20\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Banker implements a token banker with admin privileges.\n//\n// The Banker is intended to be used in two main ways:\n// 1. as a temporary object used to make the initial minting, then deleted.\n// 2. preserved in an unexported variable to support conditional administrative\n// tasks protected by the contract.\ntype Banker struct {\n\tname string\n\tsymbol string\n\tdecimals uint\n\ttotalSupply uint64\n\tbalances avl.Tree // std.Address(owner) -\u003e uint64\n\tallowances avl.Tree // string(owner+\":\"+spender) -\u003e uint64\n\ttoken *token // to share the same pointer\n}\n\nfunc NewBanker(name, symbol string, decimals uint) *Banker {\n\tif name == \"\" {\n\t\tpanic(\"name should not be empty\")\n\t}\n\tif symbol == \"\" {\n\t\tpanic(\"symbol should not be empty\")\n\t}\n\t// XXX additional checks (length, characters, limits, etc)\n\n\tb := Banker{\n\t\tname: name,\n\t\tsymbol: symbol,\n\t\tdecimals: decimals,\n\t}\n\tt := \u0026token{banker: \u0026b}\n\tb.token = t\n\treturn \u0026b\n}\n\nfunc (b Banker) Token() Token { return b.token } // Token returns a grc20 safe-object implementation.\nfunc (b Banker) GetName() string { return b.name }\nfunc (b Banker) GetSymbol() string { return b.symbol }\nfunc (b Banker) GetDecimals() uint { return b.decimals }\nfunc (b Banker) TotalSupply() uint64 { return b.totalSupply }\nfunc (b Banker) KnownAccounts() int { return b.balances.Size() }\n\nfunc (b *Banker) Mint(address std.Address, amount uint64) error {\n\tif !address.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\t// TODO: check for overflow\n\n\tb.totalSupply += amount\n\tcurrentBalance := b.BalanceOf(address)\n\tnewBalance := currentBalance + amount\n\n\tb.balances.Set(string(address), newBalance)\n\n\tstd.Emit(\n\t\tMintEvent,\n\t\t\"from\", \"\",\n\t\t\"to\", string(address),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b *Banker) Burn(address std.Address, amount uint64) error {\n\tif !address.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\t// TODO: check for overflow\n\n\tcurrentBalance := b.BalanceOf(address)\n\tif currentBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tb.totalSupply -= amount\n\tnewBalance := currentBalance - amount\n\n\tb.balances.Set(string(address), newBalance)\n\n\tstd.Emit(\n\t\tBurnEvent,\n\t\t\"from\", string(address),\n\t\t\"to\", \"\",\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b Banker) BalanceOf(address std.Address) uint64 {\n\tbalance, found := b.balances.Get(address.String())\n\tif !found {\n\t\treturn 0\n\t}\n\treturn balance.(uint64)\n}\n\nfunc (b *Banker) SpendAllowance(owner, spender std.Address, amount uint64) error {\n\tif !owner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcurrentAllowance := b.Allowance(owner, spender)\n\tif currentAllowance \u003c amount {\n\t\treturn ErrInsufficientAllowance\n\t}\n\n\tkey := allowanceKey(owner, spender)\n\tnewAllowance := currentAllowance - amount\n\n\tif newAllowance == 0 {\n\t\tb.allowances.Remove(key)\n\t} else {\n\t\tb.allowances.Set(key, newAllowance)\n\t}\n\n\treturn nil\n}\n\nfunc (b *Banker) Transfer(from, to std.Address, amount uint64) error {\n\tif !from.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\ttoBalance := b.BalanceOf(to)\n\tfromBalance := b.BalanceOf(from)\n\n\tif fromBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tnewToBalance := toBalance + amount\n\tnewFromBalance := fromBalance - amount\n\n\tb.balances.Set(string(to), newToBalance)\n\tb.balances.Set(string(from), newFromBalance)\n\n\tstd.Emit(\n\t\tTransferEvent,\n\t\t\"from\", from.String(),\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b *Banker) TransferFrom(spender, from, to std.Address, amount uint64) error {\n\tif err := b.SpendAllowance(from, spender, amount); err != nil {\n\t\treturn err\n\t}\n\treturn b.Transfer(from, to, amount)\n}\n\nfunc (b *Banker) Allowance(owner, spender std.Address) uint64 {\n\tallowance, found := b.allowances.Get(allowanceKey(owner, spender))\n\tif !found {\n\t\treturn 0\n\t}\n\treturn allowance.(uint64)\n}\n\nfunc (b *Banker) Approve(owner, spender std.Address, amount uint64) error {\n\tif !owner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tb.allowances.Set(allowanceKey(owner, spender), amount)\n\n\tstd.Emit(\n\t\tApprovalEvent,\n\t\t\"owner\", string(owner),\n\t\t\"spender\", string(spender),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b *Banker) RenderHome() string {\n\tstr := \"\"\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", b.name, b.symbol)\n\tstr += ufmt.Sprintf(\"* **Decimals**: %d\\n\", b.decimals)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", b.totalSupply)\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", b.KnownAccounts())\n\treturn str\n}\n\nfunc allowanceKey(owner, spender std.Address) string {\n\treturn owner.String() + \":\" + spender.String()\n}\n" + }, + { + "name": "banker_test.gno", + "body": "package grc20\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestBankerImpl(t *testing.T) {\n\tdummy := NewBanker(\"Dummy\", \"DUMMY\", 4)\n\turequire.False(t, dummy == nil, \"dummy should not be nil\")\n}\n\nfunc TestAllowance(t *testing.T) {\n\tvar (\n\t\towner = testutils.TestAddress(\"owner\")\n\t\tspender = testutils.TestAddress(\"spender\")\n\t\tdest = testutils.TestAddress(\"dest\")\n\t)\n\n\tb := NewBanker(\"Dummy\", \"DUMMY\", 6)\n\turequire.NoError(t, b.Mint(owner, 100000000))\n\turequire.NoError(t, b.Approve(owner, spender, 5000000))\n\turequire.Error(t, b.TransferFrom(spender, owner, dest, 10000000), ErrInsufficientAllowance.Error(), \"should not be able to transfer more than approved\")\n\n\ttests := []struct {\n\t\tspend uint64\n\t\texp uint64\n\t}{\n\t\t{3, 4999997},\n\t\t{999997, 4000000},\n\t\t{4000000, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb0 := b.BalanceOf(dest)\n\t\turequire.NoError(t, b.TransferFrom(spender, owner, dest, tt.spend))\n\t\ta := b.Allowance(owner, spender)\n\t\turequire.Equal(t, a, tt.exp, ufmt.Sprintf(\"allowance exp: %d, got %d\", tt.exp, a))\n\t\tb := b.BalanceOf(dest)\n\t\texpB := b0 + tt.spend\n\t\turequire.Equal(t, b, expB, ufmt.Sprintf(\"balance exp: %d, got %d\", expB, b))\n\t}\n\n\turequire.Error(t, b.TransferFrom(spender, owner, dest, 1), \"no allowance\")\n\tkey := allowanceKey(owner, spender)\n\turequire.False(t, b.allowances.Has(key), \"allowance should be removed\")\n\turequire.Equal(t, b.Allowance(owner, spender), uint64(0), \"allowance should be 0\")\n}\n" + }, + { + "name": "token.gno", + "body": "package grc20\n\nimport (\n\t\"std\"\n)\n\n// token implements the Token interface.\n//\n// It is generated with Banker.Token().\n// It can safely be exposed publicly.\ntype token struct {\n\tbanker *Banker\n}\n\n// var _ Token = (*token)(nil)\nfunc (t *token) GetName() string { return t.banker.name }\nfunc (t *token) GetSymbol() string { return t.banker.symbol }\nfunc (t *token) GetDecimals() uint { return t.banker.decimals }\nfunc (t *token) TotalSupply() uint64 { return t.banker.totalSupply }\n\nfunc (t *token) BalanceOf(owner std.Address) uint64 {\n\treturn t.banker.BalanceOf(owner)\n}\n\nfunc (t *token) Transfer(to std.Address, amount uint64) error {\n\tcaller := std.PrevRealm().Addr()\n\treturn t.banker.Transfer(caller, to, amount)\n}\n\nfunc (t *token) Allowance(owner, spender std.Address) uint64 {\n\treturn t.banker.Allowance(owner, spender)\n}\n\nfunc (t *token) Approve(spender std.Address, amount uint64) error {\n\tcaller := std.PrevRealm().Addr()\n\treturn t.banker.Approve(caller, spender, amount)\n}\n\nfunc (t *token) TransferFrom(from, to std.Address, amount uint64) error {\n\tspender := std.PrevRealm().Addr()\n\tif err := t.banker.SpendAllowance(from, spender, amount); err != nil {\n\t\treturn err\n\t}\n\treturn t.banker.Transfer(from, to, amount)\n}\n" + }, + { + "name": "token_test.gno", + "body": "package grc20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestUserTokenImpl(t *testing.T) {\n\tbank := NewBanker(\"Dummy\", \"DUMMY\", 4)\n\ttok := bank.Token()\n\t_ = tok\n}\n\nfunc TestUserApprove(t *testing.T) {\n\towner := testutils.TestAddress(\"owner\")\n\tspender := testutils.TestAddress(\"spender\")\n\tdest := testutils.TestAddress(\"dest\")\n\n\tbank := NewBanker(\"Dummy\", \"DUMMY\", 6)\n\ttok := bank.Token()\n\n\t// Set owner as the original caller\n\tstd.TestSetOrigCaller(owner)\n\t// Mint 100000000 tokens for owner\n\turequire.NoError(t, bank.Mint(owner, 100000000))\n\n\t// Approve spender to spend 5000000 tokens\n\turequire.NoError(t, tok.Approve(spender, 5000000))\n\n\t// Set spender as the original caller\n\tstd.TestSetOrigCaller(spender)\n\t// Try to transfer 10000000 tokens from owner to dest, should fail because it exceeds allowance\n\turequire.Error(t,\n\t\ttok.TransferFrom(owner, dest, 10000000),\n\t\tErrInsufficientAllowance.Error(),\n\t\t\"should not be able to transfer more than approved\",\n\t)\n\n\t// Define a set of test data with spend amount and expected remaining allowance\n\ttests := []struct {\n\t\tspend uint64 // Spend amount\n\t\texp uint64 // Remaining allowance\n\t}{\n\t\t{3, 4999997},\n\t\t{999997, 4000000},\n\t\t{4000000, 0},\n\t}\n\n\t// perform transfer operation,and check if allowance and balance are correct\n\tfor _, tt := range tests {\n\t\tb0 := tok.BalanceOf(dest)\n\t\t// Perform transfer from owner to dest\n\t\turequire.NoError(t, tok.TransferFrom(owner, dest, tt.spend))\n\t\ta := tok.Allowance(owner, spender)\n\t\t// Check if allowance equals expected value\n\t\turequire.True(t, a == tt.exp, ufmt.Sprintf(\"allowance exp: %d,got %d\", tt.exp, a))\n\n\t\t// Get dest current balance\n\t\tb := tok.BalanceOf(dest)\n\t\t// Calculate expected balance ,should be initial balance plus transfer amount\n\t\texpB := b0 + tt.spend\n\t\t// Check if balance equals expected value\n\t\turequire.True(t, b == expB, ufmt.Sprintf(\"balance exp: %d,got %d\", expB, b))\n\t}\n\n\t// Try to transfer one token from owner to dest ,should fail because no allowance left\n\turequire.Error(t, tok.TransferFrom(owner, dest, 1), ErrInsufficientAllowance.Error(), \"no allowance\")\n}\n" + }, + { + "name": "types.gno", + "body": "package grc20\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/exts\"\n)\n\nvar (\n\tErrInsufficientBalance = errors.New(\"insufficient balance\")\n\tErrInsufficientAllowance = errors.New(\"insufficient allowance\")\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n)\n\ntype Token interface {\n\texts.TokenMetadata\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() uint64\n\n\t// Returns the amount of tokens owned by `account`.\n\tBalanceOf(account std.Address) uint64\n\n\t// Moves `amount` tokens from the caller's account to `to`.\n\t//\n\t// Returns an error if the operation failed.\n\tTransfer(to std.Address, amount uint64) error\n\n\t// Returns the remaining number of tokens that `spender` will be\n\t// allowed to spend on behalf of `owner` through {transferFrom}. This is\n\t// zero by default.\n\t//\n\t// This value changes when {approve} or {transferFrom} are called.\n\tAllowance(owner, spender std.Address) uint64\n\n\t// Sets `amount` as the allowance of `spender` over the caller's tokens.\n\t//\n\t// Returns an error if the operation failed.\n\t//\n\t// IMPORTANT: Beware that changing an allowance with this method brings the risk\n\t// that someone may use both the old and the new allowance by unfortunate\n\t// transaction ordering. One possible solution to mitigate this race\n\t// condition is to first reduce the spender's allowance to 0 and set the\n\t// desired value afterwards:\n\t// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n\tApprove(spender std.Address, amount uint64) error\n\n\t// Moves `amount` tokens from `from` to `to` using the\n\t// allowance mechanism. `amount` is then deducted from the caller's\n\t// allowance.\n\t//\n\t// Returns an error if the operation failed.\n\tTransferFrom(from, to std.Address, amount uint64) error\n}\n\nconst (\n\tMintEvent = \"Mint\"\n\tBurnEvent = \"Burn\"\n\tTransferEvent = \"Transfer\"\n\tApprovalEvent = \"Approval\"\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "grc721", + "path": "gno.land/p/demo/grc/grc721", + "files": [ + { + "name": "basic_nft.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype basicNFT struct {\n\tname string\n\tsymbol string\n\towners avl.Tree // tokenId -\u003e OwnerAddress\n\tbalances avl.Tree // OwnerAddress -\u003e TokenCount\n\ttokenApprovals avl.Tree // TokenId -\u003e ApprovedAddress\n\ttokenURIs avl.Tree // TokenId -\u003e URIs\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\n// Returns new basic NFT\nfunc NewBasicNFT(name string, symbol string) *basicNFT {\n\treturn \u0026basicNFT{\n\t\tname: name,\n\t\tsymbol: symbol,\n\n\t\towners: avl.Tree{},\n\t\tbalances: avl.Tree{},\n\t\ttokenApprovals: avl.Tree{},\n\t\ttokenURIs: avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicNFT) Name() string { return s.name }\nfunc (s *basicNFT) Symbol() string { return s.symbol }\nfunc (s *basicNFT) TokenCount() uint64 { return uint64(s.owners.Size()) }\n\n// BalanceOf returns balance of input address\nfunc (s *basicNFT) BalanceOf(addr std.Address) (uint64, error) {\n\tif err := isValidAddress(addr); err != nil {\n\t\treturn 0, err\n\t}\n\n\tbalance, found := s.balances.Get(addr.String())\n\tif !found {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(uint64), nil\n}\n\n// OwnerOf returns owner of input token id\nfunc (s *basicNFT) OwnerOf(tid TokenID) (std.Address, error) {\n\towner, found := s.owners.Get(string(tid))\n\tif !found {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn owner.(std.Address), nil\n}\n\n// TokenURI returns the URI of input token id\nfunc (s *basicNFT) TokenURI(tid TokenID) (string, error) {\n\turi, found := s.tokenURIs.Get(string(tid))\n\tif !found {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn uri.(string), nil\n}\n\nfunc (s *basicNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) {\n\t// check for invalid TokenID\n\tif !s.exists(tid) {\n\t\treturn false, ErrInvalidTokenId\n\t}\n\n\t// check for the right owner\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner {\n\t\treturn false, ErrCallerIsNotOwner\n\t}\n\ts.tokenURIs.Set(string(tid), string(tURI))\n\treturn true, nil\n}\n\n// IsApprovedForAll returns true if operator is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicNFT) IsApprovedForAll(owner, operator std.Address) bool {\n\tkey := owner.String() + \":\" + operator.String()\n\t_, found := s.operatorApprovals.Get(key)\n\tif !found {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Approve approves the input address for particular token\nfunc (s *basicNFT) Approve(to std.Address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner == to {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner \u0026\u0026 !s.IsApprovedForAll(owner, caller) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\ts.tokenApprovals.Set(string(tid), to.String())\n\tevent := ApprovalEvent{owner, to, tid}\n\temit(\u0026event)\n\n\treturn nil\n}\n\n// GetApproved return the approved address for token\nfunc (s *basicNFT) GetApproved(tid TokenID) (std.Address, error) {\n\taddr, found := s.tokenApprovals.Get(string(tid))\n\tif !found {\n\t\treturn zeroAddress, ErrTokenIdNotHasApproved\n\t}\n\n\treturn std.Address(addr.(string)), nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicNFT) SetApprovalForAll(operator std.Address, approved bool) error {\n\tif err := isValidAddress(operator); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.PrevRealm().Addr()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC721 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error {\n\tcaller := std.PrevRealm().Addr()\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(from, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\n// Transfers `tokenId` token from `from` to `to`.\nfunc (s *basicNFT) TransferFrom(from, to std.Address, tid TokenID) error {\n\tcaller := std.PrevRealm().Addr()\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Mints `tokenId` and transfers it to `to`.\nfunc (s *basicNFT) Mint(to std.Address, tid TokenID) error {\n\treturn s.mint(to, tid)\n}\n\n// Mints `tokenId` and transfers it to `to`. Also checks that\n// contract recipients are using GRC721 protocol\nfunc (s *basicNFT) SafeMint(to std.Address, tid TokenID) error {\n\terr := s.mint(to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(zeroAddress, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\nfunc (s *basicNFT) Burn(tid TokenID) error {\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.beforeTokenTransfer(owner, zeroAddress, tid, 1)\n\n\ts.tokenApprovals.Remove(string(tid))\n\tbalance, err := s.BalanceOf(owner)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbalance -= 1\n\ts.balances.Set(owner.String(), balance)\n\ts.owners.Remove(string(tid))\n\n\tevent := TransferEvent{owner, zeroAddress, tid}\n\temit(\u0026event)\n\n\ts.afterTokenTransfer(owner, zeroAddress, tid, 1)\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll()\nfunc (s *basicNFT) setApprovalForAll(owner, operator std.Address, approved bool) error {\n\tif owner == operator {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\ts.operatorApprovals.Set(key, approved)\n\n\tevent := ApprovalForAllEvent{owner, operator, approved}\n\temit(\u0026event)\n\n\treturn nil\n}\n\n// Helper for TransferFrom() and SafeTransferFrom()\nfunc (s *basicNFT) transfer(from, to std.Address, tid TokenID) error {\n\tif err := isValidAddress(from); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\tif err := isValidAddress(to); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.beforeTokenTransfer(from, to, tid, 1)\n\n\t// Check that tokenId was not transferred by `beforeTokenTransfer`\n\towner, err = s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.tokenApprovals.Remove(string(tid))\n\tfromBalance, err := s.BalanceOf(from)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfromBalance -= 1\n\ttoBalance += 1\n\ts.balances.Set(from.String(), fromBalance)\n\ts.balances.Set(to.String(), toBalance)\n\ts.owners.Set(string(tid), to)\n\n\tevent := TransferEvent{from, to, tid}\n\temit(\u0026event)\n\n\ts.afterTokenTransfer(from, to, tid, 1)\n\n\treturn nil\n}\n\n// Helper for Mint() and SafeMint()\nfunc (s *basicNFT) mint(to std.Address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ts.beforeTokenTransfer(zeroAddress, to, tid, 1)\n\n\t// Check that tokenId was not minted by `beforeTokenTransfer`\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance += 1\n\ts.balances.Set(to.String(), toBalance)\n\ts.owners.Set(string(tid), to)\n\n\tevent := TransferEvent{zeroAddress, to, tid}\n\temit(\u0026event)\n\n\ts.afterTokenTransfer(zeroAddress, to, tid, 1)\n\n\treturn nil\n}\n\nfunc (s *basicNFT) isApprovedOrOwner(addr std.Address, tid TokenID) bool {\n\towner, found := s.owners.Get(string(tid))\n\tif !found {\n\t\treturn false\n\t}\n\n\tif addr == owner.(std.Address) || s.IsApprovedForAll(owner.(std.Address), addr) {\n\t\treturn true\n\t}\n\n\t_, err := s.GetApproved(tid)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Checks if token id already exists\nfunc (s *basicNFT) exists(tid TokenID) bool {\n\t_, found := s.owners.Get(string(tid))\n\treturn found\n}\n\nfunc (s *basicNFT) beforeTokenTransfer(from, to std.Address, firstTokenId TokenID, batchSize uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicNFT) afterTokenTransfer(from, to std.Address, firstTokenId TokenID, batchSize uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicNFT) checkOnGRC721Received(from, to std.Address, tid TokenID) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicNFT) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", s.name, s.symbol)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", s.TokenCount())\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", s.balances.Size())\n\n\treturn\n}\n" + }, + { + "name": "basic_nft_test.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tdummyNFTName = \"DummyNFT\"\n\tdummyNFTSymbol = \"DNFT\"\n)\n\nfunc TestNewBasicNFT(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestName(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tname := dummy.Name()\n\tuassert.Equal(t, dummyNFTName, name)\n}\n\nfunc TestSymbol(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tsymbol := dummy.Symbol()\n\tuassert.Equal(t, dummyNFTSymbol, symbol)\n}\n\nfunc TestTokenCount(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcount := dummy.TokenCount()\n\tuassert.Equal(t, uint64(0), count)\n\n\tdummy.mint(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", TokenID(\"1\"))\n\tdummy.mint(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", TokenID(\"2\"))\n\n\tcount = dummy.TokenCount()\n\tuassert.Equal(t, uint64(2), count)\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tbalanceAddr1, err := dummy.BalanceOf(addr1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceAddr1)\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\tdummy.mint(addr1, TokenID(\"2\"))\n\tdummy.mint(addr2, TokenID(\"3\"))\n\n\tbalanceAddr1, err = dummy.BalanceOf(addr1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceAddr2, err := dummy.BalanceOf(addr2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, uint64(2), balanceAddr1)\n\tuassert.Equal(t, uint64(1), balanceAddr2)\n}\n\nfunc TestOwnerOf(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\towner, err := dummy.OwnerOf(TokenID(\"invalid\"))\n\tuassert.Error(t, err, \"should not result in error\")\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\tdummy.mint(addr2, TokenID(\"2\"))\n\n\t// Checking for token id \"1\"\n\towner, err = dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr1.String(), owner.String())\n\n\t// Checking for token id \"2\"\n\towner, err = dummy.OwnerOf(TokenID(\"2\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr2.String(), owner.String())\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n}\n\nfunc TestGetApproved(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tapprovedAddr, err := dummy.GetApproved(TokenID(\"invalid\"))\n\tuassert.Error(t, err, \"should result in error\")\n}\n\nfunc TestApprove(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\n\t_, err := dummy.GetApproved(TokenID(\"1\"))\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.Approve(addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tapprovedAddr, err := dummy.GetApproved(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, addr.String(), approvedAddr.String())\n}\n\nfunc TestTransferFrom(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\tdummy.mint(caller, TokenID(\"2\"))\n\n\terr := dummy.TransferFrom(caller, addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller)\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfAddr)\n\n\t// Check Owner of transferred Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, addr.String(), owner.String())\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\tdummy.mint(caller, TokenID(\"2\"))\n\n\terr := dummy.SafeTransferFrom(caller, addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfAddr)\n\n\t// Check Owner of transferred Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr.String(), owner.String())\n}\n\nfunc TestMint(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\terr := dummy.Mint(addr1, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Mint(addr1, TokenID(\"2\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Mint(addr2, TokenID(\"3\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Try minting duplicate token id\n\terr = dummy.Mint(addr2, TokenID(\"1\"))\n\tuassert.Error(t, err, \"should not result in error\")\n\n\t// Check Owner of Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr1.String(), owner.String())\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(addr, TokenID(\"1\"))\n\tdummy.mint(addr, TokenID(\"2\"))\n\n\terr := dummy.Burn(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check Owner of Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.Error(t, err, \"should result in error\")\n}\n\nfunc TestSetTokenURI(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\ttokenURI := \"http://example.com/token\"\n\n\tstd.TestSetOrigCaller(std.Address(addr1)) // addr1\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\t_, derr := dummy.SetTokenURI(TokenID(\"1\"), TokenURI(tokenURI))\n\tuassert.NoError(t, derr, \"should not result in error\")\n\n\t// Test case: Invalid token ID\n\t_, err := dummy.SetTokenURI(TokenID(\"3\"), TokenURI(tokenURI))\n\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\n\tstd.TestSetOrigCaller(std.Address(addr2)) // addr2\n\n\t_, cerr := dummy.SetTokenURI(TokenID(\"1\"), TokenURI(tokenURI)) // addr2 trying to set URI for token 1\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Test case: Retrieving TokenURI\n\tstd.TestSetOrigCaller(std.Address(addr1)) // addr1\n\n\tdummyTokenURI, err := dummy.TokenURI(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"TokenURI error\")\n\tuassert.Equal(t, string(tokenURI), string(dummyTokenURI))\n}\n" + }, + { + "name": "errors.gno", + "body": "package grc721\n\nimport \"errors\"\n\nvar (\n\tErrInvalidTokenId = errors.New(\"invalid token id\")\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrTokenIdNotHasApproved = errors.New(\"token id not approved for anyone\")\n\tErrApprovalToCurrentOwner = errors.New(\"approval to current owner\")\n\tErrCallerIsNotOwner = errors.New(\"caller is not token owner\")\n\tErrCallerNotApprovedForAll = errors.New(\"caller is not approved for all\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n\tErrTransferFromIncorrectOwner = errors.New(\"transfer from incorrect owner\")\n\tErrTransferToNonGRC721Receiver = errors.New(\"transfer to non GRC721Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrTokenIdAlreadyExists = errors.New(\"token id already exists\")\n\n\t// ERC721Royalty\n\tErrInvalidRoyaltyPercentage = errors.New(\"invalid royalty percentage\")\n\tErrInvalidRoyaltyPaymentAddress = errors.New(\"invalid royalty paymentAddress\")\n\tErrCannotCalculateRoyaltyAmount = errors.New(\"cannot calculate royalty amount\")\n)\n" + }, + { + "name": "grc721_metadata.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// metadataNFT represents an NFT with metadata extensions.\ntype metadataNFT struct {\n\t*basicNFT // Embedded basicNFT struct for basic NFT functionality\n\textensions *avl.Tree // AVL tree for storing metadata extensions\n}\n\n// Ensure that metadataNFT implements the IGRC721MetadataOnchain interface.\nvar _ IGRC721MetadataOnchain = (*metadataNFT)(nil)\n\n// NewNFTWithMetadata creates a new basic NFT with metadata extensions.\nfunc NewNFTWithMetadata(name string, symbol string) *metadataNFT {\n\t// Create a new basic NFT\n\tnft := NewBasicNFT(name, symbol)\n\n\t// Return a metadataNFT with basicNFT embedded and an empty AVL tree for extensions\n\treturn \u0026metadataNFT{\n\t\tbasicNFT: nft,\n\t\textensions: avl.NewTree(),\n\t}\n}\n\n// SetTokenMetadata sets metadata for a given token ID.\nfunc (s *metadataNFT) SetTokenMetadata(tid TokenID, metadata Metadata) error {\n\t// Check if the caller is the owner of the token\n\towner, err := s.basicNFT.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set the metadata for the token ID in the extensions AVL tree\n\ts.extensions.Set(string(tid), metadata)\n\treturn nil\n}\n\n// TokenMetadata retrieves metadata for a given token ID.\nfunc (s *metadataNFT) TokenMetadata(tid TokenID) (Metadata, error) {\n\t// Retrieve metadata from the extensions AVL tree\n\tmetadata, found := s.extensions.Get(string(tid))\n\tif !found {\n\t\treturn Metadata{}, ErrInvalidTokenId\n\t}\n\n\treturn metadata.(Metadata), nil\n}\n\n// mint mints a new token and assigns it to the specified address.\nfunc (s *metadataNFT) mint(to std.Address, tid TokenID) error {\n\t// Check if the address is valid\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\t// Check if the token ID already exists\n\tif s.basicNFT.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ts.basicNFT.beforeTokenTransfer(zeroAddress, to, tid, 1)\n\n\t// Check if the token ID was minted by beforeTokenTransfer\n\tif s.basicNFT.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\t// Increment balance of the recipient address\n\ttoBalance, err := s.basicNFT.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance += 1\n\ts.basicNFT.balances.Set(to.String(), toBalance)\n\n\t// Set owner of the token ID to the recipient address\n\ts.basicNFT.owners.Set(string(tid), to)\n\n\t// Emit transfer event\n\tevent := TransferEvent{zeroAddress, to, tid}\n\temit(\u0026event)\n\n\ts.basicNFT.afterTokenTransfer(zeroAddress, to, tid, 1)\n\n\treturn nil\n}\n" + }, + { + "name": "grc721_metadata_test.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSetMetadata(t *testing.T) {\n\t// Create a new dummy NFT with metadata\n\tdummy := NewNFTWithMetadata(dummyNFTName, dummyNFTSymbol)\n\tif dummy == nil {\n\t\tt.Errorf(\"should not be nil\")\n\t}\n\n\t// Define addresses for testing purposes\n\taddr1 := testutils.TestAddress(\"alice\")\n\taddr2 := testutils.TestAddress(\"bob\")\n\n\t// Define metadata attributes\n\tname := \"test\"\n\tdescription := \"test\"\n\timage := \"test\"\n\timageData := \"test\"\n\texternalURL := \"test\"\n\tattributes := []Trait{}\n\tbackgroundColor := \"test\"\n\tanimationURL := \"test\"\n\tyoutubeURL := \"test\"\n\n\t// Set the original caller to addr1\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\t// Mint a new token for addr1\n\tdummy.mint(addr1, TokenID(\"1\"))\n\n\t// Set metadata for token 1\n\tderr := dummy.SetTokenMetadata(TokenID(\"1\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if there was an error setting metadata\n\tuassert.NoError(t, derr, \"Should not result in error\")\n\n\t// Test case: Invalid token ID\n\terr := dummy.SetTokenMetadata(TokenID(\"3\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if the error returned matches the expected error\n\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\n\t// Set the original caller to addr2\n\tstd.TestSetOrigCaller(addr2) // addr2\n\n\t// Try to set metadata for token 1 from addr2 (should fail)\n\tcerr := dummy.SetTokenMetadata(TokenID(\"1\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if the error returned matches the expected error\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Set the original caller back to addr1\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\t// Retrieve metadata for token 1\n\tdummyMetadata, err := dummy.TokenMetadata(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"Metadata error\")\n\n\t// Check if metadata attributes match expected values\n\tuassert.Equal(t, image, dummyMetadata.Image)\n\tuassert.Equal(t, imageData, dummyMetadata.ImageData)\n\tuassert.Equal(t, externalURL, dummyMetadata.ExternalURL)\n\tuassert.Equal(t, description, dummyMetadata.Description)\n\tuassert.Equal(t, name, dummyMetadata.Name)\n\tuassert.Equal(t, len(attributes), len(dummyMetadata.Attributes))\n\tuassert.Equal(t, backgroundColor, dummyMetadata.BackgroundColor)\n\tuassert.Equal(t, animationURL, dummyMetadata.AnimationURL)\n\tuassert.Equal(t, youtubeURL, dummyMetadata.YoutubeURL)\n}\n" + }, + { + "name": "grc721_royalty.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// royaltyNFT represents a non-fungible token (NFT) with royalty functionality.\ntype royaltyNFT struct {\n\t*metadataNFT // Embedding metadataNFT for NFT functionality\n\ttokenRoyaltyInfo *avl.Tree // AVL tree to store royalty information for each token\n\tmaxRoyaltyPercentage uint64 // maxRoyaltyPercentage represents the maximum royalty percentage that can be charged every sale\n}\n\n// Ensure that royaltyNFT implements the IGRC2981 interface.\nvar _ IGRC2981 = (*royaltyNFT)(nil)\n\n// NewNFTWithRoyalty creates a new royalty NFT with the specified name, symbol, and royalty calculator.\nfunc NewNFTWithRoyalty(name string, symbol string) *royaltyNFT {\n\t// Create a new NFT with metadata\n\tnft := NewNFTWithMetadata(name, symbol)\n\n\treturn \u0026royaltyNFT{\n\t\tmetadataNFT: nft,\n\t\ttokenRoyaltyInfo: avl.NewTree(),\n\t\tmaxRoyaltyPercentage: 100,\n\t}\n}\n\n// SetTokenRoyalty sets the royalty information for a specific token ID.\nfunc (r *royaltyNFT) SetTokenRoyalty(tid TokenID, royaltyInfo RoyaltyInfo) error {\n\t// Validate the payment address\n\tif err := isValidAddress(royaltyInfo.PaymentAddress); err != nil {\n\t\treturn ErrInvalidRoyaltyPaymentAddress\n\t}\n\n\t// Check if royalty percentage exceeds maxRoyaltyPercentage\n\tif royaltyInfo.Percentage \u003e r.maxRoyaltyPercentage {\n\t\treturn ErrInvalidRoyaltyPercentage\n\t}\n\n\t// Check if the caller is the owner of the token\n\towner, err := r.metadataNFT.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set royalty information for the token\n\tr.tokenRoyaltyInfo.Set(string(tid), royaltyInfo)\n\n\treturn nil\n}\n\n// RoyaltyInfo returns the royalty information for the given token ID and sale price.\nfunc (r *royaltyNFT) RoyaltyInfo(tid TokenID, salePrice uint64) (std.Address, uint64, error) {\n\t// Retrieve royalty information for the token\n\tval, found := r.tokenRoyaltyInfo.Get(string(tid))\n\tif !found {\n\t\treturn \"\", 0, ErrInvalidTokenId\n\t}\n\n\troyaltyInfo := val.(RoyaltyInfo)\n\n\t// Calculate royalty amount\n\troyaltyAmount, _ := r.calculateRoyaltyAmount(salePrice, royaltyInfo.Percentage)\n\n\treturn royaltyInfo.PaymentAddress, royaltyAmount, nil\n}\n\nfunc (r *royaltyNFT) calculateRoyaltyAmount(salePrice, percentage uint64) (uint64, error) {\n\troyaltyAmount := (salePrice * percentage) / 100\n\treturn royaltyAmount, nil\n}\n" + }, + { + "name": "grc721_royalty_test.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSetTokenRoyalty(t *testing.T) {\n\tdummy := NewNFTWithRoyalty(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := testutils.TestAddress(\"alice\")\n\taddr2 := testutils.TestAddress(\"bob\")\n\n\tpaymentAddress := testutils.TestAddress(\"john\")\n\tpercentage := uint64(10) // 10%\n\n\tsalePrice := uint64(1000)\n\texpectRoyaltyAmount := uint64(100)\n\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\n\tderr := dummy.SetTokenRoyalty(TokenID(\"1\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.NoError(t, derr, \"Should not result in error\")\n\n\t// Test case: Invalid token ID\n\terr := dummy.SetTokenRoyalty(TokenID(\"3\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, derr, ErrInvalidTokenId)\n\n\tstd.TestSetOrigCaller(addr2) // addr2\n\n\tcerr := dummy.SetTokenRoyalty(TokenID(\"1\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Test case: Invalid payment address\n\taerr := dummy.SetTokenRoyalty(TokenID(\"4\"), RoyaltyInfo{\n\t\tPaymentAddress: std.Address(\"###\"), // invalid address\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, aerr, ErrInvalidRoyaltyPaymentAddress)\n\n\t// Test case: Invalid percentage\n\tperr := dummy.SetTokenRoyalty(TokenID(\"5\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: uint64(200), // over maxRoyaltyPercentage\n\t})\n\tuassert.ErrorIs(t, perr, ErrInvalidRoyaltyPercentage)\n\n\t// Test case: Retrieving Royalty Info\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\tdummyPaymentAddress, dummyRoyaltyAmount, rerr := dummy.RoyaltyInfo(TokenID(\"1\"), salePrice)\n\tuassert.NoError(t, rerr, \"RoyaltyInfo error\")\n\tuassert.Equal(t, paymentAddress, dummyPaymentAddress)\n\tuassert.Equal(t, expectRoyaltyAmount, dummyRoyaltyAmount)\n}\n" + }, + { + "name": "igrc721.gno", + "body": "package grc721\n\nimport \"std\"\n\ntype IGRC721 interface {\n\tBalanceOf(owner std.Address) (uint64, error)\n\tOwnerOf(tid TokenID) (std.Address, error)\n\tSetTokenURI(tid TokenID, tURI TokenURI) (bool, error)\n\tSafeTransferFrom(from, to std.Address, tid TokenID) error\n\tTransferFrom(from, to std.Address, tid TokenID) error\n\tApprove(approved std.Address, tid TokenID) error\n\tSetApprovalForAll(operator std.Address, approved bool) error\n\tGetApproved(tid TokenID) (std.Address, error)\n\tIsApprovedForAll(owner, operator std.Address) bool\n}\n\ntype (\n\tTokenID string\n\tTokenURI string\n)\n\ntype TransferEvent struct {\n\tFrom std.Address\n\tTo std.Address\n\tTokenID TokenID\n}\n\ntype ApprovalEvent struct {\n\tOwner std.Address\n\tApproved std.Address\n\tTokenID TokenID\n}\n\ntype ApprovalForAllEvent struct {\n\tOwner std.Address\n\tOperator std.Address\n\tApproved bool\n}\n" + }, + { + "name": "igrc721_metadata.gno", + "body": "package grc721\n\n// IGRC721CollectionMetadata describes basic information about an NFT collection.\ntype IGRC721CollectionMetadata interface {\n\tName() string // Name returns the name of the collection.\n\tSymbol() string // Symbol returns the symbol of the collection.\n}\n\n// IGRC721Metadata follows the Ethereum standard\ntype IGRC721Metadata interface {\n\tIGRC721CollectionMetadata\n\tTokenURI(tid TokenID) (string, error) // TokenURI returns the URI of a specific token.\n}\n\n// IGRC721Metadata follows the OpenSea metadata standard\ntype IGRC721MetadataOnchain interface {\n\tIGRC721CollectionMetadata\n\tTokenMetadata(tid TokenID) (Metadata, error)\n}\n\ntype Trait struct {\n\tDisplayType string\n\tTraitType string\n\tValue string\n}\n\n// see: https://docs.opensea.io/docs/metadata-standards\ntype Metadata struct {\n\tImage string // URL to the image of the item. Can be any type of image (including SVGs, which will be cached into PNGs by OpenSea), IPFS or Arweave URLs or paths. We recommend using a minimum 3000 x 3000 image.\n\tImageData string // Raw SVG image data, if you want to generate images on the fly (not recommended). Only use this if you're not including the image parameter.\n\tExternalURL string // URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site.\n\tDescription string // Human-readable description of the item. Markdown is supported.\n\tName string // Name of the item.\n\tAttributes []Trait // Attributes for the item, which will show up on the OpenSea page for the item.\n\tBackgroundColor string // Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #\n\tAnimationURL string // URL to a multimedia attachment for the item. Supported file extensions: GLTF, GLB, WEBM, MP4, M4V, OGV, OGG, MP3, WAV, OGA, HTML (for rich experiences and interactive NFTs using JavaScript canvas, WebGL, etc.). Scripts and relative paths within the HTML page are now supported. Access to browser extensions is not supported.\n\tYoutubeURL string // URL to a YouTube video (only used if animation_url is not provided).\n}\n" + }, + { + "name": "igrc721_royalty.gno", + "body": "package grc721\n\nimport \"std\"\n\n// IGRC2981 follows the Ethereum standard\ntype IGRC2981 interface {\n\t// RoyaltyInfo retrieves royalty information for a tokenID and salePrice.\n\t// It returns the payment address, royalty amount, and an error if any.\n\tRoyaltyInfo(tokenID TokenID, salePrice uint64) (std.Address, uint64, error)\n}\n\n// RoyaltyInfo represents royalty information for a token.\ntype RoyaltyInfo struct {\n\tPaymentAddress std.Address // PaymentAddress is the address where royalty payment should be sent.\n\tPercentage uint64 // Percentage is the royalty percentage. It indicates the percentage of royalty to be paid for each sale. For example : Percentage = 10 =\u003e 10%\n}\n" + }, + { + "name": "util.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n)\n\nvar zeroAddress = std.Address(\"\")\n\nfunc isValidAddress(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\treturn nil\n}\n\nfunc emit(event interface{}) {\n\t// TODO: setup a pubsub system here?\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "grc777", + "path": "gno.land/p/demo/grc/grc777", + "files": [ + { + "name": "dummy_test.gno", + "body": "package grc777\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\ntype dummyImpl struct{}\n\n// FIXME: this should fail.\nvar _ IGRC777 = (*dummyImpl)(nil)\n\nfunc TestInterface(t *testing.T) {\n\tvar dummy IGRC777 = \u0026dummyImpl{}\n}\n\nfunc (impl *dummyImpl) GetName() string { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetSymbol() string { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetDecimals() uint { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Granularity() (granularity uint64) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) TotalSupply() (supply uint64) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) BalanceOf(address std.Address) uint64 { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Burn(amount uint64, data []byte) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) AuthorizeOperator(operator std.Address) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) RevokeOperator(operators std.Address) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) DefaultOperators() []std.Address { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Send(recipient std.Address, amount uint64, data []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) IsOperatorFor(operator, tokenHolder std.Address) bool {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorSend(sender, recipient std.Address, amount uint64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorBurn(account std.Address, amount uint64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n" + }, + { + "name": "igrc777.gno", + "body": "package grc777\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/exts\"\n)\n\n// TODO: use big.Int or a custom uint64 instead of uint64\n\ntype IGRC777 interface {\n\texts.TokenMetadata\n\n\t// Returns the smallest part of the token that is not divisible. This\n\t// means all token operations (creation, movement and destruction) must\n\t// have amounts that are a multiple of this number.\n\t//\n\t// For most token contracts, this value will equal 1.\n\tGranularity() (granularity uint64)\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() (supply uint64)\n\n\t// Returns the amount of tokens owned by an account (`owner`).\n\tBalanceOf(address std.Address) uint64\n\n\t// Moves `amount` tokens from the caller's account to `recipient`.\n\t//\n\t// If send or receive hooks are registered for the caller and `recipient`,\n\t// the corresponding functions will be called with `data` and empty\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tSend(recipient std.Address, amount uint64, data []byte)\n\n\t// Destroys `amount` tokens from the caller's account, reducing the\n\t// total supply.\n\t//\n\t// If a send hook is registered for the caller, the corresponding function\n\t// will be called with `data` and empty `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\tBurn(amount uint64, data []byte)\n\n\t// Returns true if an account is an operator of `tokenHolder`.\n\t// Operators can send and burn tokens on behalf of their owners. All\n\t// accounts are their own operator.\n\t//\n\t// See {operatorSend} and {operatorBurn}.\n\tIsOperatorFor(operator, tokenHolder std.Address) bool\n\n\t// Make an account an operator of the caller.\n\t//\n\t// See {isOperatorFor}.\n\t//\n\t// Emits an {AuthorizedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tAuthorizeOperator(operator std.Address)\n\n\t// Revoke an account's operator status for the caller.\n\t//\n\t// See {isOperatorFor} and {defaultOperators}.\n\t//\n\t// Emits a {RevokedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tRevokeOperator(operators std.Address)\n\n\t// Returns the list of default operators. These accounts are operators\n\t// for all token holders, even if {authorizeOperator} was never called on\n\t// them.\n\t//\n\t// This list is immutable, but individual holders may revoke these via\n\t// {revokeOperator}, in which case {isOperatorFor} will return false.\n\tDefaultOperators() []std.Address\n\n\t// Moves `amount` tokens from `sender` to `recipient`. The caller must\n\t// be an operator of `sender`.\n\t//\n\t// If send or receive hooks are registered for `sender` and `recipient`,\n\t// the corresponding functions will be called with `data` and\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - `sender` cannot be the zero address.\n\t// - `sender` must have at least `amount` tokens.\n\t// - the caller must be an operator for `sender`.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tOperatorSend(sender, recipient std.Address, amount uint64, data, operatorData []byte)\n\n\t// Destroys `amount` tokens from `account`, reducing the total supply.\n\t// The caller must be an operator of `account`.\n\t//\n\t// If a send hook is registered for `account`, the corresponding function\n\t// will be called with `data` and `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - `account` cannot be the zero address.\n\t// - `account` must have at least `amount` tokens.\n\t// - the caller must be an operator for `account`.\n\tOperatorBurn(account std.Address, amount uint64, data, operatorData []byte)\n}\n\n// Emitted when `amount` tokens are created by `operator` and assigned to `to`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype MintedEvent struct {\n\tOperator std.Address\n\tTo std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` destroys `amount` tokens from `account`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype BurnedEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` is made operator for `tokenHolder`\ntype AuthorizedOperatorEvent struct {\n\tOperator std.Address\n\tTokenHolder std.Address\n}\n\n// Emitted when `operator` is revoked its operator status for `tokenHolder`.\ntype RevokedOperatorEvent struct {\n\tOperator std.Address\n\tTokenHolder std.Address\n}\n\ntype SentEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "rat", + "path": "gno.land/p/demo/rat", + "files": [ + { + "name": "maths.gno", + "body": "package rat\n\nconst (\n\tintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63) // 32 or 64\n\n\tMaxInt = 1\u003c\u003c(intSize-1) - 1\n\tMinInt = -1 \u003c\u003c (intSize - 1)\n\tMaxInt8 = 1\u003c\u003c7 - 1\n\tMinInt8 = -1 \u003c\u003c 7\n\tMaxInt16 = 1\u003c\u003c15 - 1\n\tMinInt16 = -1 \u003c\u003c 15\n\tMaxInt32 = 1\u003c\u003c31 - 1\n\tMinInt32 = -1 \u003c\u003c 31\n\tMaxInt64 = 1\u003c\u003c63 - 1\n\tMinInt64 = -1 \u003c\u003c 63\n\tMaxUint = 1\u003c\u003cintSize - 1\n\tMaxUint8 = 1\u003c\u003c8 - 1\n\tMaxUint16 = 1\u003c\u003c16 - 1\n\tMaxUint32 = 1\u003c\u003c32 - 1\n\tMaxUint64 = 1\u003c\u003c64 - 1\n)\n" + }, + { + "name": "rat.gno", + "body": "package rat\n\n//----------------------------------------\n// Rat fractions\n\n// represents a fraction.\ntype Rat struct {\n\tX int32\n\tY int32 // must be positive\n}\n\nfunc NewRat(x, y int32) Rat {\n\tif y \u003c= 0 {\n\t\tpanic(\"invalid std.Rat denominator\")\n\t}\n\treturn Rat{X: x, Y: y}\n}\n\nfunc (r1 Rat) IsValid() bool {\n\tif r1.Y \u003c= 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (r1 Rat) Cmp(r2 Rat) int {\n\tif !r1.IsValid() {\n\t\tpanic(\"invalid std.Rat left operand\")\n\t}\n\tif !r2.IsValid() {\n\t\tpanic(\"invalid std.Rat right operand\")\n\t}\n\tvar p1, p2 int64\n\tp1 = int64(r1.X) * int64(r2.Y)\n\tp2 = int64(r1.Y) * int64(r2.X)\n\tif p1 \u003c p2 {\n\t\treturn -1\n\t} else if p1 == p2 {\n\t\treturn 0\n\t} else {\n\t\treturn 1\n\t}\n}\n\n//func (r1 Rat) Plus(r2 Rat) Rat {\n// XXX\n//}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "txlink", + "path": "gno.land/p/moul/txlink", + "files": [ + { + "name": "txlink.gno", + "body": "// Package txlink provides utilities for creating transaction-related links\n// compatible with Gnoweb, Gnobro, and other clients within the Gno ecosystem.\n//\n// This package is optimized for generating lightweight transaction links with\n// flexible arguments, allowing users to build dynamic links that integrate\n// seamlessly with various Gno clients.\n//\n// The primary function, URL, is designed to produce markdown links for\n// transaction functions in the current \"relative realm\". By specifying a custom\n// Realm, you can generate links that either use the current realm path or a\n// fully qualified path for another realm.\n//\n// This package is a streamlined alternative to helplink, providing similar\n// functionality for transaction links without the full feature set of helplink.\npackage txlink\n\nimport (\n\t\"std\"\n\t\"strings\"\n)\n\nconst chainDomain = \"gno.land\" // XXX: std.ChainDomain (#2911)\n\n// URL returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc URL(fn string, args ...string) string {\n\treturn Realm(\"\").URL(fn, args...)\n}\n\n// Realm represents a specific realm for generating tx links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\tcurPath := std.CurrentRealm().PkgPath()\n\t\treturn strings.TrimPrefix(curPath, chainDomain)\n\t}\n\n\t// local realm -\u003e /realm\n\trealm := string(r)\n\tif strings.Contains(realm, chainDomain) {\n\t\treturn strings.TrimPrefix(realm, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + string(r)\n}\n\n// URL returns a URL for the specified function with optional key-value\n// arguments.\nfunc (r Realm) URL(fn string, args ...string) string {\n\t// Start with the base query\n\turl := r.prefix() + \"$help\u0026func=\" + fn\n\n\t// Check if args length is even\n\tif len(args)%2 != 0 {\n\t\t// If not even, we can choose to handle the error here.\n\t\t// For example, we can just return the URL without appending\n\t\t// more args.\n\t\treturn url\n\t}\n\n\t// Append key-value pairs to the URL\n\tfor i := 0; i \u003c len(args); i += 2 {\n\t\tkey := args[i]\n\t\tvalue := args[i+1]\n\t\t// XXX: escape keys and args\n\t\turl += \"\u0026\" + key + \"=\" + value\n\t}\n\n\treturn url\n}\n" + }, + { + "name": "txlink_test.gno", + "body": "package txlink\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestURL(t *testing.T) {\n\ttests := []struct {\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"$help\u0026func=oddArgsFunc\", \"\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"https://gno.world/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tgot := tt.realm.URL(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "users", + "path": "gno.land/p/demo/users", + "files": [ + { + "name": "types.gno", + "body": "package users\n\ntype AddressOrName string\n\nfunc (aon AddressOrName) IsName() bool {\n\treturn aon != \"\" \u0026\u0026 aon[0] == '@'\n}\n\nfunc (aon AddressOrName) GetName() (string, bool) {\n\tif len(aon) \u003e= 2 \u0026\u0026 aon[0] == '@' {\n\t\treturn string(aon[1:]), true\n\t}\n\treturn \"\", false\n}\n" + }, + { + "name": "users.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\t\"strconv\"\n)\n\n//----------------------------------------\n// Types\n\ntype User struct {\n\tAddress std.Address\n\tName string\n\tProfile string\n\tNumber int\n\tInvites int\n\tInviter std.Address\n}\n\nfunc (u *User) Render() string {\n\tstr := \"## user \" + u.Name + \"\\n\" +\n\t\t\"\\n\" +\n\t\t\" * address = \" + string(u.Address) + \"\\n\" +\n\t\t\" * \" + strconv.Itoa(u.Invites) + \" invites\\n\"\n\tif u.Inviter != \"\" {\n\t\tstr = str + \" * invited by \" + string(u.Inviter) + \"\\n\"\n\t}\n\tstr = str + \"\\n\" +\n\t\tu.Profile + \"\\n\"\n\treturn str\n}\n" + }, + { + "name": "users_test.gno", + "body": "package users\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "users", + "path": "gno.land/r/demo/users", + "files": [ + { + "name": "preregister.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/users\"\n)\n\n// pre-restricted names\nvar preRestrictedNames = []string{\n\t\"bitcoin\", \"cosmos\", \"newtendermint\", \"ethereum\",\n}\n\n// pre-registered users\nvar preRegisteredUsers = []struct {\n\tName string\n\tAddress std.Address\n}{\n\t// system name\n\t{\"archives\", \"g1xlnyjrnf03ju82v0f98ruhpgnquk28knmjfe5k\"}, // -\u003e @r_archives\n\t{\"demo\", \"g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n\"}, // -\u003e @r_demo\n\t{\"gno\", \"g19602kd9tfxrfd60sgreadt9zvdyyuudcyxsz8a\"}, // -\u003e @r_gno\n\t{\"gnoland\", \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"}, // -\u003e @r_gnoland\n\t{\"gnolang\", \"g1yjlnm3z2630gg5mryjd79907e0zx658wxs9hnd\"}, // -\u003e @r_gnolang\n\t{\"gov\", \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"}, // -\u003e @r_gov\n\t{\"nt\", \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"}, // -\u003e @r_nt\n\t{\"sys\", \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"}, // -\u003e @r_sys\n\t{\"x\", \"g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz\"}, // -\u003e @r_x\n\n\t// test1 user\n\t{\"test1\", \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"}, // -\u003e @test1\n\n\t// Onbloc\n\t{\"gnoswap\", \"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c\"}, // -\u003e @r_gnoswap\n\t{\"onbloc\", \"g12vx7dn3dqq89mz550zwunvg4qw6epq73d9csay\"}, // -\u003e @r_onbloc\n\n\t// Dragos\n\t{\"flippando\", \"g1z82x8mxa0pz5s9u7csy6zya4x0ut9uw6p7d8dk\"}, // -\u003e @r_flippando\n\t{\"zentasktic\", \"g1paxgmwy2wzhx0l6qvav2p8thvphc5c030xz35c\"}, // -\u003e @r_zentasktic\n}\n\nfunc init() {\n\t// add pre-registered users\n\tfor _, res := range preRegisteredUsers {\n\t\t// assert not already registered.\n\t\t_, ok := name2User.Get(res.Name)\n\t\tif ok {\n\t\t\tpanic(\"name already registered\")\n\t\t}\n\n\t\t_, ok = addr2User.Get(res.Address.String())\n\t\tif ok {\n\t\t\tpanic(\"address already registered\")\n\t\t}\n\n\t\tcounter++\n\t\tuser := \u0026users.User{\n\t\t\tAddress: res.Address,\n\t\t\tName: res.Name,\n\t\t\tProfile: \"\",\n\t\t\tNumber: counter,\n\t\t\tInvites: int(0),\n\t\t\tInviter: admin,\n\t\t}\n\t\tname2User.Set(res.Name, user)\n\t\taddr2User.Set(res.Address.String(), user)\n\t}\n\n\t// add pre-restricted names\n\tfor _, name := range preRestrictedNames {\n\t\tif _, ok := name2User.Get(name); ok {\n\t\t\tpanic(\"name already registered\")\n\t\t}\n\n\t\trestricted.Set(name, true)\n\t}\n}\n" + }, + { + "name": "users.gno", + "body": "package users\n\nimport (\n\t\"regexp\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/avlhelpers\"\n\t\"gno.land/p/demo/users\"\n)\n\n//----------------------------------------\n// State\n\nvar (\n\tadmin std.Address = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\" // @moul\n\n\trestricted avl.Tree // Name -\u003e true - restricted name\n\tname2User avl.Tree // Name -\u003e *users.User\n\taddr2User avl.Tree // std.Address -\u003e *users.User\n\tinvites avl.Tree // string(inviter+\":\"+invited) -\u003e true\n\tcounter int // user id counter\n\tminFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register.\n\tmaxFeeMult int64 = 10 // maximum multiples of minFee accepted.\n)\n\n//----------------------------------------\n// Top-level functions\n\nfunc Register(inviter std.Address, name string, profile string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert invited or paid.\n\tcaller := std.GetCallerAt(2)\n\tif caller != std.GetOrigCaller() {\n\t\tpanic(\"should not happen\") // because std.AssertOrigCall().\n\t}\n\n\tsentCoins := std.GetOrigSend()\n\tminCoin := std.NewCoin(\"ugnot\", minFee)\n\n\tif inviter == \"\" {\n\t\t// banker := std.GetBanker(std.BankerTypeOrigSend)\n\t\tif len(sentCoins) == 1 \u0026\u0026 sentCoins[0].IsGTE(minCoin) {\n\t\t\tif sentCoins[0].Amount \u003e minFee*maxFeeMult {\n\t\t\t\tpanic(\"payment must not be greater than \" + strconv.Itoa(int(minFee*maxFeeMult)))\n\t\t\t} else {\n\t\t\t\t// ok\n\t\t\t}\n\t\t} else {\n\t\t\tpanic(\"payment must not be less than \" + strconv.Itoa(int(minFee)))\n\t\t}\n\t} else {\n\t\tinvitekey := inviter.String() + \":\" + caller.String()\n\t\t_, ok := invites.Get(invitekey)\n\t\tif !ok {\n\t\t\tpanic(\"invalid invitation\")\n\t\t}\n\t\tinvites.Remove(invitekey)\n\t}\n\n\t// assert not already registered.\n\t_, ok := name2User.Get(name)\n\tif ok {\n\t\tpanic(\"name already registered: \" + name)\n\t}\n\t_, ok = addr2User.Get(caller.String())\n\tif ok {\n\t\tpanic(\"address already registered: \" + caller.String())\n\t}\n\n\tisInviterAdmin := inviter == admin\n\n\t// check for restricted name\n\tif _, isRestricted := restricted.Get(name); isRestricted {\n\t\t// only address invite by the admin can register restricted name\n\t\tif !isInviterAdmin {\n\t\t\tpanic(\"restricted name: \" + name)\n\t\t}\n\n\t\trestricted.Remove(name)\n\t}\n\n\t// assert name is valid.\n\t// admin inviter can bypass name restriction\n\tif !isInviterAdmin \u0026\u0026 !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name + \" (must be at least 6 characters, lowercase alphanumeric with underscore)\")\n\t}\n\n\t// remainder of fees go toward invites.\n\tinvites := int(0)\n\tif len(sentCoins) == 1 {\n\t\tif sentCoins[0].Denom == \"ugnot\" \u0026\u0026 sentCoins[0].Amount \u003e= minFee {\n\t\t\tinvites = int(sentCoins[0].Amount / minFee)\n\t\t\tif inviter == \"\" \u0026\u0026 invites \u003e 0 {\n\t\t\t\tinvites -= 1\n\t\t\t}\n\t\t}\n\t}\n\t// register.\n\tcounter++\n\tuser := \u0026users.User{\n\t\tAddress: caller,\n\t\tName: name,\n\t\tProfile: profile,\n\t\tNumber: counter,\n\t\tInvites: invites,\n\t\tInviter: inviter,\n\t}\n\tname2User.Set(name, user)\n\taddr2User.Set(caller.String(), user)\n}\n\nfunc Invite(invitee string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// get caller/inviter.\n\tcaller := std.GetCallerAt(2)\n\tif caller != std.GetOrigCaller() {\n\t\tpanic(\"should not happen\") // because std.AssertOrigCall().\n\t}\n\tlines := strings.Split(invitee, \"\\n\")\n\tif caller == admin {\n\t\t// nothing to do, all good\n\t} else {\n\t\t// ensure has invites.\n\t\tuserI, ok := addr2User.Get(caller.String())\n\t\tif !ok {\n\t\t\tpanic(\"user unknown\")\n\t\t}\n\t\tuser := userI.(*users.User)\n\t\tif user.Invites \u003c= 0 {\n\t\t\tpanic(\"user has no invite tokens\")\n\t\t}\n\t\tuser.Invites -= len(lines)\n\t\tif user.Invites \u003c 0 {\n\t\t\tpanic(\"user has insufficient invite tokens\")\n\t\t}\n\t}\n\t// for each line...\n\tfor _, line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue // file bodies have a trailing newline.\n\t\t} else if strings.HasPrefix(line, `//`) {\n\t\t\tcontinue // comment\n\t\t}\n\t\t// record invite.\n\t\tinvitekey := string(caller) + \":\" + string(line)\n\t\tinvites.Set(invitekey, true)\n\t}\n}\n\nfunc GrantInvites(invites string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert admin.\n\tcaller := std.GetCallerAt(2)\n\tif caller != std.GetOrigCaller() {\n\t\tpanic(\"should not happen\") // because std.AssertOrigCall().\n\t}\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\t// for each line...\n\tlines := strings.Split(invites, \"\\n\")\n\tfor _, line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue // file bodies have a trailing newline.\n\t\t} else if strings.HasPrefix(line, `//`) {\n\t\t\tcontinue // comment\n\t\t}\n\t\t// parse name and invites.\n\t\tvar name string\n\t\tvar invites int\n\t\tparts := strings.Split(line, \":\")\n\t\tif len(parts) == 1 { // short for :1.\n\t\t\tname = parts[0]\n\t\t\tinvites = 1\n\t\t} else if len(parts) == 2 {\n\t\t\tname = parts[0]\n\t\t\tinvites_, err := strconv.Atoi(parts[1])\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tinvites = int(invites_)\n\t\t} else {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t\t// give invites.\n\t\tuserI, ok := name2User.Get(name)\n\t\tif !ok {\n\t\t\t// maybe address.\n\t\t\tuserI, ok = addr2User.Get(name)\n\t\t\tif !ok {\n\t\t\t\tpanic(\"invalid user \" + name)\n\t\t\t}\n\t\t}\n\t\tuser := userI.(*users.User)\n\t\tuser.Invites += invites\n\t}\n}\n\n// Any leftover fees go toward invitations.\nfunc SetMinFee(newMinFee int64) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert admin caller.\n\tcaller := std.GetCallerAt(2)\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\t// update global variables.\n\tminFee = newMinFee\n}\n\n// This helps prevent fat finger accidents.\nfunc SetMaxFeeMultiple(newMaxFeeMult int64) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert admin caller.\n\tcaller := std.GetCallerAt(2)\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\t// update global variables.\n\tmaxFeeMult = newMaxFeeMult\n}\n\n//----------------------------------------\n// Exposed public functions\n\nfunc GetUserByName(name string) *users.User {\n\tuserI, ok := name2User.Get(name)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn userI.(*users.User)\n}\n\nfunc GetUserByAddress(addr std.Address) *users.User {\n\tuserI, ok := addr2User.Get(addr.String())\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn userI.(*users.User)\n}\n\n// unlike GetUserByName, input must be \"@\" prefixed for names.\nfunc GetUserByAddressOrName(input users.AddressOrName) *users.User {\n\tname, isName := input.GetName()\n\tif isName {\n\t\treturn GetUserByName(name)\n\t}\n\treturn GetUserByAddress(std.Address(input))\n}\n\n// Get a list of user names starting from the given prefix. Limit the\n// number of results to maxResults. (This can be used for a name search tool.)\nfunc ListUsersByPrefix(prefix string, maxResults int) []string {\n\treturn avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults)\n}\n\nfunc Resolve(input users.AddressOrName) std.Address {\n\tname, isName := input.GetName()\n\tif !isName {\n\t\treturn std.Address(input) // TODO check validity\n\t}\n\n\tuser := GetUserByName(name)\n\treturn user.Address\n}\n\n// Add restricted name to the list\nfunc AdminAddRestrictedName(name string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// get caller\n\tcaller := std.GetOrigCaller()\n\t// assert admin\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\n\tif user := GetUserByName(name); user != nil {\n\t\tpanic(\"already registered name\")\n\t}\n\n\t// register restricted name\n\n\trestricted.Set(name, true)\n}\n\n//----------------------------------------\n// Constants\n\n// NOTE: name length must be clearly distinguishable from a bech32 address.\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`)\n\n//----------------------------------------\n// Render main page\n\nfunc Render(fullPath string) string {\n\tpath, _ := splitPathAndQuery(fullPath)\n\tif path == \"\" {\n\t\treturn renderHome(fullPath)\n\t} else if len(path) \u003e= 38 { // 39? 40?\n\t\tif path[:2] != \"g1\" {\n\t\t\treturn \"invalid address \" + path\n\t\t}\n\t\tuser := GetUserByAddress(std.Address(path))\n\t\tif user == nil {\n\t\t\t// TODO: display basic information about account.\n\t\t\treturn \"unknown address \" + path\n\t\t}\n\t\treturn user.Render()\n\t} else {\n\t\tuser := GetUserByName(path)\n\t\tif user == nil {\n\t\t\treturn \"unknown username \" + path\n\t\t}\n\t\treturn user.Render()\n\t}\n}\n\nfunc renderHome(path string) string {\n\tdoc := \"\"\n\n\tpage := pager.NewPager(\u0026name2User, 50).MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\tuser := item.Value.(*users.User)\n\t\tdoc += \" * [\" + user.Name + \"](/r/demo/users:\" + user.Name + \")\\n\"\n\t}\n\tdoc += \"\\n\"\n\tdoc += page.Selector()\n\treturn doc\n}\n\nfunc splitPathAndQuery(fullPath string) (string, string) {\n\tparts := strings.SplitN(fullPath, \"?\", 2)\n\tpath := parts[0]\n\tqueryString := \"\"\n\tif len(parts) \u003e 1 {\n\t\tqueryString = \"?\" + parts[1]\n\t}\n\treturn path, queryString\n}\n" + }, + { + "name": "users_test.gno", + "body": "package users\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPreRegisteredTest1(t *testing.T) {\n\tnames := ListUsersByPrefix(\"test1\", 1)\n\tuassert.Equal(t, len(names), 1)\n\tuassert.Equal(t, names[0], \"test1\")\n}\n" + }, + { + "name": "z_0_b_filetest.gno", + "body": "package main\n\n// SEND: 19900000ugnot\n\nimport (\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// payment must not be less than 20000000\n" + }, + { + "name": "z_0_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tstd.TestSetOrigSend(std.Coins{std.NewCoin(\"dontcare\", 1)}, nil)\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// incompatible coin denominations: dontcare, ugnot\n" + }, + { + "name": "z_10_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/users_test\npackage users_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc init() {\n\tcaller := std.GetOrigCaller() // main\n\ttest2 := testutils.TestAddress(\"test2\")\n\t// as admin, invite gnouser and test2\n\tstd.TestSetOrigCaller(admin)\n\tusers.Invite(caller.String() + \"\\n\" + test2.String())\n\t// register as caller\n\tstd.TestSetOrigCaller(caller)\n\tusers.Register(admin, \"gnouser\", \"my profile\")\n}\n\nfunc main() {\n\t// register as test2\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(admin, \"test222\", \"my profile 2\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_11_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tstd.TestSetOrigCaller(admin)\n\tusers.AdminAddRestrictedName(\"superrestricted\")\n\n\t// test restricted name\n\tstd.TestSetOrigCaller(caller)\n\tusers.Register(\"\", \"superrestricted\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// restricted name: superrestricted\n" + }, + { + "name": "z_11b_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tstd.TestSetOrigCaller(admin)\n\t// add restricted name\n\tusers.AdminAddRestrictedName(\"superrestricted\")\n\t// grant invite to caller\n\tusers.Invite(caller.String())\n\t// set back caller\n\tstd.TestSetOrigCaller(caller)\n\t// register restricted name with admin invite\n\tusers.Register(admin, \"superrestricted\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_12_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"alicia\", \"my profile\")\n\n\t{\n\t\t// Normal usage\n\t\tnames := users.ListUsersByPrefix(\"a\", 1)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t\tprintln(\"name: \" + names[0])\n\t}\n\n\t{\n\t\t// Empty prefix: match all\n\t\tnames := users.ListUsersByPrefix(\"\", 1)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t\tprintln(\"name: \" + names[0])\n\t}\n\n\t{\n\t\t// The prefix is before \"alicia\"\n\t\tnames := users.ListUsersByPrefix(\"alich\", 1)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t}\n\n\t{\n\t\t// The prefix is after the last name\n\t\tnames := users.ListUsersByPrefix(\"y\", 10)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t}\n\n\t// More tests are in p/demo/avlhelpers\n}\n\n// Output:\n// # names: 1\n// name: alicia\n// # names: 1\n// name: alicia\n// # names: 0\n// # names: 0\n" + }, + { + "name": "z_1_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_2_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_3_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_4_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\ttest2 := testutils.TestAddress(\"test2\")\n\tusers.Invite(test1.String())\n\t// switch to test2 (not test1)\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// invalid invitation\n" + }, + { + "name": "z_5_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(users.Render(\"\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"?page=2\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"gnouser\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"satoshi\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"badname\"))\n}\n\n// Output:\n// * [archives](/r/demo/users:archives)\n// * [demo](/r/demo/users:demo)\n// * [gno](/r/demo/users:gno)\n// * [gnoland](/r/demo/users:gnoland)\n// * [gnolang](/r/demo/users:gnolang)\n// * [gnouser](/r/demo/users:gnouser)\n// * [gov](/r/demo/users:gov)\n// * [nt](/r/demo/users:nt)\n// * [satoshi](/r/demo/users:satoshi)\n// * [sys](/r/demo/users:sys)\n// * [test1](/r/demo/users:test1)\n// * [x](/r/demo/users:x)\n//\n//\n// ========================================\n//\n//\n// ========================================\n// ## user gnouser\n//\n// * address = g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// * 9 invites\n//\n// my profile\n//\n// ========================================\n// ## user satoshi\n//\n// * address = g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7\n// * 0 invites\n// * invited by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// my other profile\n//\n// ========================================\n// unknown username badname\n" + }, + { + "name": "z_6_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller()\n\t// as admin, grant invites to unregistered user.\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Error:\n// invalid user g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n" + }, + { + "name": "z_7_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\t// as admin, grant invites to gnouser(again) and satoshi.\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\\n\" + test1.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_7b_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\\n\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\t// as admin, grant invites to gnouser(again) and satoshi.\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\\n\" + test1.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + }, + { + "name": "z_8_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\t// as admin, grant invites to gnouser(again) and nonexistent user.\n\tstd.TestSetOrigCaller(admin)\n\ttest2 := testutils.TestAddress(\"test2\")\n\tusers.GrantInvites(caller.String() + \":1\\n\" + test2.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Error:\n// invalid user g1w3jhxapjta047h6lta047h6lta047h6laqcyu4\n" + }, + { + "name": "z_9_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\ttest2 := testutils.TestAddress(\"test2\")\n\t// as admin, invite gnouser and test2\n\tstd.TestSetOrigCaller(admin)\n\tusers.Invite(caller.String() + \"\\n\" + test2.String())\n\t// register as caller\n\tstd.TestSetOrigCaller(caller)\n\tusers.Register(admin, \"gnouser\", \"my profile\")\n\t// register as test2\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(admin, \"test222\", \"my profile 2\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "boards", + "path": "gno.land/r/demo/boards", + "files": [ + { + "name": "README.md", + "body": "This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `-remote localhost:26657` here, that flag can be replaced\nwith `-remote gno.land:26657` if you have $GNOT on the testnet.\n(To use the testnet, also replace `-chainid dev` with `-chainid portal-loop` .)\n\n### Build `gnokey` (and other tools).\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd gno/gno.land\nmake build\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add -recover KEYNAME\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\nTake note of your `addr` which looks something like `g17sphqax3kasjptdkmuqvn740u8dhtx4kxl6ljf` .\nYou will use this as your `ACCOUNT_ADDR`.\n\n## Interact with the blockchain.\n\n### Add $GNOT for your account.\n\nBefore starting the `gnoland` node for the first time, your new account can be given $GNOT in the node genesis.\nEdit the file `gno.land/genesis/genesis_balances.txt` and add the following line (simlar to the others), using\nyour `ACCOUNT_ADDR` and `KEYNAME`\n\n`ACCOUNT_ADDR=10000000000ugnot # @KEYNAME`\n\n### Alternative: Run a faucet to add $GNOT.\n\nInstead of editing `gno.land/genesis/genesis_balances.txt`, a more general solution (with more steps)\nis to run a local \"faucet\" and use the web browser to add $GNOT. (This can be done at any time.)\nSee this page: https://github.com/gnolang/gno/blob/master/contribs/gnofaucet/README.md\n\n\n### Start the `gnoland` node.\n\n```bash\n./build/gnoland start\n```\n\nNOTE: The node already has the \"boards\" realm.\n\nLeave this running in the terminal. In a new terminal, cd to the same folder `gno/gno.land` .\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR -remote localhost:26657\n```\n\n### Register a board username with a smart contract call.\n\nThe `USERNAME` for posting can different than your `KEYNAME`. It is internally linked to your `ACCOUNT_ADDR`. It must be at least 6 characters, lowercase alphanumeric with underscore.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/users\" -func \"Register\" -args \"\" -args \"USERNAME\" -args \"Profile description\" -gas-fee \"10000000ugnot\" -gas-wanted \"2000000\" -send \"200000000ugnot\" -broadcast -chainid dev -remote 127.0.0.1:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/users$help\u0026func=Register\n\n### Create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/boards\" -func \"CreateBoard\" -args \"BOARDNAME\" -gas-fee \"1000000ugnot\" -gas-wanted \"10000000\" -broadcast -chainid dev -remote localhost:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" -data 'gno.land/r/demo/boards.GetBoardIDFromName(\"BOARDNAME\")' -remote localhost:26657\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/boards\" -func \"CreateThread\" -args BOARD_ID -args \"Hello gno.land\" -args \"Text of the post\" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/boards\" -func \"CreateReply\" -args BOARD_ID -args \"1\" -args \"1\" -args \"Nice to meet you too.\" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" -data \"gno.land/r/demo/boards:BOARDNAME/1\" -remote localhost:26657\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" -data \"gno.land/r/demo/boards:gnolang\"\n```\n## View the board in the browser.\n\n### Start the web server.\n\n```bash\n./build/gnoweb\n```\n\nThis should print something like `Running on http://127.0.0.1:8888` . Leave this running in the terminal.\n\n### View in the browser\n\nIn your browser, navigate to the printed address http://127.0.0.1:8888 .\nTo see you post, click on the package `/r/demo/boards` .\n" + }, + { + "name": "board.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/txlink\"\n)\n\n//----------------------------------------\n// Board\n\ntype BoardID uint64\n\nfunc (bid BoardID) String() string {\n\treturn strconv.Itoa(int(bid))\n}\n\ntype Board struct {\n\tid BoardID // only set for public boards.\n\turl string\n\tname string\n\tcreator std.Address\n\tthreads avl.Tree // Post.id -\u003e *Post\n\tpostsCtr uint64 // increments Post.id\n\tcreatedAt time.Time\n\tdeleted avl.Tree // TODO reserved for fast-delete.\n}\n\nfunc newBoard(id BoardID, url string, name string, creator std.Address) *Board {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\texists := gBoardsByName.Has(name)\n\tif exists {\n\t\tpanic(\"board already exists\")\n\t}\n\treturn \u0026Board{\n\t\tid: id,\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\tthreads: avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t\tdeleted: avl.Tree{},\n\t}\n}\n\n/* TODO support this once we figure out how to ensure URL correctness.\n// A private board is not tracked by gBoards*,\n// but must be persisted by the caller's realm.\n// Private boards have 0 id and does not ping\n// back the remote board on reposts.\nfunc NewPrivateBoard(url string, name string, creator std.Address) *Board {\n\treturn newBoard(0, url, name, creator)\n}\n*/\n\nfunc (board *Board) IsPrivate() bool {\n\treturn board.id == 0\n}\n\nfunc (board *Board) GetThread(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpostI, exists := board.threads.Get(pidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn postI.(*Post)\n}\n\nfunc (board *Board) AddThread(creator std.Address, title string, body string) *Post {\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\tthread := newPost(board, pid, creator, title, body, pid, 0, 0)\n\tboard.threads.Set(pidkey, thread)\n\treturn thread\n}\n\n// NOTE: this can be potentially very expensive for threads with many replies.\n// TODO: implement optional fast-delete where thread is simply moved.\nfunc (board *Board) DeleteThread(pid PostID) {\n\tpidkey := postIDKey(pid)\n\t_, removed := board.threads.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"thread does not exist with id \" + pid.String())\n\t}\n}\n\nfunc (board *Board) HasPermission(addr std.Address, perm Permission) bool {\n\tif board.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// Renders the board for display suitable as plaintext in\n// console. This is suitable for demonstration or tests,\n// but not for prod.\nfunc (board *Board) RenderBoard() string {\n\tstr := \"\"\n\tstr += \"\\\\[[post](\" + board.GetPostFormURL() + \")]\\n\\n\"\n\tif board.threads.Size() \u003e 0 {\n\t\tboard.threads.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tif str != \"\" {\n\t\t\t\tstr += \"----------------------------------------\\n\"\n\t\t\t}\n\t\t\tstr += value.(*Post).RenderSummary() + \"\\n\"\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\nfunc (board *Board) incGetPostID() PostID {\n\tboard.postsCtr++\n\treturn PostID(board.postsCtr)\n}\n\nfunc (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string {\n\tif replyID == 0 {\n\t\treturn board.url + \"/\" + threadID.String()\n\t} else {\n\t\treturn board.url + \"/\" + threadID.String() + \"/\" + replyID.String()\n\t}\n}\n\nfunc (board *Board) GetPostFormURL() string {\n\treturn txlink.URL(\"CreateThread\", \"bid\", board.id.String())\n}\n" + }, + { + "name": "boards.gno", + "body": "package boards\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgBoards avl.Tree // id -\u003e *Board\n\tgBoardsCtr int // increments Board.id\n\tgBoardsByName avl.Tree // name -\u003e *Board\n\tgDefaultAnonFee = 100000000 // minimum fee required if anonymous\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n" + }, + { + "name": "misc.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/demo/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getBoard(bid BoardID) *Board {\n\tbidkey := boardIDKey(bid)\n\tboard_, exists := gBoards.Get(bidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\tboard := board_.(*Board)\n\treturn board\n}\n\nfunc incGetBoardID() BoardID {\n\tgBoardsCtr++\n\treturn BoardID(gBoardsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\" \", length-len(str)) + str\n\t}\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc boardIDKey(bid BoardID) string {\n\treturn padZero(uint64(bid), 10)\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/r/demo/users:\" + addr.String() + \")\"\n\t} else {\n\t\treturn \"[@\" + user.Name + \"](/r/demo/users:\" + user.Name + \")\"\n\t}\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t}\n\treturn user.Name\n}\n" + }, + { + "name": "post.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/txlink\"\n)\n\n//----------------------------------------\n// Post\n\n// NOTE: a PostID is relative to the board.\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\n// A Post is a \"thread\" or a \"reply\" depending on context.\n// A thread is a Post of a Board that holds other replies.\ntype Post struct {\n\tboard *Board\n\tid PostID\n\tcreator std.Address\n\ttitle string // optional\n\tbody string\n\treplies avl.Tree // Post.id -\u003e *Post\n\trepliesAll avl.Tree // Post.id -\u003e *Post (all replies, for top-level posts)\n\treposts avl.Tree // Board.id -\u003e Post.id\n\tthreadID PostID // original Post.id\n\tparentID PostID // parent Post.id (if reply or repost)\n\trepostBoard BoardID // original Board.id (if repost)\n\tcreatedAt time.Time\n\tupdatedAt time.Time\n}\n\nfunc newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {\n\treturn \u0026Post{\n\t\tboard: board,\n\t\tid: id,\n\t\tcreator: creator,\n\t\ttitle: title,\n\t\tbody: body,\n\t\treplies: avl.Tree{},\n\t\trepliesAll: avl.Tree{},\n\t\treposts: avl.Tree{},\n\t\tthreadID: threadID,\n\t\tparentID: parentID,\n\t\trepostBoard: repostBoard,\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (post *Post) IsThread() bool {\n\treturn post.parentID == 0\n}\n\nfunc (post *Post) GetPostID() PostID {\n\treturn post.id\n}\n\nfunc (post *Post) AddReply(creator std.Address, body string) *Post {\n\tboard := post.board\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\treply := newPost(board, pid, creator, \"\", body, post.threadID, post.id, 0)\n\tpost.replies.Set(pidkey, reply)\n\tif post.threadID == post.id {\n\t\tpost.repliesAll.Set(pidkey, reply)\n\t} else {\n\t\tthread := board.GetThread(post.threadID)\n\t\tthread.repliesAll.Set(pidkey, reply)\n\t}\n\treturn reply\n}\n\nfunc (post *Post) Update(title string, body string) {\n\tpost.title = title\n\tpost.body = body\n\tpost.updatedAt = time.Now()\n}\n\nfunc (thread *Post) GetReply(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\treplyI, ok := thread.repliesAll.Get(pidkey)\n\tif !ok {\n\t\treturn nil\n\t} else {\n\t\treturn replyI.(*Post)\n\t}\n}\n\nfunc (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post {\n\tif !post.IsThread() {\n\t\tpanic(\"cannot repost non-thread post\")\n\t}\n\tpid := dst.incGetPostID()\n\tpidkey := postIDKey(pid)\n\trepost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)\n\tdst.threads.Set(pidkey, repost)\n\tif !dst.IsPrivate() {\n\t\tbidkey := boardIDKey(dst.id)\n\t\tpost.reposts.Set(bidkey, pid)\n\t}\n\treturn repost\n}\n\nfunc (thread *Post) DeletePost(pid PostID) {\n\tif thread.id == pid {\n\t\tpanic(\"should not happen\")\n\t}\n\tpidkey := postIDKey(pid)\n\tpostI, removed := thread.repliesAll.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"post not found in thread\")\n\t}\n\tpost := postI.(*Post)\n\tif post.parentID != thread.id {\n\t\tparent := thread.GetReply(post.parentID)\n\t\tparent.replies.Remove(pidkey)\n\t} else {\n\t\tthread.replies.Remove(pidkey)\n\t}\n}\n\nfunc (post *Post) HasPermission(addr std.Address, perm Permission) bool {\n\tif post.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\t// post notes inherit permissions of the board.\n\treturn post.board.HasPermission(addr, perm)\n}\n\nfunc (post *Post) GetSummary() string {\n\treturn summaryOf(post.body, 80)\n}\n\nfunc (post *Post) GetURL() string {\n\tif post.IsThread() {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.id, 0)\n\t} else {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.threadID, post.id)\n\t}\n}\n\nfunc (post *Post) GetReplyFormURL() string {\n\treturn txlink.URL(\"CreateReply\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetRepostFormURL() string {\n\treturn txlink.URL(\"CreateRepost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetDeleteFormURL() string {\n\treturn txlink.URL(\"DeletePost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) RenderSummary() string {\n\tif post.repostBoard != 0 {\n\t\tdstBoard := getBoard(post.repostBoard)\n\t\tif dstBoard == nil {\n\t\t\tpanic(\"repostBoard does not exist\")\n\t\t}\n\t\tthread := dstBoard.GetThread(PostID(post.parentID))\n\t\tif thread == nil {\n\t\t\treturn \"reposted post does not exist\"\n\t\t}\n\t\treturn \"Repost: \" + post.GetSummary() + \"\\n\" + thread.RenderSummary()\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += \"## [\" + summaryOf(post.title, 80) + \"](\" + post.GetURL() + \")\\n\"\n\t\tstr += \"\\n\"\n\t}\n\tstr += post.GetSummary() + \"\\n\"\n\tstr += \"\\\\- \" + displayAddressMD(post.creator) + \",\"\n\tstr += \" [\" + post.createdAt.Format(\"2006-01-02 3:04pm MST\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\"\n\tstr += \" (\" + strconv.Itoa(post.replies.Size()) + \" replies)\"\n\tstr += \" (\" + strconv.Itoa(post.reposts.Size()) + \" reposts)\" + \"\\n\"\n\treturn str\n}\n\nfunc (post *Post) RenderPost(indent string, levels int) string {\n\tif post == nil {\n\t\treturn \"nil post\"\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += indent + \"# \" + post.title + \"\\n\"\n\t\tstr += indent + \"\\n\"\n\t}\n\tstr += indentBody(indent, post.body) + \"\\n\" // TODO: indent body lines.\n\tstr += indent + \"\\\\- \" + displayAddressMD(post.creator) + \", \"\n\tstr += \"[\" + post.createdAt.Format(\"2006-01-02 3:04pm (MST)\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[reply](\" + post.GetReplyFormURL() + \")]\"\n\tif post.IsThread() {\n\t\tstr += \" \\\\[[repost](\" + post.GetRepostFormURL() + \")]\"\n\t}\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\\n\"\n\tif levels \u003e 0 {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tpost.replies.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\t\tstr += indent + \"\\n\"\n\t\t\t\tstr += value.(*Post).RenderPost(indent+\"\u003e \", levels-1)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t} else {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tstr += indent + \"\\n\"\n\t\t\tstr += indent + \"_[see all \" + strconv.Itoa(post.replies.Size()) + \" replies](\" + post.GetURL() + \")_\\n\"\n\t\t}\n\t}\n\treturn str\n}\n\n// render reply and link to context thread\nfunc (post *Post) RenderInner() string {\n\tif post.IsThread() {\n\t\tpanic(\"unexpected thread\")\n\t}\n\tthreadID := post.threadID\n\t// replyID := post.id\n\tparentID := post.parentID\n\tstr := \"\"\n\tstr += \"_[see thread](\" + post.board.GetURLFromThreadAndReplyID(\n\t\tthreadID, 0) + \")_\\n\\n\"\n\tthread := post.board.GetThread(post.threadID)\n\tvar parent *Post\n\tif thread.id == parentID {\n\t\tparent = thread\n\t} else {\n\t\tparent = thread.GetReply(parentID)\n\t}\n\tstr += parent.RenderPost(\"\", 0)\n\tstr += \"\\n\"\n\tstr += post.RenderPost(\"\u003e \", 5)\n\treturn str\n}\n" + }, + { + "name": "public.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetBoardIDFromName(name string) (BoardID, bool) {\n\tboardI, exists := gBoardsByName.Get(name)\n\tif !exists {\n\t\treturn 0, false\n\t}\n\treturn boardI.(*Board).id, true\n}\n\nfunc CreateBoard(name string) BoardID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tbid := incGetBoardID()\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tpanic(\"unauthorized\")\n\t}\n\turl := \"/r/demo/boards:\" + name\n\tboard := newBoard(bid, url, name, caller)\n\tbidkey := boardIDKey(bid)\n\tgBoards.Set(bidkey, board)\n\tgBoardsByName.Set(name, board)\n\treturn board.id\n}\n\nfunc checkAnonFee() bool {\n\tsent := std.GetOrigSend()\n\tanonFeeCoin := std.NewCoin(\"ugnot\", int64(gDefaultAnonFee))\n\tif len(sent) == 1 \u0026\u0026 sent[0].IsGTE(anonFeeCoin) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc CreateThread(bid BoardID, title string, body string) PostID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.AddThread(caller, title, body)\n\treturn thread.id\n}\n\nfunc CreateReply(bid BoardID, threadid, postid PostID, body string) PostID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\treply := thread.AddReply(caller, body)\n\t\treturn reply.id\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\treply := post.AddReply(caller, body)\n\t\treturn reply.id\n\t}\n}\n\n// If dstBoard is private, does not ping back.\n// If board specified by bid is private, panics.\nfunc CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\t// TODO: allow with gDefaultAnonFee payment.\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"src board not exist\")\n\t}\n\tif board.IsPrivate() {\n\t\tpanic(\"cannot repost from a private board\")\n\t}\n\tdst := getBoard(dstBoardID)\n\tif dst == nil {\n\t\tpanic(\"dst board not exist\")\n\t}\n\tthread := board.GetThread(postid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\trepost := thread.AddRepostTo(caller, title, body, dst)\n\treturn repost.id\n}\n\nfunc DeletePost(bid BoardID, threadid, postid PostID, reason string) {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// delete thread\n\t\tif !thread.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tboard.DeleteThread(threadid)\n\t} else {\n\t\t// delete thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.DeletePost(postid)\n\t}\n}\n\nfunc EditPost(bid BoardID, threadid, postid PostID, title, body string) {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// edit thread\n\t\tif !thread.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.Update(title, body)\n\t} else {\n\t\t// edit thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tpost.Update(title, body)\n\t}\n}\n" + }, + { + "name": "render.gno", + "body": "package boards\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderBoard(bid BoardID) string {\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\treturn \"missing board\"\n\t}\n\treturn board.RenderBoard()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"These are all the boards of this realm:\\n\\n\"\n\t\tgBoards.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tboard := value.(*Board)\n\t\t\tstr += \" * [\" + board.url + \"](\" + board.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/demo/boards:BOARD_NAME\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\treturn boardI.(*Board).RenderBoard()\n\t} else if len(parts) == 2 {\n\t\t// /r/demo/boards:BOARD_NAME/THREAD_ID\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\treturn thread.RenderPost(\"\", 5)\n\t} else if len(parts) == 3 {\n\t\t// /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\trid, err := strconv.Atoi(parts[2])\n\t\tif err != nil {\n\t\t\treturn \"invalid reply id: \" + parts[2]\n\t\t}\n\t\treply := thread.GetReply(PostID(rid))\n\t\tif reply == nil {\n\t\t\treturn \"reply does not exist with id: \" + parts[2]\n\t\t}\n\t\treturn reply.RenderInner()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n" + }, + { + "name": "role.gno", + "body": "package boards\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission Permission = \"role:edit\"\n)\n" + }, + { + "name": "z_0_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\nimport (\n\t\"gno.land/r/demo/boards\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// unauthorized\n" + }, + { + "name": "z_0_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 19900000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid = boards.CreateBoard(\"test_board\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// payment must not be less than 20000000\n" + }, + { + "name": "z_0_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tboards.CreateThread(1, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n" + }, + { + "name": "z_0_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateReply(bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// thread not exist\n" + }, + { + "name": "z_0_e_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tboards.CreateReply(bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 20000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/demo/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)] (0 replies) (0 reposts)\n//\n// ----------------------------------------\n// ## [Second Post (title)](/r/demo/boards:test_board/2)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)] (1 replies) (0 reposts)\n" + }, + { + "name": "z_10_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// boardId 2 not exist\n\tboards.DeletePost(2, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// board not exist\n" + }, + { + "name": "z_10_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// pid of 2 not exist\n\tboards.DeletePost(bid, 2, 2, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// thread not exist\n" + }, + { + "name": "z_10_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.DeletePost(bid, pid, rid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n" + }, + { + "name": "z_10_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.DeletePost(bid, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// ----------------------------------------------------\n// thread does not exist with id: 1\n" + }, + { + "name": "z_11_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// board 2 not exist\n\tboards.EditPost(2, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// board not exist\n" + }, + { + "name": "z_11_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// thread 2 not exist\n\tboards.EditPost(bid, 2, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// thread not exist\n" + }, + { + "name": "z_11_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// post 2 not exist\n\tboards.EditPost(bid, pid, 2, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// post not exist\n" + }, + { + "name": "z_11_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.EditPost(bid, pid, rid, \"\", \"Edited: First reply of the First post\\n\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e Edited: First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n" + }, + { + "name": "z_11_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.EditPost(bid, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// ----------------------------------------------------\n// # Edited: First Post in (title)\n//\n// Edited: Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n" + }, + { + "name": "z_12_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create a post via registered user\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n" + }, + { + "name": "z_12_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing board\n\trid := boards.CreateRepost(5, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// src board not exist\n" + }, + { + "name": "z_12_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tboards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing thread\n\trid := boards.CreateRepost(bid1, 5, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// thread not exist\n" + }, + { + "name": "z_12_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing destination board\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", 5)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// dst board not exist\n" + }, + { + "name": "z_12_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid1 boards.BoardID\n\tbid2 boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid1 = boards.CreateBoard(\"test_board1\")\n\tpid = boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 = boards.CreateBoard(\"test_board2\")\n}\n\nfunc main() {\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board2\"))\n}\n\n// Output:\n// 1\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=2)]\n//\n// ----------------------------------------\n// Repost: Check this out\n// ## [First Post (title)](/r/demo/boards:test_board1/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)] (0 replies) (1 reposts)\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar board *boards.Board\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\t_ = boards.CreateBoard(\"test_board_1\")\n\t_ = boards.CreateBoard(\"test_board_2\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"\"))\n}\n\n// Output:\n// These are all the boards of this realm:\n//\n// * [/r/demo/boards:test_board_1](/r/demo/boards:test_board_1)\n// * [/r/demo/boards:test_board_2](/r/demo/boards:test_board_2)\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n}\n\nfunc main() {\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n" + }, + { + "name": "z_4_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n}\n\nfunc main() {\n\trid2 := boards.CreateReply(bid, pid, pid, \"Second reply of the second post\")\n\tprintln(rid2)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// 4\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n//\n// \u003e Second reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=4)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=4)]\n\n// Realm:\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/boards\"]\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\",\n// \"ModTime\": \"123\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"68663c8895d37d479e417c11e21badfe21345c61\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:112\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"3f34ac77289aa1d5f9a2f8b6d083138325816fb0\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"94a6665a44bac6ede7f3e3b87173e537b12f9532\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"bc8e5b4e782a0bbc4ac9689681f119beb7b34d59\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9957eadbc91dd32f33b0d815e041a32dbdea0671\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131]={\n// \"Fields\": [\n// {\n// \"N\": \"AAAAgJSeXbo=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"N\": \"AbSNdvQQIhE=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"1024\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Location\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"336074805fc853987abe6f7fe3ad97a6a6f3077a:2\"\n// },\n// \"Index\": \"182\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"1024\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Location\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Board\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"N\": \"BAAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"Second reply of the second post\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"f91e355bd19240f0f3350a7fa0e6a82b72225916\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9ee9c4117be283fc51ffcc5ecd65b75ecef5a9dd\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"eb768b0140a5fe95f9c58747f0960d647dacfd42\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.BoardID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"0fd3352422af0a56a77ef2c9e88f479054e3d51f\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"bed4afa8ffdbbf775451c947fc68b27a345ce32a\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"0\",\n// \"RefCount\": \"2\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c45bbd47a46681a63af973db0ec2180922e4a8ae\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\"\n// }\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\",\n// \"ModTime\": \"134\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"dc1f011553dc53e7a846049e08cc77fa35ea6a51\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:121\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"96b86b4585c7f1075d7794180a5581f72733a7ab\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"32274e1f28fb2b97d67a1262afd362d370de7faa\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c2cfd6aec36a462f35bf02e5bf4a127aa1bb7ac2\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"5cb875179e86d32c517322af7a323b2a5f3e6cc5\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\"\n// }\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85]={\n// \"Fields\": [\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.BoardID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"/r/demo/boards:test_board\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"test_board\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"a416a751c3a45a1e5cba11e737c51340b081e372\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:86\"\n// }\n// },\n// {\n// \"N\": \"BAAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"36299fccbc13f2a84c4629fad4cb940f0bd4b1c6\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:87\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"af6ed0268f99b7f369329094eb6dfaea7812708b\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:88\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85\",\n// \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\",\n// \"RefCount\": \"1\"\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9809329dc1ddc5d3556f7a8fa3c2cebcbf65560b\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:105\",\n// \"RefCount\": \"1\"\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ceae9a1c4ed28bb51062e6ccdccfad0caafd1c4f\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:105\",\n// \"RefCount\": \"1\"\n// }\n// }\n// switchrealm[\"gno.land/r/demo/boards\"]\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/boards\"]\n// switchrealm[\"gno.land/r/demo/boards_test\"]\n" + }, + { + "name": "z_5_b_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n" + }, + { + "name": "z_5_c_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 101000000}}, nil)\n\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post (title)\n//\n// Body of the first post. (body)\n// \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e Reply of the first post\n// \u003e \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n" + }, + { + "name": "z_5_d_filetest.gno", + "body": "package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\n\t// create reply via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\tboards.CreateReply(bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n" + }, + { + "name": "z_5_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\trid2 := boards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=4)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=4)]\n" + }, + { + "name": "z_6_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tboards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\tboards.CreateReply(bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n// \u003e\n// \u003e \u003e First reply of the first reply\n// \u003e \u003e\n// \u003e \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=5)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=5)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=4)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=4)]\n" + }, + { + "name": "z_7_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc init() {\n\t// register\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\t// create board and post\n\tbid := boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/demo/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)] (0 replies) (0 reposts)\n" + }, + { + "name": "z_8_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tboards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\trid2 := boards.CreateReply(bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid)) + \"/\" + strconv.Itoa(int(rid2))))\n}\n\n// Output:\n// _[see thread](/r/demo/boards:test_board/2)_\n//\n// Reply of the second post\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n//\n// _[see all 1 replies](/r/demo/boards:test_board/2/3)_\n//\n// \u003e First reply of the first reply\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=5)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=5)]\n" + }, + { + "name": "z_9_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar dstBoard boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tdstBoard = boards.CreateBoard(\"dst_board\")\n\n\tboards.CreateRepost(0, 0, \"First Post in (title)\", \"Body of the first post. (body)\", dstBoard)\n}\n\nfunc main() {\n}\n\n// Error:\n// src board not exist\n" + }, + { + "name": "z_9_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tsrcBoard boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tsrcBoard = boards.CreateBoard(\"first_board\")\n\tpid = boards.CreateThread(srcBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(srcBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", 0)\n}\n\nfunc main() {\n}\n\n// Error:\n// dst board not exist\n" + }, + { + "name": "z_9_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tfirstBoard boards.BoardID\n\tsecondBoard boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tfirstBoard = boards.CreateBoard(\"first_board\")\n\tsecondBoard = boards.CreateBoard(\"second_board\")\n\tpid = boards.CreateThread(firstBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(firstBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", secondBoard)\n}\n\nfunc main() {\n\tprintln(boards.Render(\"second_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=2\u0026threadid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=2\u0026threadid=1\u0026postid=1)]\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "groups", + "path": "gno.land/p/demo/groups", + "files": [ + { + "name": "groups.gno", + "body": "package groups\n\nimport \"gno.land/r/demo/boards\"\n\n// TODO implement something and test.\ntype Group struct {\n\tBoard *boards.Board\n}\n" + }, + { + "name": "vote_set.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/rat\"\n)\n\n//----------------------------------------\n// VoteSet\n\ntype VoteSet interface {\n\t// number of present votes in set.\n\tSize() int\n\t// add or update vote for voter.\n\tSetVote(voter std.Address, value string) error\n\t// count the number of votes for value.\n\tCountVotes(value string) int\n}\n\n//----------------------------------------\n// VoteList\n\ntype Vote struct {\n\tVoter std.Address\n\tValue string\n}\n\ntype VoteList []Vote\n\nfunc NewVoteList() *VoteList {\n\treturn \u0026VoteList{}\n}\n\nfunc (vlist *VoteList) Size() int {\n\treturn len(*vlist)\n}\n\nfunc (vlist *VoteList) SetVote(voter std.Address, value string) error {\n\t// TODO optimize with binary algorithm\n\tfor i, vote := range *vlist {\n\t\tif vote.Voter == voter {\n\t\t\t// update vote\n\t\t\t(*vlist)[i] = Vote{\n\t\t\t\tVoter: voter,\n\t\t\t\tValue: value,\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\t*vlist = append(*vlist, Vote{\n\t\tVoter: voter,\n\t\tValue: value,\n\t})\n\treturn nil\n}\n\nfunc (vlist *VoteList) CountVotes(target string) int {\n\t// TODO optimize with binary algorithm\n\tvar count int\n\tfor _, vote := range *vlist {\n\t\tif vote.Value == target {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\n//----------------------------------------\n// Committee\n\ntype Committee struct {\n\tQuorum rat.Rat\n\tThreshold rat.Rat\n\tAddresses std.AddressSet\n}\n\n//----------------------------------------\n// VoteSession\n// NOTE: this seems a bit too formal and\n// complicated vs what might be possible;\n// something simpler, more informal.\n\ntype SessionStatus int\n\nconst (\n\tSessionNew SessionStatus = iota\n\tSessionStarted\n\tSessionCompleted\n\tSessionCanceled\n)\n\ntype VoteSession struct {\n\tName string\n\tCreator std.Address\n\tBody string\n\tStart time.Time\n\tDeadline time.Time\n\tStatus SessionStatus\n\tCommittee *Committee\n\tVotes VoteSet\n\tChoices []string\n\tResult string\n}\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/p/demo/groups\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nvar vset groups.VoteSet\n\nfunc init() {\n\taddr1 := testutils.TestAddress(\"test1\")\n\taddr2 := testutils.TestAddress(\"test2\")\n\tvset = groups.NewVoteList()\n\tvset.SetVote(addr1, \"yes\")\n\tvset.SetVote(addr2, \"yes\")\n}\n\nfunc main() {\n\tprintln(vset.Size())\n\tprintln(\"yes:\", vset.CountVotes(\"yes\"))\n\tprintln(\"no:\", vset.CountVotes(\"no\"))\n}\n\n// Output:\n// 2\n// yes: 2\n// no: 0\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "uint256", + "path": "gno.land/p/demo/uint256", + "files": [ + { + "name": "LICENSE", + "body": "BSD 3-Clause License\n\nCopyright 2020 uint256 Authors\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "README.md", + "body": "# Fixed size 256-bit math library\n\nThis is a library specialized at replacing the `big.Int` library for math based on 256-bit types.\n\noriginal repository: [uint256](\u003chttps://github.com/holiman/uint256/tree/master\u003e)\n" + }, + { + "name": "arithmetic.gno", + "body": "// arithmetic provides arithmetic operations for Uint objects.\n// This includes basic binary operations such as addition, subtraction, multiplication, division, and modulo operations\n// as well as overflow checks, and negation. These functions are essential for numeric\n// calculations using 256-bit unsigned integers.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Add sets z to the sum x+y\nfunc (z *Uint) Add(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// AddOverflow sets z to the sum x+y, and returns z and whether overflow occurred\nfunc (z *Uint) AddOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Sub sets z to the difference x-y\nfunc (z *Uint) Sub(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// SubOverflow sets z to the difference x-y and returns z and true if the operation underflowed\nfunc (z *Uint) SubOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Neg returns -x mod 2^256.\nfunc (z *Uint) Neg(x *Uint) *Uint {\n\treturn z.Sub(new(Uint), x)\n}\n\n// commented out for possible overflow\n// Mul sets z to the product x*y\nfunc (z *Uint) Mul(x, y *Uint) *Uint {\n\tvar (\n\t\tres Uint\n\t\tcarry uint64\n\t\tres1, res2, res3 uint64\n\t)\n\n\tcarry, res.arr[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tres3 = x.arr[3]*y.arr[0] + carry\n\n\tcarry, res.arr[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tres3 = res3 + x.arr[2]*y.arr[1] + carry\n\n\tcarry, res.arr[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tres3 = res3 + x.arr[1]*y.arr[2] + carry\n\n\tres.arr[3] = res3 + x.arr[0]*y.arr[3]\n\n\treturn z.Set(\u0026res)\n}\n\n// MulOverflow sets z to the product x*y, and returns z and whether overflow occurred\nfunc (z *Uint) MulOverflow(x, y *Uint) (*Uint, bool) {\n\tp := umul(x, y)\n\tcopy(z.arr[:], p[:4])\n\treturn z, (p[4] | p[5] | p[6] | p[7]) != 0\n}\n\n// commented out for possible overflow\n// Div sets z to the quotient x/y for returns z.\n// If y == 0, z is set to 0\nfunc (z *Uint) Div(x, y *Uint) *Uint {\n\tif y.IsZero() || y.Gt(x) {\n\t\treturn z.Clear()\n\t}\n\tif x.Eq(y) {\n\t\treturn z.SetOne()\n\t}\n\t// Shortcut some cases\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() / y.Uint64())\n\t}\n\n\t// At this point, we know\n\t// x/y ; x \u003e y \u003e 0\n\n\tvar quot Uint\n\tudivrem(quot.arr[:], x.arr[:], y)\n\treturn z.Set(\u0026quot)\n}\n\n// MulMod calculates the modulo-m multiplication of x and y and\n// returns z.\n// If m == 0, z is set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) MulMod(x, y, m *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() || m.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tp := umul(x, y)\n\n\tif m.arr[3] != 0 {\n\t\tmu := Reciprocal(m)\n\t\tr := reduce4(p, m, mu)\n\t\treturn z.Set(\u0026r)\n\t}\n\n\tvar (\n\t\tpl Uint\n\t\tph Uint\n\t)\n\n\tpl = Uint{arr: [4]uint64{p[0], p[1], p[2], p[3]}}\n\tph = Uint{arr: [4]uint64{p[4], p[5], p[6], p[7]}}\n\n\t// If the multiplication is within 256 bits use Mod().\n\tif ph.IsZero() {\n\t\treturn z.Mod(\u0026pl, m)\n\t}\n\n\tvar quot [8]uint64\n\trem := udivrem(quot[:], p[:], m)\n\treturn z.Set(\u0026rem)\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// If y == 0, z is set to 0 (OBS: differs from the big.Uint)\nfunc (z *Uint) Mod(x, y *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tswitch x.Cmp(y) {\n\tcase -1:\n\t\t// x \u003c y\n\t\tcopy(z.arr[:], x.arr[:])\n\t\treturn z\n\tcase 0:\n\t\t// x == y\n\t\treturn z.Clear() // They are equal\n\t}\n\n\t// At this point:\n\t// x != 0\n\t// y != 0\n\t// x \u003e y\n\n\t// Shortcut trivial case\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() % y.Uint64())\n\t}\n\n\tvar quot Uint\n\t*z = udivrem(quot.arr[:], x.arr[:], y)\n\treturn z\n}\n\n// DivMod sets z to the quotient x div y and m to the modulus x mod y and returns the pair (z, m) for y != 0.\n// If y == 0, both z and m are set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) DivMod(x, y, m *Uint) (*Uint, *Uint) {\n\tif y.IsZero() {\n\t\treturn z.Clear(), m.Clear()\n\t}\n\tvar quot Uint\n\t*m = udivrem(quot.arr[:], x.arr[:], y)\n\t*z = quot\n\treturn z, m\n}\n\n// Exp sets z = base**exponent mod 2**256, and returns z.\nfunc (z *Uint) Exp(base, exponent *Uint) *Uint {\n\tres := Uint{arr: [4]uint64{1, 0, 0, 0}}\n\tmultiplier := *base\n\texpBitLen := exponent.BitLen()\n\n\tcurBit := 0\n\tword := exponent.arr[0]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 64; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[1]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 128; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[2]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 192; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[3]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 256; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\treturn z.Set(\u0026res)\n}\n\nfunc (z *Uint) squared() {\n\tvar (\n\t\tres Uint\n\t\tcarry0, carry1, carry2 uint64\n\t\tres1, res2 uint64\n\t)\n\n\tcarry0, res.arr[0] = bits.Mul64(z.arr[0], z.arr[0])\n\tcarry0, res1 = umulHop(carry0, z.arr[0], z.arr[1])\n\tcarry0, res2 = umulHop(carry0, z.arr[0], z.arr[2])\n\n\tcarry1, res.arr[1] = umulHop(res1, z.arr[0], z.arr[1])\n\tcarry1, res2 = umulStep(res2, z.arr[1], z.arr[1], carry1)\n\n\tcarry2, res.arr[2] = umulHop(res2, z.arr[0], z.arr[2])\n\n\tres.arr[3] = 2*(z.arr[0]*z.arr[3]+z.arr[1]*z.arr[2]) + carry0 + carry1 + carry2\n\n\tz.Set(\u0026res)\n}\n\n// udivrem divides u by d and produces both quotient and remainder.\n// The quotient is stored in provided quot - len(u)-len(d)+1 words.\n// It loosely follows the Knuth's division algorithm (sometimes referenced as \"schoolbook\" division) using 64-bit words.\n// See Knuth, Volume 2, section 4.3.1, Algorithm D.\nfunc udivrem(quot, u []uint64, d *Uint) (rem Uint) {\n\tvar dLen int\n\tfor i := len(d.arr) - 1; i \u003e= 0; i-- {\n\t\tif d.arr[i] != 0 {\n\t\t\tdLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tshift := uint(bits.LeadingZeros64(d.arr[dLen-1]))\n\n\tvar dnStorage Uint\n\tdn := dnStorage.arr[:dLen]\n\tfor i := dLen - 1; i \u003e 0; i-- {\n\t\tdn[i] = (d.arr[i] \u003c\u003c shift) | (d.arr[i-1] \u003e\u003e (64 - shift))\n\t}\n\tdn[0] = d.arr[0] \u003c\u003c shift\n\n\tvar uLen int\n\tfor i := len(u) - 1; i \u003e= 0; i-- {\n\t\tif u[i] != 0 {\n\t\t\tuLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif uLen \u003c dLen {\n\t\tcopy(rem.arr[:], u)\n\t\treturn rem\n\t}\n\n\tvar unStorage [9]uint64\n\tun := unStorage[:uLen+1]\n\tun[uLen] = u[uLen-1] \u003e\u003e (64 - shift)\n\tfor i := uLen - 1; i \u003e 0; i-- {\n\t\tun[i] = (u[i] \u003c\u003c shift) | (u[i-1] \u003e\u003e (64 - shift))\n\t}\n\tun[0] = u[0] \u003c\u003c shift\n\n\t// TODO: Skip the highest word of numerator if not significant.\n\n\tif dLen == 1 {\n\t\tr := udivremBy1(quot, un, dn[0])\n\t\trem.SetUint64(r \u003e\u003e shift)\n\t\treturn rem\n\t}\n\n\tudivremKnuth(quot, un, dn)\n\n\tfor i := 0; i \u003c dLen-1; i++ {\n\t\trem.arr[i] = (un[i] \u003e\u003e shift) | (un[i+1] \u003c\u003c (64 - shift))\n\t}\n\trem.arr[dLen-1] = un[dLen-1] \u003e\u003e shift\n\n\treturn rem\n}\n\n// umul computes full 256 x 256 -\u003e 512 multiplication.\nfunc umul(x, y *Uint) [8]uint64 {\n\tvar (\n\t\tres [8]uint64\n\t\tcarry, carry4, carry5, carry6 uint64\n\t\tres1, res2, res3, res4, res5 uint64\n\t)\n\n\tcarry, res[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tcarry4, res3 = umulHop(carry, x.arr[3], y.arr[0])\n\n\tcarry, res[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tcarry, res3 = umulStep(res3, x.arr[2], y.arr[1], carry)\n\tcarry5, res4 = umulStep(carry4, x.arr[3], y.arr[1], carry)\n\n\tcarry, res[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tcarry, res3 = umulStep(res3, x.arr[1], y.arr[2], carry)\n\tcarry, res4 = umulStep(res4, x.arr[2], y.arr[2], carry)\n\tcarry6, res5 = umulStep(carry5, x.arr[3], y.arr[2], carry)\n\n\tcarry, res[3] = umulHop(res3, x.arr[0], y.arr[3])\n\tcarry, res[4] = umulStep(res4, x.arr[1], y.arr[3], carry)\n\tcarry, res[5] = umulStep(res5, x.arr[2], y.arr[3], carry)\n\tres[7], res[6] = umulStep(carry6, x.arr[3], y.arr[3], carry)\n\n\treturn res\n}\n\n// umulStep computes (hi * 2^64 + lo) = z + (x * y) + carry.\nfunc umulStep(z, x, y, carry uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry = bits.Add64(lo, carry, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\tlo, carry = bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// umulHop computes (hi * 2^64 + lo) = z + (x * y)\nfunc umulHop(z, x, y uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry := bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// udivremBy1 divides u by single normalized word d and produces both quotient and remainder.\n// The quotient is stored in provided quot.\nfunc udivremBy1(quot, u []uint64, d uint64) (rem uint64) {\n\treciprocal := reciprocal2by1(d)\n\trem = u[len(u)-1] // Set the top word as remainder.\n\tfor j := len(u) - 2; j \u003e= 0; j-- {\n\t\tquot[j], rem = udivrem2by1(rem, u[j], d, reciprocal)\n\t}\n\treturn rem\n}\n\n// udivremKnuth implements the division of u by normalized multiple word d from the Knuth's division algorithm.\n// The quotient is stored in provided quot - len(u)-len(d) words.\n// Updates u to contain the remainder - len(d) words.\nfunc udivremKnuth(quot, u, d []uint64) {\n\tdh := d[len(d)-1]\n\tdl := d[len(d)-2]\n\treciprocal := reciprocal2by1(dh)\n\n\tfor j := len(u) - len(d) - 1; j \u003e= 0; j-- {\n\t\tu2 := u[j+len(d)]\n\t\tu1 := u[j+len(d)-1]\n\t\tu0 := u[j+len(d)-2]\n\n\t\tvar qhat, rhat uint64\n\t\tif u2 \u003e= dh { // Division overflows.\n\t\t\tqhat = ^uint64(0)\n\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t} else {\n\t\t\tqhat, rhat = udivrem2by1(u2, u1, dh, reciprocal)\n\t\t\tph, pl := bits.Mul64(qhat, dl)\n\t\t\tif ph \u003e rhat || (ph == rhat \u0026\u0026 pl \u003e u0) {\n\t\t\t\tqhat--\n\t\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t\t}\n\t\t}\n\n\t\t// Multiply and subtract.\n\t\tborrow := subMulTo(u[j:], d, qhat)\n\t\tu[j+len(d)] = u2 - borrow\n\t\tif u2 \u003c borrow { // Too much subtracted, add back.\n\t\t\tqhat--\n\t\t\tu[j+len(d)] += addTo(u[j:], d)\n\t\t}\n\n\t\tquot[j] = qhat // Store quotient digit.\n\t}\n}\n\n// isBitSet returns true if bit n-th is set, where n = 0 is LSB.\n// The n must be \u003c= 255.\nfunc (z *Uint) isBitSet(n uint) bool {\n\treturn (z.arr[n/64] \u0026 (1 \u003c\u003c (n % 64))) != 0\n}\n\n// addTo computes x += y.\n// Requires len(x) \u003e= len(y).\nfunc addTo(x, y []uint64) uint64 {\n\tvar carry uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\tx[i], carry = bits.Add64(x[i], y[i], carry)\n\t}\n\treturn carry\n}\n\n// subMulTo computes x -= y * multiplier.\n// Requires len(x) \u003e= len(y).\nfunc subMulTo(x, y []uint64, multiplier uint64) uint64 {\n\tvar borrow uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\ts, carry1 := bits.Sub64(x[i], borrow, 0)\n\t\tph, pl := bits.Mul64(y[i], multiplier)\n\t\tt, carry2 := bits.Sub64(s, pl, 0)\n\t\tx[i] = t\n\t\tborrow = ph + carry1 + carry2\n\t}\n\treturn borrow\n}\n\n// reciprocal2by1 computes \u003c^d, ^0\u003e / d.\nfunc reciprocal2by1(d uint64) uint64 {\n\treciprocal, _ := bits.Div64(^d, ^uint64(0), d)\n\treturn reciprocal\n}\n\n// udivrem2by1 divides \u003cuh, ul\u003e / d and produces both quotient and remainder.\n// It uses the provided d's reciprocal.\n// Implementation ported from https://github.com/chfast/intx and is based on\n// \"Improved division by invariant integers\", Algorithm 4.\nfunc udivrem2by1(uh, ul, d, reciprocal uint64) (quot, rem uint64) {\n\tqh, ql := bits.Mul64(reciprocal, uh)\n\tql, carry := bits.Add64(ql, ul, 0)\n\tqh, _ = bits.Add64(qh, uh, carry)\n\tqh++\n\n\tr := ul - qh*d\n\n\tif r \u003e ql {\n\t\tqh--\n\t\tr += d\n\t}\n\n\tif r \u003e= d {\n\t\tqh++\n\t\tr -= d\n\t}\n\n\treturn qh, r\n}\n" + }, + { + "name": "arithmetic_test.gno", + "body": "package uint256\n\nimport (\n\t\"testing\"\n)\n\ntype binOp2Test struct {\n\tx, y, want string\n}\n\nfunc TestAdd(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"3\", \"4\"},\n\t\t{\"10\", \"10\", \"20\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\", \"36893488147419103230\"}, // uint64 overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Add(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Add(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant string\n\t\toverflow bool\n\t}{\n\t\t{\"0\", \"1\", \"1\", false},\n\t\t{\"1\", \"0\", \"1\", false},\n\t\t{\"1\", \"1\", \"2\", false},\n\t\t{\"10\", \"10\", \"20\", false},\n\t\t{\"18446744073709551615\", \"18446744073709551615\", \"36893488147419103230\", false}, // uint64 overflow, but not Uint256 overflow\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"0\", true}, // 2^256 - 1 + 1, should overflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819967\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", false}, // (2^255 - 1) + 2^255, no overflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819967\", \"57896044618658097711785492504343953926634992332820282019728792003956564819969\", \"0\", true}, // (2^255 - 1) + (2^255 + 1), should overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant, _ := FromDecimal(tt.want)\n\n\t\tgot, overflow := new(Uint).AddOverflow(x, y)\n\n\t\tif got.Cmp(want) != 0 || overflow != tt.overflow {\n\t\t\tt.Errorf(\"AddOverflow(%s, %s) = (%s, %v), want (%s, %v)\",\n\t\t\t\ttt.x, tt.y, got.ToString(), overflow, tt.want, tt.overflow)\n\t\t}\n\t}\n}\n\nfunc TestSub(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"10\", \"10\", \"0\"},\n\t\t{\"31337\", \"1337\", \"30000\"},\n\t\t{\"2\", \"3\", twoPow256Sub1}, // underflow\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\n\t\twant := MustFromDecimal(tc.want)\n\n\t\tgot := new(Uint).Sub(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Sub(%s, %s) = %v, want %v\",\n\t\t\t\ttc.x, tc.y, got.ToString(), want.ToString(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestSubOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant string\n\t\toverflow bool\n\t}{\n\t\t{\"1\", \"0\", \"1\", false},\n\t\t{\"1\", \"1\", \"0\", false},\n\t\t{\"10\", \"10\", \"0\", false},\n\t\t{\"31337\", \"1337\", \"30000\", false},\n\t\t{\"0\", \"1\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", true}, // 0 - 1, should underflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"1\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\", false}, // 2^255 - 1, no underflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"57896044618658097711785492504343953926634992332820282019728792003956564819969\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", true}, // 2^255 - (2^255 + 1), should underflow\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\t\twant := MustFromDecimal(tc.want)\n\n\t\tgot, overflow := new(Uint).SubOverflow(x, y)\n\n\t\tif got.Cmp(want) != 0 || overflow != tc.overflow {\n\t\t\tt.Errorf(\n\t\t\t\t\"SubOverflow(%s, %s) = (%s, %v), want (%s, %v)\",\n\t\t\t\ttc.x, tc.y, got.ToString(), overflow, tc.want, tc.overflow,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestMul(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"10\", \"10\", \"100\"},\n\t\t{\"18446744073709551615\", \"2\", \"36893488147419103230\"}, // uint64 overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Mul(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mul(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMulOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twantZ string\n\t\twantOver bool\n\t}{\n\t\t{\"0x1\", \"0x1\", \"0x1\", false},\n\t\t{\"0x0\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x0\", false},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x2\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", true},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x1\", true},\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000000\", \"0x2\", \"0x0\", true},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x2\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", false},\n\t\t{\"0x100000000000000000\", \"0x100000000000000000\", \"0x10000000000000000000000000000000000\", false},\n\t\t{\"0x10000000000000000000000000000000\", \"0x10000000000000000000000000000000\", \"0x100000000000000000000000000000000000000000000000000000000000000\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\ty := MustFromHex(tt.y)\n\t\twantZ := MustFromHex(tt.wantZ)\n\n\t\tgotZ, gotOver := new(Uint).MulOverflow(x, y)\n\n\t\tif gotZ.Neq(wantZ) {\n\t\t\tt.Errorf(\n\t\t\t\t\"MulOverflow(%s, %s) = %s, want %s\",\n\t\t\t\ttt.x, tt.y, gotZ.ToString(), wantZ.ToString(),\n\t\t\t)\n\t\t}\n\t\tif gotOver != tt.wantOver {\n\t\t\tt.Errorf(\"MulOverflow(%s, %s) = %v, want %v\", tt.x, tt.y, gotOver, tt.wantOver)\n\t\t}\n\t}\n}\n\nfunc TestDiv(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"10445\"},\n\t\t{\"31337\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1000000000000000000\", \"3\", \"333333333333333333\"},\n\t\t{twoPow256Sub1, \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Div(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Div(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMod(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"2\"},\n\t\t{\"31337\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"2\", \"31337\", \"2\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"1\"}, // 2^256 - 1 mod 2\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"3\", \"0\"}, // 2^256 - 1 mod 3\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"}, // 2^256 - 1 mod 2^255\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Mod(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMulMod(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\tm string\n\t\twant string\n\t}{\n\t\t{\"0x1\", \"0x1\", \"0x2\", \"0x1\"},\n\t\t{\"0x10\", \"0x10\", \"0x7\", \"0x4\"},\n\t\t{\"0x100\", \"0x100\", \"0x17\", \"0x9\"},\n\t\t{\"0x31337\", \"0x31337\", \"0x31338\", \"0x1\"},\n\t\t{\"0x0\", \"0x31337\", \"0x31338\", \"0x0\"},\n\t\t{\"0x31337\", \"0x0\", \"0x31338\", \"0x0\"},\n\t\t{\"0x2\", \"0x3\", \"0x5\", \"0x1\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x0\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", \"0x1\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffff\", \"0x0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\ty := MustFromHex(tt.y)\n\t\tm := MustFromHex(tt.m)\n\t\twant := MustFromHex(tt.want)\n\n\t\tgot := new(Uint).MulMod(x, y, m)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"MulMod(%s, %s, %s) = %s, want %s\",\n\t\t\t\ttt.x, tt.y, tt.m, got.ToString(), want.ToString(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestDivMod(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twantDiv string\n\t\twantMod string\n\t}{\n\t\t{\"1\", \"1\", \"1\", \"0\"},\n\t\t{\"10\", \"10\", \"1\", \"0\"},\n\t\t{\"100\", \"10\", \"10\", \"0\"},\n\t\t{\"31337\", \"3\", \"10445\", \"2\"},\n\t\t{\"31337\", \"0\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\", \"0\"},\n\t\t{\"2\", \"31337\", \"0\", \"2\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twantDiv := MustFromDecimal(tt.wantDiv)\n\t\twantMod := MustFromDecimal(tt.wantMod)\n\n\t\tgotDiv := new(Uint)\n\t\tgotMod := new(Uint)\n\t\tgotDiv.DivMod(x, y, gotMod)\n\n\t\tfor i := range gotDiv.arr {\n\t\t\tif gotDiv.arr[i] != wantDiv.arr[i] {\n\t\t\t\tt.Errorf(\"DivMod(%s, %s) got Div %v, want Div %v\", tt.x, tt.y, gotDiv, wantDiv)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor i := range gotMod.arr {\n\t\t\tif gotMod.arr[i] != wantMod.arr[i] {\n\t\t\t\tt.Errorf(\"DivMod(%s, %s) got Mod %v, want Mod %v\", tt.x, tt.y, gotMod, wantMod)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"31337\", \"115792089237316195423570985008687907853269984665640564039457584007913129608599\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129608599\", \"31337\"},\n\t\t{\"0\", \"0\"},\n\t\t{\"2\", \"115792089237316195423570985008687907853269984665640564039457584007913129639934\"},\n\t\t{\"1\", twoPow256Sub1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Neg(x)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Neg(%s) = %v, want %v\", tt.x, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestExp(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"30773171189753\"},\n\t\t{\"31337\", \"0\", \"1\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"2\", \"3\", \"8\"},\n\t\t{\"2\", \"64\", \"18446744073709551616\"},\n\t\t{\"2\", \"128\", \"340282366920938463463374607431768211456\"},\n\t\t{\"2\", \"255\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"2\", \"256\", \"0\"}, // overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Exp(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Exp(%s, %s) = %v, want %v\",\n\t\t\t\ttt.x, tt.y, got.ToString(), want.ToString(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestExp_LargeExponent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbase string\n\t\texponent string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"2^129\",\n\t\t\tbase: \"2\",\n\t\t\texponent: \"680564733841876926926749214863536422912\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"2^193\",\n\t\t\tbase: \"2\",\n\t\t\texponent: \"12379400392853802746563808384000000000000000000\",\n\t\t\texpected: \"0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbase := MustFromDecimal(tt.base)\n\t\t\texponent := MustFromDecimal(tt.exponent)\n\t\t\texpected := MustFromDecimal(tt.expected)\n\n\t\t\tresult := new(Uint).Exp(base, exponent)\n\n\t\t\tif result.Neq(expected) {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Test %s failed. Expected %s, got %s\",\n\t\t\t\t\ttt.name, expected.ToString(), result.ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "bits_table.gno", + "body": "// Copyright 2017 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Code generated by go run make_tables.go. DO NOT EDIT.\n\npackage uint256\n\nconst ntz8tab = \"\" +\n\t\"\\x08\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x07\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\"\n\nconst pop8tab = \"\" +\n\t\"\\x00\\x01\\x01\\x02\\x01\\x02\\x02\\x03\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\\x05\\x06\\x06\\x07\\x06\\x07\\x07\\x08\"\n\nconst rev8tab = \"\" +\n\t\"\\x00\\x80\\x40\\xc0\\x20\\xa0\\x60\\xe0\\x10\\x90\\x50\\xd0\\x30\\xb0\\x70\\xf0\" +\n\t\"\\x08\\x88\\x48\\xc8\\x28\\xa8\\x68\\xe8\\x18\\x98\\x58\\xd8\\x38\\xb8\\x78\\xf8\" +\n\t\"\\x04\\x84\\x44\\xc4\\x24\\xa4\\x64\\xe4\\x14\\x94\\x54\\xd4\\x34\\xb4\\x74\\xf4\" +\n\t\"\\x0c\\x8c\\x4c\\xcc\\x2c\\xac\\x6c\\xec\\x1c\\x9c\\x5c\\xdc\\x3c\\xbc\\x7c\\xfc\" +\n\t\"\\x02\\x82\\x42\\xc2\\x22\\xa2\\x62\\xe2\\x12\\x92\\x52\\xd2\\x32\\xb2\\x72\\xf2\" +\n\t\"\\x0a\\x8a\\x4a\\xca\\x2a\\xaa\\x6a\\xea\\x1a\\x9a\\x5a\\xda\\x3a\\xba\\x7a\\xfa\" +\n\t\"\\x06\\x86\\x46\\xc6\\x26\\xa6\\x66\\xe6\\x16\\x96\\x56\\xd6\\x36\\xb6\\x76\\xf6\" +\n\t\"\\x0e\\x8e\\x4e\\xce\\x2e\\xae\\x6e\\xee\\x1e\\x9e\\x5e\\xde\\x3e\\xbe\\x7e\\xfe\" +\n\t\"\\x01\\x81\\x41\\xc1\\x21\\xa1\\x61\\xe1\\x11\\x91\\x51\\xd1\\x31\\xb1\\x71\\xf1\" +\n\t\"\\x09\\x89\\x49\\xc9\\x29\\xa9\\x69\\xe9\\x19\\x99\\x59\\xd9\\x39\\xb9\\x79\\xf9\" +\n\t\"\\x05\\x85\\x45\\xc5\\x25\\xa5\\x65\\xe5\\x15\\x95\\x55\\xd5\\x35\\xb5\\x75\\xf5\" +\n\t\"\\x0d\\x8d\\x4d\\xcd\\x2d\\xad\\x6d\\xed\\x1d\\x9d\\x5d\\xdd\\x3d\\xbd\\x7d\\xfd\" +\n\t\"\\x03\\x83\\x43\\xc3\\x23\\xa3\\x63\\xe3\\x13\\x93\\x53\\xd3\\x33\\xb3\\x73\\xf3\" +\n\t\"\\x0b\\x8b\\x4b\\xcb\\x2b\\xab\\x6b\\xeb\\x1b\\x9b\\x5b\\xdb\\x3b\\xbb\\x7b\\xfb\" +\n\t\"\\x07\\x87\\x47\\xc7\\x27\\xa7\\x67\\xe7\\x17\\x97\\x57\\xd7\\x37\\xb7\\x77\\xf7\" +\n\t\"\\x0f\\x8f\\x4f\\xcf\\x2f\\xaf\\x6f\\xef\\x1f\\x9f\\x5f\\xdf\\x3f\\xbf\\x7f\\xff\"\n\nconst len8tab = \"\" +\n\t\"\\x00\\x01\\x02\\x02\\x03\\x03\\x03\\x03\\x04\\x04\\x04\\x04\\x04\\x04\\x04\\x04\" +\n\t\"\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\"\n" + }, + { + "name": "bitwise.gno", + "body": "// bitwise contains bitwise operations for Uint instances.\n// This file includes functions to perform bitwise AND, OR, XOR, and NOT operations, as well as bit shifting.\n// These operations are crucial for manipulating individual bits within a 256-bit unsigned integer.\npackage uint256\n\n// Or sets z = x | y and returns z.\nfunc (z *Uint) Or(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] | y.arr[0]\n\tz.arr[1] = x.arr[1] | y.arr[1]\n\tz.arr[2] = x.arr[2] | y.arr[2]\n\tz.arr[3] = x.arr[3] | y.arr[3]\n\treturn z\n}\n\n// And sets z = x \u0026 y and returns z.\nfunc (z *Uint) And(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026 y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026 y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026 y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026 y.arr[3]\n\treturn z\n}\n\n// Not sets z = ^x and returns z.\nfunc (z *Uint) Not(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = ^x.arr[3], ^x.arr[2], ^x.arr[1], ^x.arr[0]\n\treturn z\n}\n\n// AndNot sets z = x \u0026^ y and returns z.\nfunc (z *Uint) AndNot(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026^ y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026^ y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026^ y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026^ y.arr[3]\n\treturn z\n}\n\n// Xor sets z = x ^ y and returns z.\nfunc (z *Uint) Xor(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] ^ y.arr[0]\n\tz.arr[1] = x.arr[1] ^ y.arr[1]\n\tz.arr[2] = x.arr[2] ^ y.arr[2]\n\tz.arr[3] = x.arr[3] ^ y.arr[3]\n\treturn z\n}\n\n// Lsh sets z = x \u003c\u003c n and returns z.\nfunc (z *Uint) Lsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.lsh64(x)\n\t\tcase 128:\n\t\t\treturn z.lsh128(x)\n\t\tcase 192:\n\t\t\treturn z.lsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.lsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.lsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.lsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[0] \u003e\u003e (64 - n)\n\tz.arr[0] = z.arr[0] \u003c\u003c n\n\nsh64:\n\tb = z.arr[1] \u003e\u003e (64 - n)\n\tz.arr[1] = (z.arr[1] \u003c\u003c n) | a\n\nsh128:\n\ta = z.arr[2] \u003e\u003e (64 - n)\n\tz.arr[2] = (z.arr[2] \u003c\u003c n) | b\n\nsh192:\n\tz.arr[3] = (z.arr[3] \u003c\u003c n) | a\n\n\treturn z\n}\n\n// Rsh sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) Rsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.rsh64(x)\n\t\tcase 128:\n\t\t\treturn z.rsh128(x)\n\t\tcase 192:\n\t\t\treturn z.rsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.rsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.rsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.rsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[3] \u003c\u003c (64 - n)\n\tz.arr[3] = z.arr[3] \u003e\u003e n\n\nsh64:\n\tb = z.arr[2] \u003c\u003c (64 - n)\n\tz.arr[2] = (z.arr[2] \u003e\u003e n) | a\n\nsh128:\n\ta = z.arr[1] \u003c\u003c (64 - n)\n\tz.arr[1] = (z.arr[1] \u003e\u003e n) | b\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\n// SRsh (Signed/Arithmetic right shift)\n// considers z to be a signed integer, during right-shift\n// and sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) SRsh(x *Uint, n uint) *Uint {\n\t// If the MSB is 0, SRsh is same as Rsh.\n\tif !x.isBitSet(255) {\n\t\treturn z.Rsh(x, n)\n\t}\n\tif n%64 == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.srsh64(x)\n\t\tcase 128:\n\t\t\treturn z.srsh128(x)\n\t\tcase 192:\n\t\t\treturn z.srsh192(x)\n\t\tdefault:\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t}\n\tvar a uint64 = MaxUint64 \u003c\u003c (64 - n%64)\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t\tz.srsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.srsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.srsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\tz.arr[3], a = (z.arr[3]\u003e\u003en)|a, z.arr[3]\u003c\u003c(64-n)\n\nsh64:\n\tz.arr[2], a = (z.arr[2]\u003e\u003en)|a, z.arr[2]\u003c\u003c(64-n)\n\nsh128:\n\tz.arr[1], a = (z.arr[1]\u003e\u003en)|a, z.arr[1]\u003c\u003c(64-n)\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\nfunc (z *Uint) lsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[2], x.arr[1], x.arr[0], 0\n\treturn z\n}\n\nfunc (z *Uint) lsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[1], x.arr[0], 0, 0\n\treturn z\n}\n\nfunc (z *Uint) lsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[0], 0, 0, 0\n\treturn z\n}\n\nfunc (z *Uint) rsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) rsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) rsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x.arr[3]\n\treturn z\n}\n\nfunc (z *Uint) srsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) srsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) srsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, x.arr[3]\n\treturn z\n}\n" + }, + { + "name": "bitwise_test.gno", + "body": "package uint256\n\nimport \"testing\"\n\ntype logicOpTest struct {\n\tname string\n\tx Uint\n\ty Uint\n\twant Uint\n}\n\nfunc TestOr(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).Or(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Or(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAnd(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).And(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"And(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNot(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx Uint\n\t\twant Uint\n\t}{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).Not(\u0026tt.x)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Not(%s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAndNot(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).AndNot(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"AndNot(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestXor(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).Xor(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Xor(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t{\"0\", 0, \"0\"},\n\t\t{\"0\", 1, \"0\"},\n\t\t{\"0\", 64, \"0\"},\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"2\"},\n\t\t{\"1\", 64, \"18446744073709551616\"},\n\t\t{\"1\", 128, \"340282366920938463463374607431768211456\"},\n\t\t{\"1\", 192, \"6277101735386680763835789423207666416102355444464034512896\"},\n\t\t{\"1\", 255, \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"1\", 256, \"0\"},\n\t\t{\"31337\", 0, \"31337\"},\n\t\t{\"31337\", 1, \"62674\"},\n\t\t{\"31337\", 64, \"578065619037836218990592\"},\n\t\t{\"31337\", 128, \"10663428532201448629551770073089320442396672\"},\n\t\t{\"31337\", 192, \"196705537081812415096322133155058642481399512563169449530621952\"},\n\t\t{\"31337\", 193, \"393411074163624830192644266310117284962799025126338899061243904\"},\n\t\t{\"31337\", 255, \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"31337\", 256, \"0\"},\n\t\t// 64 \u003c n \u003c 128\n\t\t{\"1\", 65, \"36893488147419103232\"},\n\t\t{\"31337\", 100, \"39724366859352024754702188346867712\"},\n\n\t\t// 128 \u003c n \u003c 192\n\t\t{\"1\", 129, \"680564733841876926926749214863536422912\"},\n\t\t{\"31337\", 150, \"44725660946326664792723507424638829088826130956288\"},\n\n\t\t// 192 \u003c n \u003c 256\n\t\t{\"1\", 193, \"12554203470773361527671578846415332832204710888928069025792\"},\n\t\t{\"31337\", 200, \"50356617492943978264658466087695012475238275216171379079839219712\"},\n\n\t\t// n \u003e 256\n\t\t{\"1\", 257, \"0\"},\n\t\t{\"31337\", 300, \"0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Lsh(x, tt.y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Lsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t{\"0\", 0, \"0\"},\n\t\t{\"0\", 1, \"0\"},\n\t\t{\"0\", 64, \"0\"},\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"0\"},\n\t\t{\"1\", 64, \"0\"},\n\t\t{\"1\", 128, \"0\"},\n\t\t{\"1\", 192, \"0\"},\n\t\t{\"1\", 255, \"0\"},\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", 255, \"1\"},\n\t\t{\"6277101735386680763835789423207666416102355444464034512896\", 192, \"1\"},\n\t\t{\"340282366920938463463374607431768211456\", 128, \"1\"},\n\t\t{\"18446744073709551616\", 64, \"1\"},\n\t\t{\"393411074163624830192644266310117284962799025126338899061243904\", 193, \"31337\"},\n\t\t{\"196705537081812415096322133155058642481399512563169449530621952\", 192, \"31337\"},\n\t\t{\"10663428532201448629551770073089320442396672\", 128, \"31337\"},\n\t\t{\"578065619037836218990592\", 64, \"31337\"},\n\t\t{twoPow256Sub1, 256, \"0\"},\n\t\t// outliers\n\t\t{\"340282366920938463463374607431768211455\", 129, \"0\"},\n\t\t{\"18446744073709551615\", 65, \"0\"},\n\t\t{twoPow256Sub1, 1, \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\n\t\t// n \u003e 256\n\t\t{\"1\", 257, \"0\"},\n\t\t{\"31337\", 300, \"0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Rsh(x, tt.y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestSRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t// Positive numbers (behaves like Rsh)\n\t\t{\"0x0\", 0, \"0x0\"},\n\t\t{\"0x0\", 1, \"0x0\"},\n\t\t{\"0x1\", 0, \"0x1\"},\n\t\t{\"0x1\", 1, \"0x0\"},\n\t\t{\"0x31337\", 0, \"0x31337\"},\n\t\t{\"0x31337\", 4, \"0x3133\"},\n\t\t{\"0x31337\", 8, \"0x313\"},\n\t\t{\"0x31337\", 16, \"0x3\"},\n\t\t{\"0x10000000000000000\", 64, \"0x1\"}, // 2^64 \u003e\u003e 64\n\n\t\t// // Numbers with MSB set (negative numbers in two's complement)\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 0, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 1, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 4, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 64, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 128, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 192, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 255, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\n\t\t// Large positive number close to max value\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 1, \"0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 2, \"0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 64, \"0x7fffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 128, \"0x7fffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 192, \"0x7fffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 255, \"0x0\"},\n\n\t\t// Specific cases\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000000\", 1, \"0xc000000000000000000000000000000000000000000000000000000000000000\"},\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000001\", 1, \"0xc000000000000000000000000000000000000000000000000000000000000000\"},\n\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 65, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 127, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 129, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 193, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\n\t\t// n \u003e 256\n\t\t{\"0x1\", 257, \"0x0\"},\n\t\t{\"0x31337\", 300, \"0x0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\twant := MustFromHex(tt.want)\n\n\t\tgot := new(Uint).SRsh(x, tt.y)\n\n\t\tif !got.Eq(want) {\n\t\t\tt.Errorf(\"SRsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n" + }, + { + "name": "cmp.gno", + "body": "// cmp (or, comparisons) includes methods for comparing Uint instances.\n// These comparison functions cover a range of operations including equality checks, less than/greater than\n// evaluations, and specialized comparisons such as signed greater than. These are fundamental for logical\n// decision making based on Uint values.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Cmp compares z and x and returns:\n//\n//\t-1 if z \u003c x\n//\t 0 if z == x\n//\t+1 if z \u003e x\nfunc (z *Uint) Cmp(x *Uint) (r int) {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\td0, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\td1, carry := bits.Sub64(z.arr[1], x.arr[1], carry)\n\td2, carry := bits.Sub64(z.arr[2], x.arr[2], carry)\n\td3, carry := bits.Sub64(z.arr[3], x.arr[3], carry)\n\tif carry == 1 {\n\t\treturn -1\n\t}\n\tif d0|d1|d2|d3 == 0 {\n\t\treturn 0\n\t}\n\treturn 1\n}\n\n// IsZero returns true if z == 0\nfunc (z *Uint) IsZero() bool {\n\treturn (z.arr[0] | z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Sign returns:\n//\n//\t-1 if z \u003c 0\n//\t 0 if z == 0\n//\t+1 if z \u003e 0\n//\n// Where z is interpreted as a two's complement signed number\nfunc (z *Uint) Sign() int {\n\tif z.IsZero() {\n\t\treturn 0\n\t}\n\tif z.arr[3] \u003c 0x8000000000000000 {\n\t\treturn 1\n\t}\n\treturn -1\n}\n\n// LtUint64 returns true if z is smaller than n\nfunc (z *Uint) LtUint64(n uint64) bool {\n\treturn z.arr[0] \u003c n \u0026\u0026 (z.arr[1]|z.arr[2]|z.arr[3]) == 0\n}\n\n// GtUint64 returns true if z is larger than n\nfunc (z *Uint) GtUint64(n uint64) bool {\n\treturn z.arr[0] \u003e n || (z.arr[1]|z.arr[2]|z.arr[3]) != 0\n}\n\n// Lt returns true if z \u003c x\nfunc (z *Uint) Lt(x *Uint) bool {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\t_, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\t_, carry = bits.Sub64(z.arr[1], x.arr[1], carry)\n\t_, carry = bits.Sub64(z.arr[2], x.arr[2], carry)\n\t_, carry = bits.Sub64(z.arr[3], x.arr[3], carry)\n\n\treturn carry != 0\n}\n\n// Gt returns true if z \u003e x\nfunc (z *Uint) Gt(x *Uint) bool {\n\treturn x.Lt(z)\n}\n\n// Lte returns true if z \u003c= x\nfunc (z *Uint) Lte(x *Uint) bool {\n\tcond1 := z.Lt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Gte returns true if z \u003e= x\nfunc (z *Uint) Gte(x *Uint) bool {\n\tcond1 := z.Gt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Eq returns true if z == x\nfunc (z *Uint) Eq(x *Uint) bool {\n\treturn (z.arr[0] == x.arr[0]) \u0026\u0026 (z.arr[1] == x.arr[1]) \u0026\u0026 (z.arr[2] == x.arr[2]) \u0026\u0026 (z.arr[3] == x.arr[3])\n}\n\n// Neq returns true if z != x\nfunc (z *Uint) Neq(x *Uint) bool {\n\treturn !z.Eq(x)\n}\n\n// Sgt interprets z and x as signed integers, and returns\n// true if z \u003e x\nfunc (z *Uint) Sgt(x *Uint) bool {\n\tzSign := z.Sign()\n\txSign := x.Sign()\n\n\tswitch {\n\tcase zSign \u003e= 0 \u0026\u0026 xSign \u003c 0:\n\t\treturn true\n\tcase zSign \u003c 0 \u0026\u0026 xSign \u003e= 0:\n\t\treturn false\n\tdefault:\n\t\treturn z.Gt(x)\n\t}\n}\n" + }, + { + "name": "cmp_test.gno", + "body": "package uint256\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestSign(t *testing.T) {\n\ttests := []struct {\n\t\tinput *Uint\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tinput: NewUint(0),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(1),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(0x7fffffffffffffff),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(0x8000000000000000),\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input.ToString(), func(t *testing.T) {\n\t\t\tresult := tt.input.Sign()\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Sign() = %d; want %d\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCmp(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant int\n\t}{\n\t\t{\"0\", \"0\", 0},\n\t\t{\"0\", \"1\", -1},\n\t\t{\"1\", \"0\", 1},\n\t\t{\"1\", \"1\", 0},\n\t\t{\"10\", \"10\", 0},\n\t\t{\"10\", \"11\", -1},\n\t\t{\"11\", \"10\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\n\t\tgot := x.Cmp(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Cmp(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", true},\n\t\t{\"1\", false},\n\t\t{\"10\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\n\t\tgot := x.IsZero()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"IsZero(%s) = %v, want %v\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestLtUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint64\n\t\twant bool\n\t}{\n\t\t{\"0\", 1, true},\n\t\t{\"1\", 0, false},\n\t\t{\"10\", 10, false},\n\t\t{\"0xffffffffffffffff\", 0, false},\n\t\t{\"0x10000000000000000\", 10000000000000000, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := parseTestString(t, tc.x)\n\n\t\tgot := x.LtUint64(tc.y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"LtUint64(%s, %d) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_GtUint64(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tz string\n\t\tn uint64\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"z \u003e n\",\n\t\t\tz: \"1\",\n\t\t\tn: 0,\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"z \u003c n\",\n\t\t\tz: \"18446744073709551615\",\n\t\t\tn: 0xFFFFFFFFFFFFFFFF,\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"z == n\",\n\t\t\tz: \"18446744073709551615\",\n\t\t\tn: 0xFFFFFFFFFFFFFFFF,\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := MustFromDecimal(tt.z)\n\n\t\t\tif got := z.GtUint64(tt.n); got != tt.want {\n\t\t\t\tt.Errorf(\"Uint.GtUint64() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSGT(t *testing.T) {\n\tx := MustFromHex(\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\")\n\ty := MustFromHex(\"0x0\")\n\tactual := x.Sgt(y)\n\tif actual {\n\t\tt.Fatalf(\"Expected %v false\", actual)\n\t}\n\n\tx = MustFromHex(\"0x0\")\n\ty = MustFromHex(\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\")\n\tactual = x.Sgt(y)\n\tif !actual {\n\t\tt.Fatalf(\"Expected %v true\", actual)\n\t}\n}\n\nfunc TestEq(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twant bool\n\t}{\n\t\t{\"0xffffffffffffffff\", \"18446744073709551615\", true},\n\t\t{\"0x10000000000000000\", \"18446744073709551616\", true},\n\t\t{\"0\", \"0\", true},\n\t\t{twoPow256Sub1, twoPow256Sub1, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := parseTestString(t, tt.x)\n\n\t\ty, err := FromDecimal(tt.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Eq(y)\n\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Eq(%s, %s) = %v, want %v\", tt.x, tt.y, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_Lte(t *testing.T) {\n\ttests := []struct {\n\t\tz, x string\n\t\twant bool\n\t}{\n\t\t{\"10\", \"20\", true},\n\t\t{\"20\", \"10\", false},\n\t\t{\"10\", \"10\", true},\n\t\t{\"0\", \"0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, err := FromDecimal(tt.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tx, err := FromDecimal(tt.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tif got := z.Lte(x); got != tt.want {\n\t\t\tt.Errorf(\"Uint.Lte(%v, %v) = %v, want %v\", tt.z, tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_Gte(t *testing.T) {\n\ttests := []struct {\n\t\tz, x string\n\t\twant bool\n\t}{\n\t\t{\"20\", \"10\", true},\n\t\t{\"10\", \"20\", false},\n\t\t{\"10\", \"10\", true},\n\t\t{\"0\", \"0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := parseTestString(t, tt.z)\n\t\tx := parseTestString(t, tt.x)\n\n\t\tif got := z.Gte(x); got != tt.want {\n\t\t\tt.Errorf(\"Uint.Gte(%v, %v) = %v, want %v\", tt.z, tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc parseTestString(_ *testing.T, s string) *Uint {\n\tvar x *Uint\n\n\tif strings.HasPrefix(s, \"0x\") {\n\t\tx = MustFromHex(s)\n\t} else {\n\t\tx = MustFromDecimal(s)\n\t}\n\n\treturn x\n}\n" + }, + { + "name": "conversion.gno", + "body": "// conversions contains methods for converting Uint instances to other types and vice versa.\n// This includes conversions to and from basic types such as uint64 and int32, as well as string representations\n// and byte slices. Additionally, it covers marshaling and unmarshaling for JSON and other text formats.\npackage uint256\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Uint) Uint64() uint64 {\n\treturn z.arr[0]\n}\n\n// Uint64WithOverflow returns the lower 64-bits of z and bool whether overflow occurred\nfunc (z *Uint) Uint64WithOverflow() (uint64, bool) {\n\treturn z.arr[0], (z.arr[1] | z.arr[2] | z.arr[3]) != 0\n}\n\n// SetUint64 sets z to the value x\nfunc (z *Uint) SetUint64(x uint64) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x\n\treturn z\n}\n\n// IsUint64 reports whether z can be represented as a uint64.\nfunc (z *Uint) IsUint64() bool {\n\treturn (z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Dec returns the decimal representation of z.\nfunc (z *Uint) Dec() string {\n\tif z.IsZero() {\n\t\treturn \"0\"\n\t}\n\tif z.IsUint64() {\n\t\treturn strconv.FormatUint(z.Uint64(), 10)\n\t}\n\n\t// The max uint64 value being 18446744073709551615, the largest\n\t// power-of-ten below that is 10000000000000000000.\n\t// When we do a DivMod using that number, the remainder that we\n\t// get back is the lower part of the output.\n\t//\n\t// The ascii-output of remainder will never exceed 19 bytes (since it will be\n\t// below 10000000000000000000).\n\t//\n\t// Algorithm example using 100 as divisor\n\t//\n\t// 12345 % 100 = 45 (rem)\n\t// 12345 / 100 = 123 (quo)\n\t// -\u003e output '45', continue iterate on 123\n\tvar (\n\t\t// out is 98 bytes long: 78 (max size of a string without leading zeroes,\n\t\t// plus slack so we can copy 19 bytes every iteration).\n\t\t// We init it with zeroes, because when strconv appends the ascii representations,\n\t\t// it will omit leading zeroes.\n\t\tout = []byte(\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\")\n\t\tdivisor = NewUint(10000000000000000000) // 20 digits\n\t\ty = new(Uint).Set(z) // copy to avoid modifying z\n\t\tpos = len(out) // position to write to\n\t\tbuf = make([]byte, 0, 19) // buffer to write uint64:s to\n\t)\n\tfor {\n\t\t// Obtain Q and R for divisor\n\t\tvar quot Uint\n\t\trem := udivrem(quot.arr[:], y.arr[:], divisor)\n\t\ty.Set(\u0026quot) // Set Q for next loop\n\t\t// Convert the R to ascii representation\n\t\tbuf = strconv.AppendUint(buf[:0], rem.Uint64(), 10)\n\t\t// Copy in the ascii digits\n\t\tcopy(out[pos-len(buf):], buf)\n\t\tif y.IsZero() {\n\t\t\tbreak\n\t\t}\n\t\t// Move 19 digits left\n\t\tpos -= 19\n\t}\n\t// skip leading zeroes by only using the 'used size' of buf\n\treturn string(out[pos-len(buf):])\n}\n\nfunc (z *Uint) Scan(src interface{}) error {\n\tif src == nil {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tswitch src := src.(type) {\n\tcase string:\n\t\treturn z.scanScientificFromString(src)\n\tcase []byte:\n\t\treturn z.scanScientificFromString(string(src))\n\t}\n\treturn errors.New(\"default // unsupported type: can't convert to uint256.Uint\")\n}\n\nfunc (z *Uint) scanScientificFromString(src string) error {\n\tif len(src) == 0 {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tidx := strings.IndexByte(src, 'e')\n\tif idx == -1 {\n\t\treturn z.SetFromDecimal(src)\n\t}\n\tif err := z.SetFromDecimal(src[:idx]); err != nil {\n\t\treturn err\n\t}\n\tif src[(idx+1):] == \"0\" {\n\t\treturn nil\n\t}\n\texp := new(Uint)\n\tif err := exp.SetFromDecimal(src[(idx + 1):]); err != nil {\n\t\treturn err\n\t}\n\tif exp.GtUint64(77) { // 10**78 is larger than 2**256\n\t\treturn ErrBig256Range\n\t}\n\texp.Exp(NewUint(10), exp)\n\tif _, overflow := z.MulOverflow(z, exp); overflow {\n\t\treturn ErrBig256Range\n\t}\n\treturn nil\n}\n\n// ToString returns the decimal string representation of z. It returns an empty string if z is nil.\n// OBS: doesn't exist from holiman's uint256\nfunc (z *Uint) ToString() string {\n\tif z == nil {\n\t\treturn \"\"\n\t}\n\n\treturn z.Dec()\n}\n\n// MarshalJSON implements json.Marshaler.\n// MarshalJSON marshals using the 'decimal string' representation. This is _not_ compatible\n// with big.Uint: big.Uint marshals into JSON 'native' numeric format.\n//\n// The JSON native format is, on some platforms, (e.g. javascript), limited to 53-bit large\n// integer space. Thus, U256 uses string-format, which is not compatible with\n// big.int (big.Uint refuses to unmarshal a string representation).\nfunc (z *Uint) MarshalJSON() ([]byte, error) {\n\treturn []byte(`\"` + z.Dec() + `\"`), nil\n}\n\n// UnmarshalJSON implements json.Unmarshaler. UnmarshalJSON accepts either\n// - Quoted string: either hexadecimal OR decimal\n// - Not quoted string: only decimal\nfunc (z *Uint) UnmarshalJSON(input []byte) error {\n\tif len(input) \u003c 2 || input[0] != '\"' || input[len(input)-1] != '\"' {\n\t\t// if not quoted, it must be decimal\n\t\treturn z.fromDecimal(string(input))\n\t}\n\treturn z.UnmarshalText(input[1 : len(input)-1])\n}\n\n// MarshalText implements encoding.TextMarshaler\n// MarshalText marshals using the decimal representation (compatible with big.Uint)\nfunc (z *Uint) MarshalText() ([]byte, error) {\n\treturn []byte(z.Dec()), nil\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler. This method\n// can unmarshal either hexadecimal or decimal.\n// - For hexadecimal, the input _must_ be prefixed with 0x or 0X\nfunc (z *Uint) UnmarshalText(input []byte) error {\n\tif len(input) \u003e= 2 \u0026\u0026 input[0] == '0' \u0026\u0026 (input[1] == 'x' || input[1] == 'X') {\n\t\treturn z.fromHex(string(input))\n\t}\n\treturn z.fromDecimal(string(input))\n}\n\n// SetBytes interprets buf as the bytes of a big-endian unsigned\n// integer, sets z to that value, and returns z.\n// If buf is larger than 32 bytes, the last 32 bytes is used.\nfunc (z *Uint) SetBytes(buf []byte) *Uint {\n\tswitch l := len(buf); l {\n\tcase 0:\n\t\tz.Clear()\n\tcase 1:\n\t\tz.SetBytes1(buf)\n\tcase 2:\n\t\tz.SetBytes2(buf)\n\tcase 3:\n\t\tz.SetBytes3(buf)\n\tcase 4:\n\t\tz.SetBytes4(buf)\n\tcase 5:\n\t\tz.SetBytes5(buf)\n\tcase 6:\n\t\tz.SetBytes6(buf)\n\tcase 7:\n\t\tz.SetBytes7(buf)\n\tcase 8:\n\t\tz.SetBytes8(buf)\n\tcase 9:\n\t\tz.SetBytes9(buf)\n\tcase 10:\n\t\tz.SetBytes10(buf)\n\tcase 11:\n\t\tz.SetBytes11(buf)\n\tcase 12:\n\t\tz.SetBytes12(buf)\n\tcase 13:\n\t\tz.SetBytes13(buf)\n\tcase 14:\n\t\tz.SetBytes14(buf)\n\tcase 15:\n\t\tz.SetBytes15(buf)\n\tcase 16:\n\t\tz.SetBytes16(buf)\n\tcase 17:\n\t\tz.SetBytes17(buf)\n\tcase 18:\n\t\tz.SetBytes18(buf)\n\tcase 19:\n\t\tz.SetBytes19(buf)\n\tcase 20:\n\t\tz.SetBytes20(buf)\n\tcase 21:\n\t\tz.SetBytes21(buf)\n\tcase 22:\n\t\tz.SetBytes22(buf)\n\tcase 23:\n\t\tz.SetBytes23(buf)\n\tcase 24:\n\t\tz.SetBytes24(buf)\n\tcase 25:\n\t\tz.SetBytes25(buf)\n\tcase 26:\n\t\tz.SetBytes26(buf)\n\tcase 27:\n\t\tz.SetBytes27(buf)\n\tcase 28:\n\t\tz.SetBytes28(buf)\n\tcase 29:\n\t\tz.SetBytes29(buf)\n\tcase 30:\n\t\tz.SetBytes30(buf)\n\tcase 31:\n\t\tz.SetBytes31(buf)\n\tdefault:\n\t\tz.SetBytes32(buf[l-32:])\n\t}\n\treturn z\n}\n\n// SetBytes1 is identical to SetBytes(in[:1]), but panics is input is too short\nfunc (z *Uint) SetBytes1(in []byte) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(in[0])\n\treturn z\n}\n\n// SetBytes2 is identical to SetBytes(in[:2]), but panics is input is too short\nfunc (z *Uint) SetBytes2(in []byte) *Uint {\n\t_ = in[1] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\treturn z\n}\n\n// SetBytes3 is identical to SetBytes(in[:3]), but panics is input is too short\nfunc (z *Uint) SetBytes3(in []byte) *Uint {\n\t_ = in[2] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\treturn z\n}\n\n// SetBytes4 is identical to SetBytes(in[:4]), but panics is input is too short\nfunc (z *Uint) SetBytes4(in []byte) *Uint {\n\t_ = in[3] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\treturn z\n}\n\n// SetBytes5 is identical to SetBytes(in[:5]), but panics is input is too short\nfunc (z *Uint) SetBytes5(in []byte) *Uint {\n\t_ = in[4] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint40(in[0:5])\n\treturn z\n}\n\n// SetBytes6 is identical to SetBytes(in[:6]), but panics is input is too short\nfunc (z *Uint) SetBytes6(in []byte) *Uint {\n\t_ = in[5] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint48(in[0:6])\n\treturn z\n}\n\n// SetBytes7 is identical to SetBytes(in[:7]), but panics is input is too short\nfunc (z *Uint) SetBytes7(in []byte) *Uint {\n\t_ = in[6] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint56(in[0:7])\n\treturn z\n}\n\n// SetBytes8 is identical to SetBytes(in[:8]), but panics is input is too short\nfunc (z *Uint) SetBytes8(in []byte) *Uint {\n\t_ = in[7] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = binary.BigEndian.Uint64(in[0:8])\n\treturn z\n}\n\n// SetBytes9 is identical to SetBytes(in[:9]), but panics is input is too short\nfunc (z *Uint) SetBytes9(in []byte) *Uint {\n\t_ = in[8] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(in[0])\n\tz.arr[0] = binary.BigEndian.Uint64(in[1:9])\n\treturn z\n}\n\n// SetBytes10 is identical to SetBytes(in[:10]), but panics is input is too short\nfunc (z *Uint) SetBytes10(in []byte) *Uint {\n\t_ = in[9] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[2:10])\n\treturn z\n}\n\n// SetBytes11 is identical to SetBytes(in[:11]), but panics is input is too short\nfunc (z *Uint) SetBytes11(in []byte) *Uint {\n\t_ = in[10] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[0] = binary.BigEndian.Uint64(in[3:11])\n\treturn z\n}\n\n// SetBytes12 is identical to SetBytes(in[:12]), but panics is input is too short\nfunc (z *Uint) SetBytes12(in []byte) *Uint {\n\t_ = in[11] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[4:12])\n\treturn z\n}\n\n// SetBytes13 is identical to SetBytes(in[:13]), but panics is input is too short\nfunc (z *Uint) SetBytes13(in []byte) *Uint {\n\t_ = in[12] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint40(in[0:5])\n\tz.arr[0] = binary.BigEndian.Uint64(in[5:13])\n\treturn z\n}\n\n// SetBytes14 is identical to SetBytes(in[:14]), but panics is input is too short\nfunc (z *Uint) SetBytes14(in []byte) *Uint {\n\t_ = in[13] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint48(in[0:6])\n\tz.arr[0] = binary.BigEndian.Uint64(in[6:14])\n\treturn z\n}\n\n// SetBytes15 is identical to SetBytes(in[:15]), but panics is input is too short\nfunc (z *Uint) SetBytes15(in []byte) *Uint {\n\t_ = in[14] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint56(in[0:7])\n\tz.arr[0] = binary.BigEndian.Uint64(in[7:15])\n\treturn z\n}\n\n// SetBytes16 is identical to SetBytes(in[:16]), but panics is input is too short\nfunc (z *Uint) SetBytes16(in []byte) *Uint {\n\t_ = in[15] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[0] = binary.BigEndian.Uint64(in[8:16])\n\treturn z\n}\n\n// SetBytes17 is identical to SetBytes(in[:17]), but panics is input is too short\nfunc (z *Uint) SetBytes17(in []byte) *Uint {\n\t_ = in[16] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(in[0])\n\tz.arr[1] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[0] = binary.BigEndian.Uint64(in[9:17])\n\treturn z\n}\n\n// SetBytes18 is identical to SetBytes(in[:18]), but panics is input is too short\nfunc (z *Uint) SetBytes18(in []byte) *Uint {\n\t_ = in[17] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[0] = binary.BigEndian.Uint64(in[10:18])\n\treturn z\n}\n\n// SetBytes19 is identical to SetBytes(in[:19]), but panics is input is too short\nfunc (z *Uint) SetBytes19(in []byte) *Uint {\n\t_ = in[18] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[1] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[0] = binary.BigEndian.Uint64(in[11:19])\n\treturn z\n}\n\n// SetBytes20 is identical to SetBytes(in[:20]), but panics is input is too short\nfunc (z *Uint) SetBytes20(in []byte) *Uint {\n\t_ = in[19] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[0] = binary.BigEndian.Uint64(in[12:20])\n\treturn z\n}\n\n// SetBytes21 is identical to SetBytes(in[:21]), but panics is input is too short\nfunc (z *Uint) SetBytes21(in []byte) *Uint {\n\t_ = in[20] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint40(in[0:5])\n\tz.arr[1] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[0] = binary.BigEndian.Uint64(in[13:21])\n\treturn z\n}\n\n// SetBytes22 is identical to SetBytes(in[:22]), but panics is input is too short\nfunc (z *Uint) SetBytes22(in []byte) *Uint {\n\t_ = in[21] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint48(in[0:6])\n\tz.arr[1] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[0] = binary.BigEndian.Uint64(in[14:22])\n\treturn z\n}\n\n// SetBytes23 is identical to SetBytes(in[:23]), but panics is input is too short\nfunc (z *Uint) SetBytes23(in []byte) *Uint {\n\t_ = in[22] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint56(in[0:7])\n\tz.arr[1] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[0] = binary.BigEndian.Uint64(in[15:23])\n\treturn z\n}\n\n// SetBytes24 is identical to SetBytes(in[:24]), but panics is input is too short\nfunc (z *Uint) SetBytes24(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[1] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[0] = binary.BigEndian.Uint64(in[16:24])\n\treturn z\n}\n\n// SetBytes25 is identical to SetBytes(in[:25]), but panics is input is too short\nfunc (z *Uint) SetBytes25(in []byte) *Uint {\n\t_ = in[24] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(in[0])\n\tz.arr[2] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[1] = binary.BigEndian.Uint64(in[9:17])\n\tz.arr[0] = binary.BigEndian.Uint64(in[17:25])\n\treturn z\n}\n\n// SetBytes26 is identical to SetBytes(in[:26]), but panics is input is too short\nfunc (z *Uint) SetBytes26(in []byte) *Uint {\n\t_ = in[25] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[1] = binary.BigEndian.Uint64(in[10:18])\n\tz.arr[0] = binary.BigEndian.Uint64(in[18:26])\n\treturn z\n}\n\n// SetBytes27 is identical to SetBytes(in[:27]), but panics is input is too short\nfunc (z *Uint) SetBytes27(in []byte) *Uint {\n\t_ = in[26] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[2] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[1] = binary.BigEndian.Uint64(in[11:19])\n\tz.arr[0] = binary.BigEndian.Uint64(in[19:27])\n\treturn z\n}\n\n// SetBytes28 is identical to SetBytes(in[:28]), but panics is input is too short\nfunc (z *Uint) SetBytes28(in []byte) *Uint {\n\t_ = in[27] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[1] = binary.BigEndian.Uint64(in[12:20])\n\tz.arr[0] = binary.BigEndian.Uint64(in[20:28])\n\treturn z\n}\n\n// SetBytes29 is identical to SetBytes(in[:29]), but panics is input is too short\nfunc (z *Uint) SetBytes29(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint40(in[0:5])\n\tz.arr[2] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[1] = binary.BigEndian.Uint64(in[13:21])\n\tz.arr[0] = binary.BigEndian.Uint64(in[21:29])\n\treturn z\n}\n\n// SetBytes30 is identical to SetBytes(in[:30]), but panics is input is too short\nfunc (z *Uint) SetBytes30(in []byte) *Uint {\n\t_ = in[29] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint48(in[0:6])\n\tz.arr[2] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[1] = binary.BigEndian.Uint64(in[14:22])\n\tz.arr[0] = binary.BigEndian.Uint64(in[22:30])\n\treturn z\n}\n\n// SetBytes31 is identical to SetBytes(in[:31]), but panics is input is too short\nfunc (z *Uint) SetBytes31(in []byte) *Uint {\n\t_ = in[30] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint56(in[0:7])\n\tz.arr[2] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[1] = binary.BigEndian.Uint64(in[15:23])\n\tz.arr[0] = binary.BigEndian.Uint64(in[23:31])\n\treturn z\n}\n\n// SetBytes32 sets z to the value of the big-endian 256-bit unsigned integer in.\nfunc (z *Uint) SetBytes32(in []byte) *Uint {\n\t_ = in[31] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[2] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[1] = binary.BigEndian.Uint64(in[16:24])\n\tz.arr[0] = binary.BigEndian.Uint64(in[24:32])\n\treturn z\n}\n\n// Utility methods that are \"missing\" among the bigEndian.UintXX methods.\n\n// bigEndianUint40 returns the uint64 value represented by the 5 bytes in big-endian order.\nfunc bigEndianUint40(b []byte) uint64 {\n\t_ = b[4] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[4]) | uint64(b[3])\u003c\u003c8 | uint64(b[2])\u003c\u003c16 | uint64(b[1])\u003c\u003c24 |\n\t\tuint64(b[0])\u003c\u003c32\n}\n\n// bigEndianUint56 returns the uint64 value represented by the 7 bytes in big-endian order.\nfunc bigEndianUint56(b []byte) uint64 {\n\t_ = b[6] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[6]) | uint64(b[5])\u003c\u003c8 | uint64(b[4])\u003c\u003c16 | uint64(b[3])\u003c\u003c24 |\n\t\tuint64(b[2])\u003c\u003c32 | uint64(b[1])\u003c\u003c40 | uint64(b[0])\u003c\u003c48\n}\n\n// bigEndianUint48 returns the uint64 value represented by the 6 bytes in big-endian order.\nfunc bigEndianUint48(b []byte) uint64 {\n\t_ = b[5] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[5]) | uint64(b[4])\u003c\u003c8 | uint64(b[3])\u003c\u003c16 | uint64(b[2])\u003c\u003c24 |\n\t\tuint64(b[1])\u003c\u003c32 | uint64(b[0])\u003c\u003c40\n}\n" + }, + { + "name": "conversion_test.gno", + "body": "package uint256\n\nimport \"testing\"\n\nfunc TestIsUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0x0\", true},\n\t\t{\"0x1\", true},\n\t\t{\"0x10\", true},\n\t\t{\"0xffffffffffffffff\", true},\n\t\t{\"0x10000000000000000\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\tgot := x.IsUint64()\n\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"IsUint64(%s) = %v, want %v\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestDec(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tz Uint\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"zero\",\n\t\t\tz: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"less than 20 digits\",\n\t\t\tz: Uint{arr: [4]uint64{1234567890, 0, 0, 0}},\n\t\t\twant: \"1234567890\",\n\t\t},\n\t\t{\n\t\t\tname: \"max possible value\",\n\t\t\tz: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: twoPow256Sub1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.z.Dec()\n\t\t\tif result != tt.want {\n\t\t\t\tt.Errorf(\"Dec(%v) = %s, want %s\", tt.z, result, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUint_Scan(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput interface{}\n\t\twant *Uint\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tinput: nil,\n\t\t\twant: NewUint(0),\n\t\t},\n\t\t{\n\t\t\tname: \"valid scientific notation\",\n\t\t\tinput: \"1e4\",\n\t\t\twant: NewUint(10000),\n\t\t},\n\t\t{\n\t\t\tname: \"valid decimal string\",\n\t\t\tinput: \"12345\",\n\t\t\twant: NewUint(12345),\n\t\t},\n\t\t{\n\t\t\tname: \"valid byte slice\",\n\t\t\tinput: []byte(\"12345\"),\n\t\t\twant: NewUint(12345),\n\t\t},\n\t\t{\n\t\t\tname: \"invalid string\",\n\t\t\tinput: \"invalid\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"out of range\",\n\t\t\tinput: \"115792089237316195423570985008687907853269984665640564039457584007913129639936\", // 2^256\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"unsupported type\",\n\t\t\tinput: 123,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := new(Uint)\n\t\t\terr := z.Scan(tt.input)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Scan() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Scan() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\t}\n\t\t\t\tif !z.Eq(tt.want) {\n\t\t\t\t\tt.Errorf(\"Scan() = %v, want %v\", z, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpected string\n\t}{\n\t\t{[]byte{}, \"0\"},\n\t\t{[]byte{0x01}, \"1\"},\n\t\t{[]byte{0x12, 0x34}, \"4660\"},\n\t\t{[]byte{0x12, 0x34, 0x56}, \"1193046\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78}, \"305419896\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a}, \"78187493530\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"20015998343868\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"5124095576030430\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"1311768467463790320\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"335812727670730321938\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"85968058283706962416180\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"22007822920628982378542166\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"5634002667681019488906794616\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"1442304682926340989160139421850\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"369229998829143293224995691993788\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"94522879700260683065598897150409950\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"24197857203266734864793317670504947440\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"6194651444036284125387089323649266544658\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"1585830769673288736099094866854212235432500\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"405972677036361916441368285914678332270720086\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"103929005321308650608990281194157653061304342136\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"26605825362255014555901511985704359183693911586970\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"6811091292737283726310787068340315951025641366264508\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"1743639370940744633935561489495120883462564189763714270\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"446371678960830626287503741310750946166416432579510853360\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"114271149813972640329600957775552242218602606740354778460178\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"29253414352376995924377845190541374007962267325530823285805620\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"7488874074208510956640728368778591746038340435335890761166238806\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"1917151762997378804900026462407319486985815151445988034858557134456\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"490790851327328974054406774376273788668368678770172936923790626420890\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"125642457939796217357928134240326089899102381765164271852490400363748028\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"32164469232587831643629602365523479014170209731882053594237542493119495390\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"8234104123542484900769178205574010627627573691361805720124810878238590820080\"},\n\t\t// over 32 bytes (last 32 bytes are used)\n\t\t{append([]byte{0xff}, []byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}...), \"8234104123542484900769178205574010627627573691361805720124810878238590820080\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tz := new(Uint)\n\t\tz.SetBytes(test.input)\n\t\texpected := MustFromDecimal(test.expected)\n\t\tif z.Cmp(expected) != 0 {\n\t\t\tt.Errorf(\"SetBytes(%x) = %s, expected %s\", test.input, z.ToString(), test.expected)\n\t\t}\n\t}\n}\n" + }, + { + "name": "error.gno", + "body": "package uint256\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrEmptyString = errors.New(\"empty hex string\")\n\tErrSyntax = errors.New(\"invalid hex string\")\n\tErrRange = errors.New(\"number out of range\")\n\tErrMissingPrefix = errors.New(\"hex string without 0x prefix\")\n\tErrEmptyNumber = errors.New(\"hex string \\\"0x\\\"\")\n\tErrLeadingZero = errors.New(\"hex number with leading zero digits\")\n\tErrBig256Range = errors.New(\"hex number \u003e 256 bits\")\n\tErrBadBufferLength = errors.New(\"bad ssz buffer length\")\n\tErrBadEncodedLength = errors.New(\"bad ssz encoded length\")\n\tErrInvalidBase = errors.New(\"invalid base\")\n\tErrInvalidBitSize = errors.New(\"invalid bit size\")\n)\n\ntype u256Error struct {\n\tfn string // function name\n\tinput string\n\terr error\n}\n\nfunc (e *u256Error) Error() string {\n\treturn e.fn + \": \" + e.input + \": \" + e.err.Error()\n}\n\nfunc (e *u256Error) Unwrap() error {\n\treturn e.err\n}\n\nfunc errEmptyString(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyString}\n}\n\nfunc errSyntax(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrSyntax}\n}\n\nfunc errMissingPrefix(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrMissingPrefix}\n}\n\nfunc errEmptyNumber(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyNumber}\n}\n\nfunc errLeadingZero(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrLeadingZero}\n}\n\nfunc errRange(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrRange}\n}\n\nfunc errBig256Range(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBig256Range}\n}\n\nfunc errBadBufferLength(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBadBufferLength}\n}\n\nfunc errInvalidBase(fn string, base int) error {\n\treturn \u0026u256Error{fn: fn, input: string(base), err: ErrInvalidBase}\n}\n\nfunc errInvalidBitSize(fn string, bitSize int) error {\n\treturn \u0026u256Error{fn: fn, input: string(bitSize), err: ErrInvalidBitSize}\n}\n" + }, + { + "name": "mod.gno", + "body": "package uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Some utility functions\n\n// Reciprocal computes a 320-bit value representing 1/m\n//\n// Notes:\n// - specialized for m.arr[3] != 0, hence limited to 2^192 \u003c= m \u003c 2^256\n// - returns zero if m.arr[3] == 0\n// - starts with a 32-bit division, refines with newton-raphson iterations\nfunc Reciprocal(m *Uint) (mu [5]uint64) {\n\tif m.arr[3] == 0 {\n\t\treturn mu\n\t}\n\n\ts := bits.LeadingZeros64(m.arr[3]) // Replace with leadingZeros(m) for general case\n\tp := 255 - s // floor(log_2(m)), m\u003e0\n\n\t// 0 or a power of 2?\n\n\t// Check if at least one bit is set in m.arr[2], m.arr[1] or m.arr[0],\n\t// or at least two bits in m.arr[3]\n\n\tif m.arr[0]|m.arr[1]|m.arr[2]|(m.arr[3]\u0026(m.arr[3]-1)) == 0 {\n\n\t\tmu[4] = ^uint64(0) \u003e\u003e uint(p\u002663)\n\t\tmu[3] = ^uint64(0)\n\t\tmu[2] = ^uint64(0)\n\t\tmu[1] = ^uint64(0)\n\t\tmu[0] = ^uint64(0)\n\n\t\treturn mu\n\t}\n\n\t// Maximise division precision by left-aligning divisor\n\n\tvar (\n\t\ty Uint // left-aligned copy of m\n\t\tr0 uint32 // estimate of 2^31/y\n\t)\n\n\ty.Lsh(m, uint(s)) // 1/2 \u003c y \u003c 1\n\n\t// Extract most significant 32 bits\n\n\tyh := uint32(y.arr[3] \u003e\u003e 32)\n\n\tif yh == 0x80000000 { // Avoid overflow in division\n\t\tr0 = 0xffffffff\n\t} else {\n\t\tr0, _ = bits.Div32(0x80000000, 0, yh)\n\t}\n\n\t// First iteration: 32 -\u003e 64\n\n\tt1 := uint64(r0) // 2^31/y\n\tt1 *= t1 // 2^62/y^2\n\tt1, _ = bits.Mul64(t1, y.arr[3]) // 2^62/y^2 * 2^64/y / 2^64 = 2^62/y\n\n\tr1 := uint64(r0) \u003c\u003c 32 // 2^63/y\n\tr1 -= t1 // 2^63/y - 2^62/y = 2^62/y\n\tr1 *= 2 // 2^63/y\n\n\tif (r1 | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr1 = ^uint64(0)\n\t}\n\n\t// Second iteration: 64 -\u003e 128\n\n\t// square: 2^126/y^2\n\ta2h, a2l := bits.Mul64(r1, r1)\n\n\t// multiply by y: e2h:e2l:b2h = 2^126/y^2 * 2^128/y / 2^128 = 2^126/y\n\tb2h, _ := bits.Mul64(a2l, y.arr[2])\n\tc2h, c2l := bits.Mul64(a2l, y.arr[3])\n\td2h, d2l := bits.Mul64(a2h, y.arr[2])\n\te2h, e2l := bits.Mul64(a2h, y.arr[3])\n\n\tb2h, c := bits.Add64(b2h, c2l, 0)\n\te2l, c = bits.Add64(e2l, c2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t_, c = bits.Add64(b2h, d2l, 0)\n\te2l, c = bits.Add64(e2l, d2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t// subtract: t2h:t2l = 2^127/y - 2^126/y = 2^126/y\n\tt2l, b := bits.Sub64(0, e2l, 0)\n\tt2h, _ := bits.Sub64(r1, e2h, b)\n\n\t// double: r2h:r2l = 2^127/y\n\tr2l, c := bits.Add64(t2l, t2l, 0)\n\tr2h, _ := bits.Add64(t2h, t2h, c)\n\n\tif (r2h | r2l | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr2h = ^uint64(0)\n\t\tr2l = ^uint64(0)\n\t}\n\n\t// Third iteration: 128 -\u003e 192\n\n\t// square r2 (keep 256 bits): 2^190/y^2\n\ta3h, a3l := bits.Mul64(r2l, r2l)\n\tb3h, b3l := bits.Mul64(r2l, r2h)\n\tc3h, c3l := bits.Mul64(r2h, r2h)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\t// multiply by y: q = 2^190/y^2 * 2^192/y / 2^192 = 2^190/y\n\n\tx0 := a3l\n\tx1 := a3h\n\tx2 := c3l\n\tx3 := c3h\n\n\tvar q0, q1, q2, q3, q4, t0 uint64\n\n\tq0, _ = bits.Mul64(x2, y.arr[0])\n\tq1, t0 = bits.Mul64(x3, y.arr[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x1, y.arr[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x3, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x3, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, y.arr[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x3, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\t// subtract: t3 = 2^191/y - 2^190/y = 2^190/y\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\tt3l, b := bits.Sub64(0, q2, b)\n\tt3m, b := bits.Sub64(r2l, q3, b)\n\tt3h, _ := bits.Sub64(r2h, q4, b)\n\n\t// double: r3 = 2^191/y\n\tr3l, c := bits.Add64(t3l, t3l, 0)\n\tr3m, c := bits.Add64(t3m, t3m, c)\n\tr3h, _ := bits.Add64(t3h, t3h, c)\n\n\t// Fourth iteration: 192 -\u003e 320\n\n\t// square r3\n\n\ta4h, a4l := bits.Mul64(r3l, r3l)\n\tb4h, b4l := bits.Mul64(r3l, r3m)\n\tc4h, c4l := bits.Mul64(r3l, r3h)\n\td4h, d4l := bits.Mul64(r3m, r3m)\n\te4h, e4l := bits.Mul64(r3m, r3h)\n\tf4h, f4l := bits.Mul64(r3h, r3h)\n\n\tb4h, c = bits.Add64(b4h, c4l, 0)\n\te4l, c = bits.Add64(e4l, c4h, c)\n\te4h, _ = bits.Add64(e4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\t// multiply by y\n\n\tx1, x0 = bits.Mul64(d4h, y.arr[0])\n\tx3, x2 = bits.Mul64(f4h, y.arr[0])\n\tt1, t0 = bits.Mul64(f4l, y.arr[0])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx3, _ = bits.Add64(x3, 0, c)\n\n\tt1, t0 = bits.Mul64(d4h, y.arr[1])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx4, t0 := bits.Mul64(f4h, y.arr[1])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[1])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[1])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[2])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[2])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx5, t0 := bits.Mul64(f4h, y.arr[2])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[2])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[2])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[3])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[3])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx6, t0 := bits.Mul64(f4h, y.arr[3])\n\tx5, c = bits.Add64(x5, t0, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\tt1, t0 = bits.Mul64(a4l, y.arr[3])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[3])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[3])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, c = bits.Add64(x5, t1, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\n\t// subtract\n\t_, b = bits.Sub64(0, x0, 0)\n\t_, b = bits.Sub64(0, x1, b)\n\tr4l, b := bits.Sub64(0, x2, b)\n\tr4k, b := bits.Sub64(0, x3, b)\n\tr4j, b := bits.Sub64(r3l, x4, b)\n\tr4i, b := bits.Sub64(r3m, x5, b)\n\tr4h, _ := bits.Sub64(r3h, x6, b)\n\n\t// Multiply candidate for 1/4y by y, with full precision\n\n\tx0 = r4l\n\tx1 = r4k\n\tx2 = r4j\n\tx3 = r4i\n\tx4 = r4h\n\n\tq1, q0 = bits.Mul64(x0, y.arr[0])\n\tq3, q2 = bits.Mul64(x2, y.arr[0])\n\tq5, q4 := bits.Mul64(x4, y.arr[0])\n\n\tt1, t0 = bits.Mul64(x1, y.arr[0])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[0])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq6, t0 := bits.Mul64(x4, y.arr[1])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[1])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[1])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq7, t0 := bits.Mul64(x4, y.arr[2])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[2])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq8, t0 := bits.Mul64(x4, y.arr[3])\n\tq7, c = bits.Add64(q7, t0, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq4, c = bits.Add64(q4, t0, 0)\n\tq5, c = bits.Add64(q5, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[3])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, c = bits.Add64(q7, t1, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\t// Final adjustment\n\n\t// subtract q from 1/4\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\t_, b = bits.Sub64(0, q2, b)\n\t_, b = bits.Sub64(0, q3, b)\n\t_, b = bits.Sub64(0, q4, b)\n\t_, b = bits.Sub64(0, q5, b)\n\t_, b = bits.Sub64(0, q6, b)\n\t_, b = bits.Sub64(0, q7, b)\n\t_, b = bits.Sub64(uint64(1)\u003c\u003c62, q8, b)\n\n\t// decrement the result\n\tx0, t := bits.Sub64(r4l, 1, 0)\n\tx1, t = bits.Sub64(r4k, 0, t)\n\tx2, t = bits.Sub64(r4j, 0, t)\n\tx3, t = bits.Sub64(r4i, 0, t)\n\tx4, _ = bits.Sub64(r4h, 0, t)\n\n\t// commit the decrement if the subtraction underflowed (reciprocal was too large)\n\tif b != 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\t// Shift to correct bit alignment, truncating excess bits\n\n\tp = (p \u0026 63) - 1\n\n\tx0, c = bits.Add64(r4l, r4l, 0)\n\tx1, c = bits.Add64(r4k, r4k, c)\n\tx2, c = bits.Add64(r4j, r4j, c)\n\tx3, c = bits.Add64(r4i, r4i, c)\n\tx4, _ = bits.Add64(r4h, r4h, c)\n\n\tif p \u003c 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t\tp = 0 // avoid negative shift below\n\t}\n\n\t{\n\t\tr := uint(p) // right shift\n\t\tl := uint(64 - r) // left shift\n\n\t\tx0 = (r4l \u003e\u003e r) | (r4k \u003c\u003c l)\n\t\tx1 = (r4k \u003e\u003e r) | (r4j \u003c\u003c l)\n\t\tx2 = (r4j \u003e\u003e r) | (r4i \u003c\u003c l)\n\t\tx3 = (r4i \u003e\u003e r) | (r4h \u003c\u003c l)\n\t\tx4 = (r4h \u003e\u003e r)\n\t}\n\n\tif p \u003e 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\tmu[0] = r4l\n\tmu[1] = r4k\n\tmu[2] = r4j\n\tmu[3] = r4i\n\tmu[4] = r4h\n\n\treturn mu\n}\n\n// reduce4 computes the least non-negative residue of x modulo m\n//\n// requires a four-word modulus (m.arr[3] \u003e 1) and its inverse (mu)\nfunc reduce4(x [8]uint64, m *Uint, mu [5]uint64) (z Uint) {\n\t// NB: Most variable names in the comments match the pseudocode for\n\t// \tBarrett reduction in the Handbook of Applied Cryptography.\n\n\t// q1 = x/2^192\n\n\tx0 := x[3]\n\tx1 := x[4]\n\tx2 := x[5]\n\tx3 := x[6]\n\tx4 := x[7]\n\n\t// q2 = q1 * mu; q3 = q2 / 2^320\n\n\tvar q0, q1, q2, q3, q4, q5, t0, t1, c uint64\n\n\tq0, _ = bits.Mul64(x3, mu[0])\n\tq1, t0 = bits.Mul64(x4, mu[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x2, mu[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x4, mu[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x3, mu[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, mu[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x4, mu[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x1, mu[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x3, mu[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, mu[3])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, mu[3])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x4, mu[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, mu[4])\n\t_, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, mu[4])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq5, t0 = bits.Mul64(x4, mu[4])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[4])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[4])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\t// Drop the fractional part of q3\n\n\tq0 = q1\n\tq1 = q2\n\tq2 = q3\n\tq3 = q4\n\tq4 = q5\n\n\t// r1 = x mod 2^320\n\n\tx0 = x[0]\n\tx1 = x[1]\n\tx2 = x[2]\n\tx3 = x[3]\n\tx4 = x[4]\n\n\t// r2 = q3 * m mod 2^320\n\n\tvar r0, r1, r2, r3, r4 uint64\n\n\tr4, r3 = bits.Mul64(q0, m.arr[3])\n\t_, t0 = bits.Mul64(q1, m.arr[3])\n\tr4, _ = bits.Add64(r4, t0, 0)\n\n\tt1, r2 = bits.Mul64(q0, m.arr[2])\n\tr3, c = bits.Add64(r3, t1, 0)\n\t_, t0 = bits.Mul64(q2, m.arr[2])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[2])\n\tr3, c = bits.Add64(r3, t0, 0)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, r1 = bits.Mul64(q0, m.arr[1])\n\tr2, c = bits.Add64(r2, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[1])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[1])\n\tr2, c = bits.Add64(r2, t0, 0)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q3, m.arr[1])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, r0 = bits.Mul64(q0, m.arr[0])\n\tr1, c = bits.Add64(r1, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[0])\n\tr2, c = bits.Add64(r2, t0, c)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q4, m.arr[0])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[0])\n\tr1, c = bits.Add64(r1, t0, 0)\n\tr2, c = bits.Add64(r2, t1, c)\n\tt1, t0 = bits.Mul64(q3, m.arr[0])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\t// r = r1 - r2\n\n\tvar b uint64\n\n\tr0, b = bits.Sub64(x0, r0, 0)\n\tr1, b = bits.Sub64(x1, r1, b)\n\tr2, b = bits.Sub64(x2, r2, b)\n\tr3, b = bits.Sub64(x3, r3, b)\n\tr4, b = bits.Sub64(x4, r4, b)\n\n\t// if r\u003c0 then r+=m\n\n\tif b != 0 {\n\t\tr0, c = bits.Add64(r0, m.arr[0], 0)\n\t\tr1, c = bits.Add64(r1, m.arr[1], c)\n\t\tr2, c = bits.Add64(r2, m.arr[2], c)\n\t\tr3, c = bits.Add64(r3, m.arr[3], c)\n\t\tr4, _ = bits.Add64(r4, 0, c)\n\t}\n\n\t// while (r\u003e=m) r-=m\n\n\tfor {\n\t\t// q = r - m\n\t\tq0, b = bits.Sub64(r0, m.arr[0], 0)\n\t\tq1, b = bits.Sub64(r1, m.arr[1], b)\n\t\tq2, b = bits.Sub64(r2, m.arr[2], b)\n\t\tq3, b = bits.Sub64(r3, m.arr[3], b)\n\t\tq4, b = bits.Sub64(r4, 0, b)\n\n\t\t// if borrow break\n\t\tif b != 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// r = q\n\t\tr4, r3, r2, r1, r0 = q4, q3, q2, q1, q0\n\t}\n\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = r3, r2, r1, r0\n\n\treturn z\n}\n" + }, + { + "name": "uint256.gno", + "body": "// Ported from https://github.com/holiman/uint256\n// This package provides a 256-bit unsigned integer type, Uint256, and associated functions.\npackage uint256\n\nimport (\n\t\"errors\"\n\t\"math/bits\"\n\t\"strconv\"\n)\n\nconst (\n\tMaxUint64 = 1\u003c\u003c64 - 1\n\tuintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63)\n)\n\n// Uint is represented as an array of 4 uint64, in little-endian order,\n// so that Uint[3] is the most significant, and Uint[0] is the least significant\ntype Uint struct {\n\tarr [4]uint64\n}\n\n// NewUint returns a new initialized Uint.\nfunc NewUint(val uint64) *Uint {\n\tz := \u0026Uint{arr: [4]uint64{val, 0, 0, 0}}\n\treturn z\n}\n\n// Zero returns a new Uint initialized to zero.\nfunc Zero() *Uint {\n\treturn NewUint(0)\n}\n\n// One returns a new Uint initialized to one.\nfunc One() *Uint {\n\treturn NewUint(1)\n}\n\n// SetAllOne sets all the bits of z to 1\nfunc (z *Uint) SetAllOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, MaxUint64\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Uint) Set(x *Uint) *Uint {\n\t*z = *x\n\n\treturn z\n}\n\n// SetOne sets z to 1\nfunc (z *Uint) SetOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 1\n\treturn z\n}\n\nconst twoPow256Sub1 = \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n\n// SetFromDecimal sets z from the given string, interpreted as a decimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Uint).SetString(..., 10) method.\n// Notable differences:\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0\",\n// - (this method does not accept any negative input as valid))\nfunc (z *Uint) SetFromDecimal(s string) (err error) {\n\t// Remove max one leading +\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '+' {\n\t\ts = s[1:]\n\t}\n\t// Remove any number of leading zeroes\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '0' {\n\t\tvar i int\n\t\tvar c rune\n\t\tfor i, c = range s {\n\t\t\tif c != '0' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ts = s[i:]\n\t}\n\tif len(s) \u003c len(twoPow256Sub1) {\n\t\treturn z.fromDecimal(s)\n\t}\n\tif len(s) == len(twoPow256Sub1) {\n\t\tif s \u003e twoPow256Sub1 {\n\t\t\treturn ErrBig256Range\n\t\t}\n\t\treturn z.fromDecimal(s)\n\t}\n\treturn ErrBig256Range\n}\n\n// FromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string. Numbers larger than 256 bits are not accepted.\nfunc FromDecimal(decimal string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromDecimal(decimal string) *Uint {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// multipliers holds the values that are needed for fromDecimal\nvar multipliers = [5]*Uint{\n\tnil, // represents first round, no multiplication needed\n\t{[4]uint64{10000000000000000000, 0, 0, 0}}, // 10 ^ 19\n\t{[4]uint64{687399551400673280, 5421010862427522170, 0, 0}}, // 10 ^ 38\n\t{[4]uint64{5332261958806667264, 17004971331911604867, 2938735877055718769, 0}}, // 10 ^ 57\n\t{[4]uint64{0, 8607968719199866880, 532749306367912313, 1593091911132452277}}, // 10 ^ 76\n}\n\n// fromDecimal is a helper function to only ever be called via SetFromDecimal\n// this function takes a string and chunks it up, calling ParseUint on it up to 5 times\n// these chunks are then multiplied by the proper power of 10, then added together.\nfunc (z *Uint) fromDecimal(bs string) error {\n\t// first clear the input\n\tz.Clear()\n\t// the maximum value of uint64 is 18446744073709551615, which is 20 characters\n\t// one less means that a string of 19 9's is always within the uint64 limit\n\tvar (\n\t\tnum uint64\n\t\terr error\n\t\tremaining = len(bs)\n\t)\n\tif remaining == 0 {\n\t\treturn errors.New(\"EOF\")\n\t}\n\t// We proceed in steps of 19 characters (nibbles), from least significant to most significant.\n\t// This means that the first (up to) 19 characters do not need to be multiplied.\n\t// In the second iteration, our slice of 19 characters needs to be multipleied\n\t// by a factor of 10^19. Et cetera.\n\tfor i, mult := range multipliers {\n\t\tif remaining \u003c= 0 {\n\t\t\treturn nil // Done\n\t\t} else if remaining \u003e 19 {\n\t\t\tnum, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64)\n\t\t} else {\n\t\t\t// Final round\n\t\t\tnum, err = strconv.ParseUint(bs, 10, 64)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// add that number to our running total\n\t\tif i == 0 {\n\t\t\tz.SetUint64(num)\n\t\t} else {\n\t\t\tbase := NewUint(num)\n\t\t\tz.Add(z, base.Mul(base, mult))\n\t\t}\n\t\t// Chop off another 19 characters\n\t\tif remaining \u003e 19 {\n\t\t\tbs = bs[0 : remaining-19]\n\t\t}\n\t\tremaining -= 19\n\t}\n\treturn nil\n}\n\n// Byte sets z to the value of the byte at position n,\n// with 'z' considered as a big-endian 32-byte integer\n// if 'n' \u003e 32, f is set to 0\n// Example: f = '5', n=31 =\u003e 5\nfunc (z *Uint) Byte(n *Uint) *Uint {\n\t// in z, z.arr[0] is the least significant\n\tif number, overflow := n.Uint64WithOverflow(); !overflow {\n\t\tif number \u003c 32 {\n\t\t\tnumber := z.arr[4-1-number/8]\n\t\t\toffset := (n.arr[0] \u0026 0x7) \u003c\u003c 3 // 8*(n.d % 8)\n\t\t\tz.arr[0] = (number \u0026 (0xff00000000000000 \u003e\u003e offset)) \u003e\u003e (56 - offset)\n\t\t\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\t\t\treturn z\n\t\t}\n\t}\n\n\treturn z.Clear()\n}\n\n// BitLen returns the number of bits required to represent z\nfunc (z *Uint) BitLen() int {\n\tswitch {\n\tcase z.arr[3] != 0:\n\t\treturn 192 + bits.Len64(z.arr[3])\n\tcase z.arr[2] != 0:\n\t\treturn 128 + bits.Len64(z.arr[2])\n\tcase z.arr[1] != 0:\n\t\treturn 64 + bits.Len64(z.arr[1])\n\tdefault:\n\t\treturn bits.Len64(z.arr[0])\n\t}\n}\n\n// ByteLen returns the number of bytes required to represent z\nfunc (z *Uint) ByteLen() int {\n\treturn (z.BitLen() + 7) / 8\n}\n\n// Clear sets z to 0\nfunc (z *Uint) Clear() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 0\n\treturn z\n}\n\nconst (\n\t// hextable = \"0123456789abcdef\"\n\tbintable = \"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\a\\b\\t\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n\tbadNibble = 0xff\n)\n\n// SetFromHex sets z from the given string, interpreted as a hexadecimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Int).SetString(..., 16) method.\n// Notable differences:\n// - This method _require_ \"0x\" or \"0X\" prefix.\n// - This method does not accept zero-prefixed hex, e.g. \"0x0001\"\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0x0\",\n// - (this method does not accept any negative input as valid)\nfunc (z *Uint) SetFromHex(hex string) error {\n\treturn z.fromHex(hex)\n}\n\n// fromHex is the internal implementation of parsing a hex-string.\nfunc (z *Uint) fromHex(hex string) error {\n\tif err := checkNumberS(hex); err != nil {\n\t\treturn err\n\t}\n\tif len(hex) \u003e 66 {\n\t\treturn ErrBig256Range\n\t}\n\tz.Clear()\n\tend := len(hex)\n\tfor i := 0; i \u003c 4; i++ {\n\t\tstart := end - 16\n\t\tif start \u003c 2 {\n\t\t\tstart = 2\n\t\t}\n\t\tfor ri := start; ri \u003c end; ri++ {\n\t\t\tnib := bintable[hex[ri]]\n\t\t\tif nib == badNibble {\n\t\t\t\treturn ErrSyntax\n\t\t\t}\n\t\t\tz.arr[i] = z.arr[i] \u003c\u003c 4\n\t\t\tz.arr[i] += uint64(nib)\n\t\t}\n\t\tend = start\n\t}\n\treturn nil\n}\n\n// FromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string. The string is required to be '0x'-prefixed\n// Numbers larger than 256 bits are not accepted.\nfunc FromHex(hex string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromHex(hex string) *Uint {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// Clone creates a new Uint identical to z\nfunc (z *Uint) Clone() *Uint {\n\tvar x Uint\n\tx.arr[0] = z.arr[0]\n\tx.arr[1] = z.arr[1]\n\tx.arr[2] = z.arr[2]\n\tx.arr[3] = z.arr[3]\n\n\treturn \u0026x\n}\n" + }, + { + "name": "uint256_test.gno", + "body": "package uint256\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSetAllOne(t *testing.T) {\n\tz := Zero()\n\tz.SetAllOne()\n\tif z.ToString() != twoPow256Sub1 {\n\t\tt.Errorf(\"Expected all ones, got %s\", z.ToString())\n\t}\n}\n\nfunc TestByte(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\tposition uint64\n\t\texpected byte\n\t}{\n\t\t{\"0x1000000000000000000000000000000000000000000000000000000000000000\", 0, 16},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 0, 255},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 31, 255},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tn := NewUint(tt.position)\n\t\tresult := z.Byte(n)\n\n\t\tif result.arr[0] != uint64(tt.expected) {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Position: %d, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.position, tt.expected, result.arr[0])\n\t\t}\n\n\t\t// check other array elements are 0\n\t\tif result.arr[1] != 0 || result.arr[2] != 0 || result.arr[3] != 0 {\n\t\t\tt.Errorf(\"Test case %d failed. Non-zero values in upper bytes\", i)\n\t\t}\n\t}\n\n\t// overflow\n\tz, _ := FromHex(\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\")\n\tn := NewUint(32)\n\tresult := z.Byte(n)\n\n\tif !result.IsZero() {\n\t\tt.Errorf(\"Expected zero for position \u003e= 32, got %v\", result)\n\t}\n}\n\nfunc TestBitLen(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"0x0\", 0},\n\t\t{\"0x1\", 1},\n\t\t{\"0xff\", 8},\n\t\t{\"0x100\", 9},\n\t\t{\"0xffff\", 16},\n\t\t{\"0x10000\", 17},\n\t\t{\"0xffffffffffffffff\", 64},\n\t\t{\"0x10000000000000000\", 65},\n\t\t{\"0xffffffffffffffffffffffffffffffff\", 128},\n\t\t{\"0x100000000000000000000000000000000\", 129},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 256},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.BitLen()\n\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.expected, result)\n\t\t}\n\t}\n}\n\nfunc TestByteLen(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"0x0\", 0},\n\t\t{\"0x1\", 1},\n\t\t{\"0xff\", 1},\n\t\t{\"0x100\", 2},\n\t\t{\"0xffff\", 2},\n\t\t{\"0x10000\", 3},\n\t\t{\"0xffffffffffffffff\", 8},\n\t\t{\"0x10000000000000000\", 9},\n\t\t{\"0xffffffffffffffffffffffffffffffff\", 16},\n\t\t{\"0x100000000000000000000000000000000\", 17},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 32},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.ByteLen()\n\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.expected, result)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t}{\n\t\t{\"0x1\", \"1\"},\n\t\t{\"0x100\", \"256\"},\n\t\t{\"0x10000000000000000\", \"18446744073709551616\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.Clone()\n\t\tif result.ToString() != tt.expected {\n\t\t\tt.Errorf(\"Test %s failed. Expected %s, got %s\", tt.input, tt.expected, result.ToString())\n\t\t}\n\t}\n}\n" + }, + { + "name": "utils.gno", + "body": "package uint256\n\nfunc checkNumberS(input string) error {\n\tconst fn = \"UnmarshalText\"\n\tl := len(input)\n\tif l == 0 {\n\t\treturn errEmptyString(fn, input)\n\t}\n\tif l \u003c 2 || input[0] != '0' ||\n\t\t(input[1] != 'x' \u0026\u0026 input[1] != 'X') {\n\t\treturn errMissingPrefix(fn, input)\n\t}\n\tif l == 2 {\n\t\treturn errEmptyNumber(fn, input)\n\t}\n\tif len(input) \u003e 3 \u0026\u0026 input[2] == '0' {\n\t\treturn errLeadingZero(fn, input)\n\t}\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "int256", + "path": "gno.land/p/demo/int256", + "files": [ + { + "name": "LICENSE", + "body": "MIT License\n\nCopyright (c) 2023 Trịnh Đức Bảo Linh(Kevin)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE." + }, + { + "name": "README.md", + "body": "# Fixed size signed 256-bit math library\n\n1. This is a library specialized at replacing the big.Int library for math based on signed 256-bit types.\n2. It uses [uint256](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/uint256) as the underlying type.\n\nported from [mempooler/int256](https://github.com/mempooler/int256)\n" + }, + { + "name": "absolute.gno", + "body": "package int256\n\nimport \"gno.land/p/demo/uint256\"\n\n// Abs returns |z|\nfunc (z *Int) Abs() *uint256.Uint {\n\treturn z.abs.Clone()\n}\n\n// AbsGt returns true if |z| \u003e x, where x is a uint256\nfunc (z *Int) AbsGt(x *uint256.Uint) bool {\n\treturn z.abs.Gt(x)\n}\n\n// AbsLt returns true if |z| \u003c x, where x is a uint256\nfunc (z *Int) AbsLt(x *uint256.Uint) bool {\n\treturn z.abs.Lt(x)\n}\n" + }, + { + "name": "absolute_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestAbs(t *testing.T) {\n\ttests := []struct {\n\t\tx, want string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"1\"},\n\t\t{\"-2\", \"2\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Abs()\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"Abs(%s) = %v, want %v\", tc.x, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n\nfunc TestAbsGt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"false\"},\n\t\t{\"1\", \"0\", \"true\"},\n\t\t{\"-1\", \"0\", \"true\"},\n\t\t{\"-1\", \"1\", \"false\"},\n\t\t{\"-2\", \"1\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"false\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.AbsGt(y)\n\n\t\tif got != (tc.want == \"true\") {\n\t\t\tt.Errorf(\"AbsGt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestAbsLt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"false\"},\n\t\t{\"1\", \"0\", \"false\"},\n\t\t{\"-1\", \"0\", \"false\"},\n\t\t{\"-1\", \"1\", \"false\"},\n\t\t{\"-2\", \"1\", \"false\"},\n\t\t{\"-5\", \"10\", \"true\"},\n\t\t{\"31330\", \"31337\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\", \"false\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"false\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"false\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.AbsLt(y)\n\n\t\tif got != (tc.want == \"true\") {\n\t\t\tt.Errorf(\"AbsLt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n" + }, + { + "name": "arithmetic.gno", + "body": "package int256\n\nimport \"gno.land/p/demo/uint256\"\n\nfunc (z *Int) Add(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tif x.neg == y.neg {\n\t\t// If both numbers have the same sign, add their absolute values\n\t\tz.abs.Add(x.abs, y.abs)\n\t\tz.neg = x.neg\n\t} else {\n\t\tswitch x.abs.Cmp(y.abs) {\n\t\tcase 1: // x \u003e y\n\t\t\tz.abs.Sub(x.abs, y.abs)\n\t\t\tz.neg = x.neg\n\t\tcase -1: // x \u003c y\n\t\t\tz.abs.Sub(y.abs, x.abs)\n\t\t\tz.neg = y.neg\n\t\tcase 0: // x == y\n\t\t\tz.abs = uint256.NewUint(0)\n\t\t}\n\t}\n\n\treturn z\n}\n\n// AddUint256 set z to the sum x + y, where y is a uint256, and returns z\nfunc (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int {\n\tif x.neg {\n\t\tif x.abs.Gt(y) {\n\t\t\tz.abs.Sub(x.abs, y)\n\t\t\tz.neg = true\n\t\t} else {\n\t\t\tz.abs.Sub(y, x.abs)\n\t\t\tz.neg = false\n\t\t}\n\t} else {\n\t\tz.abs.Add(x.abs, y)\n\t\tz.neg = false\n\t}\n\treturn z\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\nfunc AddDelta(z, x *uint256.Uint, y *Int) {\n\tif y.neg {\n\t\tz.Sub(x, y.abs)\n\t} else {\n\t\tz.Add(x, y.abs)\n\t}\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\nfunc AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool {\n\tvar overflow bool\n\tif y.neg {\n\t\t_, overflow = z.SubOverflow(x, y.abs)\n\t} else {\n\t\t_, overflow = z.AddOverflow(x, y.abs)\n\t}\n\treturn overflow\n}\n\n// Sub sets z to the difference x-y and returns z.\nfunc (z *Int) Sub(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tif x.neg != y.neg {\n\t\t// If sign are different, add the absolute values\n\t\tz.abs.Add(x.abs, y.abs)\n\t\tz.neg = x.neg\n\t} else {\n\t\tswitch x.abs.Cmp(y.abs) {\n\t\tcase 1: // x \u003e y\n\t\t\tz.abs.Sub(x.abs, y.abs)\n\t\t\tz.neg = x.neg\n\t\tcase -1: // x \u003c y\n\t\t\tz.abs.Sub(y.abs, x.abs)\n\t\t\tz.neg = !x.neg\n\t\tcase 0: // x == y\n\t\t\tz.abs = uint256.NewUint(0)\n\t\t}\n\t}\n\n\t// Ensure zero is always positive\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t}\n\treturn z\n}\n\n// SubUint256 set z to the difference x - y, where y is a uint256, and returns z\nfunc (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int {\n\tif x.neg {\n\t\tz.abs.Add(x.abs, y)\n\t\tz.neg = true\n\t} else {\n\t\tif x.abs.Lt(y) {\n\t\t\tz.abs.Sub(y, x.abs)\n\t\t\tz.neg = true\n\t\t} else {\n\t\t\tz.abs.Sub(x.abs, y)\n\t\t\tz.neg = false\n\t\t}\n\t}\n\treturn z\n}\n\n// Mul sets z to the product x*y and returns z.\nfunc (z *Int) Mul(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tz.abs = z.abs.Mul(x.abs, y.abs)\n\tz.neg = x.neg != y.neg \u0026\u0026 !z.abs.IsZero() // 0 has no sign\n\treturn z\n}\n\n// MulUint256 sets z to the product x*y, where y is a uint256, and returns z\nfunc (z *Int) MulUint256(x *Int, y *uint256.Uint) *Int {\n\tz.abs.Mul(x.abs, y)\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t} else {\n\t\tz.neg = x.neg\n\t}\n\treturn z\n}\n\n// Div sets z to the quotient x/y for y != 0 and returns z.\nfunc (z *Int) Div(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tif y.abs.IsZero() {\n\t\tpanic(\"division by zero\")\n\t}\n\n\tz.abs.Div(x.abs, y.abs)\n\tz.neg = (x.neg != y.neg) \u0026\u0026 !z.abs.IsZero() // 0 has no sign\n\n\treturn z\n}\n\n// DivUint256 sets z to the quotient x/y, where y is a uint256, and returns z\n// If y == 0, z is set to 0\nfunc (z *Int) DivUint256(x *Int, y *uint256.Uint) *Int {\n\tz.abs.Div(x.abs, y)\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t} else {\n\t\tz.neg = x.neg\n\t}\n\treturn z\n}\n\n// Quo sets z to the quotient x/y for y != 0 and returns z.\n// If y == 0, a division-by-zero run-time panic occurs.\n// OBS: differs from mempooler int256, we need to panic manually if y == 0\n// Quo implements truncated division (like Go); see QuoRem for more details.\nfunc (z *Int) Quo(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(\"division by zero\")\n\t}\n\n\tz.initiateAbs()\n\n\tz.abs = z.abs.Div(x.abs, y.abs)\n\tz.neg = !(z.abs.IsZero()) \u0026\u0026 x.neg != y.neg // 0 has no sign\n\treturn z\n}\n\n// Rem sets z to the remainder x%y for y != 0 and returns z.\n// If y == 0, a division-by-zero run-time panic occurs.\n// OBS: differs from mempooler int256, we need to panic manually if y == 0\n// Rem implements truncated modulus (like Go); see QuoRem for more details.\nfunc (z *Int) Rem(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(\"division by zero\")\n\t}\n\n\tz.initiateAbs()\n\n\tz.abs.Mod(x.abs, y.abs)\n\tz.neg = z.abs.Sign() \u003e 0 \u0026\u0026 x.neg // 0 has no sign\n\treturn z\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// If y == 0, z is set to 0 (OBS: differs from the big.Int)\nfunc (z *Int) Mod(x, y *Int) *Int {\n\tif x.neg {\n\t\tz.abs.Div(x.abs, y.abs)\n\t\tz.abs.Add(z.abs, one)\n\t\tz.abs.Mul(z.abs, y.abs)\n\t\tz.abs.Sub(z.abs, x.abs)\n\t\tz.abs.Mod(z.abs, y.abs)\n\t} else {\n\t\tz.abs.Mod(x.abs, y.abs)\n\t}\n\tz.neg = false\n\treturn z\n}\n" + }, + { + "name": "arithmetic_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestAdd(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\"},\n\t\t// NEGATIVE\n\t\t{\"-1\", \"1\", \"0\"},\n\t\t{\"1\", \"-1\", \"0\"},\n\t\t{\"3\", \"-3\", \"0\"},\n\t\t{\"-1\", \"-1\", \"-2\"},\n\t\t{\"-1\", \"-2\", \"-3\"},\n\t\t{\"-1\", \"3\", \"2\"},\n\t\t{\"3\", \"-1\", \"2\"},\n\t\t// OVERFLOW\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Add(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Add(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\"},\n\t\t{\"-1\", \"1\", \"0\"},\n\t\t{\"-1\", \"3\", \"2\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"-1\"},\n\t\t// OVERFLOW\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.AddUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"AddUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddDelta(t *testing.T) {\n\ttests := []struct {\n\t\tz, x, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"0\", \"0\"},\n\t\t{\"0\", \"0\", \"1\", \"1\"},\n\t\t{\"0\", \"1\", \"0\", \"1\"},\n\t\t{\"0\", \"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\", \"5\"},\n\t\t{\"5\", \"10\", \"-3\", \"7\"},\n\t\t// underflow\n\t\t{\"1\", \"2\", \"-3\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz, err := uint256.FromDecimal(tc.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tx, err := uint256.FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := uint256.FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tAddDelta(z, x, y)\n\n\t\tif z.Neq(want) {\n\t\t\tt.Errorf(\"AddDelta(%s, %s, %s) = %v, want %v\", tc.z, tc.x, tc.y, z.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddDeltaOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tz, x, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", \"0\", false},\n\t\t// underflow\n\t\t{\"1\", \"2\", \"-3\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz, err := uint256.FromDecimal(tc.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tx, err := uint256.FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tresult := AddDeltaOverflow(z, x, y)\n\t\tif result != tc.want {\n\t\t\tt.Errorf(\"AddDeltaOverflow(%s, %s, %s) = %v, want %v\", tc.z, tc.x, tc.y, result, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSub(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"-1\", \"1\", \"-2\"},\n\t\t{\"1\", \"-1\", \"2\"},\n\t\t{\"-1\", \"-1\", \"0\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{x: \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", y: \"1\", want: \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Sub(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Sub(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestSubUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"-1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"1\", \"2\", \"-1\"},\n\t\t{\"-1\", \"1\", \"-2\"},\n\t\t{\"-1\", \"3\", \"-4\"},\n\t\t// underflow\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"-0\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"-1\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"3\", \"-2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.SubUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"SubUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMul(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"5\", \"3\", \"15\"},\n\t\t{\"-5\", \"3\", \"-15\"},\n\t\t{\"5\", \"-3\", \"-15\"},\n\t\t{\"0\", \"3\", \"0\"},\n\t\t{\"3\", \"0\", \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Mul(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mul(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMulUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1\", \"2\", \"2\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"-1\", \"3\", \"-3\"},\n\t\t{\"3\", \"4\", \"12\"},\n\t\t{\"-3\", \"4\", \"-12\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639932\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"115792089237316195423570985008687907853269984665640564039457584007913129639932\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.MulUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"MulUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestDiv(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, expected string\n\t}{\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"1\", \"-1\", \"-1\"},\n\t\t{\"-1\", \"-1\", \"1\"},\n\t\t{\"-6\", \"3\", \"-2\"},\n\t\t{\"10\", \"-2\", \"-5\"},\n\t\t{\"-10\", \"3\", \"-3\"},\n\t\t{\"7\", \"3\", \"2\"},\n\t\t{\"-7\", \"3\", \"-2\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"}, // Max uint256 / 2\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.x+\"/\"+tt.y, func(t *testing.T) {\n\t\t\tx := MustFromDecimal(tt.x)\n\t\t\ty := MustFromDecimal(tt.y)\n\t\t\tresult := Zero().Div(x, y)\n\t\t\tif result.ToString() != tt.expected {\n\t\t\t\tt.Errorf(\"Div(%s, %s) = %s, want %s\", tt.x, tt.y, result.ToString(), tt.expected)\n\t\t\t}\n\t\t\tif result.abs.IsZero() \u0026\u0026 result.neg {\n\t\t\t\tt.Errorf(\"Div(%s, %s) resulted in negative zero\", tt.x, tt.y)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Division by zero\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"Div(1, 0) did not panic\")\n\t\t\t}\n\t\t}()\n\t\tx := MustFromDecimal(\"1\")\n\t\ty := MustFromDecimal(\"0\")\n\t\tZero().Div(x, y)\n\t})\n}\n\nfunc TestDivUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1\", \"2\", \"0\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"-1\", \"3\", \"0\"},\n\t\t{\"4\", \"3\", \"1\"},\n\t\t{\"25\", \"5\", \"5\"},\n\t\t{\"25\", \"4\", \"6\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"-57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.DivUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"DivUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestQuo(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"10\"},\n\t\t{\"10\", \"-1\", \"-10\"},\n\t\t{\"-10\", \"1\", \"-10\"},\n\t\t{\"-10\", \"-1\", \"10\"},\n\t\t{\"10\", \"-3\", \"-3\"},\n\t\t{\"10\", \"3\", \"3\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Quo(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Quo(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestRem(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"0\"},\n\t\t{\"10\", \"-1\", \"0\"},\n\t\t{\"-10\", \"1\", \"0\"},\n\t\t{\"-10\", \"-1\", \"0\"},\n\t\t{\"10\", \"3\", \"1\"},\n\t\t{\"10\", \"-3\", \"1\"},\n\t\t{\"-10\", \"3\", \"-1\"},\n\t\t{\"-10\", \"-3\", \"-1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Rem(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rem(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMod(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"0\", \"0\"},\n\t\t{\"10\", \"1\", \"0\"},\n\t\t{\"10\", \"-1\", \"0\"},\n\t\t{\"-10\", \"0\", \"0\"},\n\t\t{\"-10\", \"1\", \"0\"},\n\t\t{\"-10\", \"-1\", \"0\"},\n\t\t{\"10\", \"3\", \"1\"},\n\t\t{\"10\", \"-3\", \"1\"},\n\t\t{\"-10\", \"3\", \"2\"},\n\t\t{\"-10\", \"-3\", \"2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Mod(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n" + }, + { + "name": "bitwise.gno", + "body": "package int256\n\nimport (\n\t\"gno.land/p/demo/uint256\"\n)\n\n// Or sets z = x | y and returns z.\nfunc (z *Int) Or(x, y *Int) *Int {\n\tif x.neg == y.neg {\n\t\tif x.neg {\n\t\t\t// (-x) | (-y) == ^(x-1) | ^(y-1) == ^((x-1) \u0026 (y-1)) == -(((x-1) \u0026 (y-1)) + 1)\n\t\t\tx1 := new(uint256.Uint).Sub(x.abs, one)\n\t\t\ty1 := new(uint256.Uint).Sub(y.abs, one)\n\t\t\tz.abs = z.abs.Add(z.abs.And(x1, y1), one)\n\t\t\tz.neg = true // z cannot be zero if x and y are negative\n\t\t\treturn z\n\t\t}\n\n\t\t// x | y == x | y\n\t\tz.abs = z.abs.Or(x.abs, y.abs)\n\t\tz.neg = false\n\t\treturn z\n\t}\n\n\t// x.neg != y.neg\n\tif x.neg {\n\t\tx, y = y, x // | is symmetric\n\t}\n\n\t// x | (-y) == x | ^(y-1) == ^((y-1) \u0026^ x) == -(^((y-1) \u0026^ x) + 1)\n\ty1 := new(uint256.Uint).Sub(y.abs, one)\n\tz.abs = z.abs.Add(z.abs.AndNot(y1, x.abs), one)\n\tz.neg = true // z cannot be zero if one of x or y is negative\n\n\treturn z\n}\n\n// And sets z = x \u0026 y and returns z.\nfunc (z *Int) And(x, y *Int) *Int {\n\tif x.neg == y.neg {\n\t\tif x.neg {\n\t\t\t// (-x) \u0026 (-y) == ^(x-1) \u0026 ^(y-1) == ^((x-1) | (y-1)) == -(((x-1) | (y-1)) + 1)\n\t\t\tx1 := new(uint256.Uint).Sub(x.abs, one)\n\t\t\ty1 := new(uint256.Uint).Sub(y.abs, one)\n\t\t\tz.abs = z.abs.Add(z.abs.Or(x1, y1), one)\n\t\t\tz.neg = true // z cannot be zero if x and y are negative\n\t\t\treturn z\n\t\t}\n\n\t\t// x \u0026 y == x \u0026 y\n\t\tz.abs = z.abs.And(x.abs, y.abs)\n\t\tz.neg = false\n\t\treturn z\n\t}\n\n\t// x.neg != y.neg\n\t// REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1192-1202;drc=d57303e65f00b84b528ee682747dbe1fd3316d30\n\tif x.neg {\n\t\tx, y = y, x // \u0026 is symmetric\n\t}\n\n\t// x \u0026 (-y) == x \u0026 ^(y-1) == x \u0026^ (y-1)\n\ty1 := new(uint256.Uint).Sub(y.abs, uint256.One())\n\tz.abs = z.abs.AndNot(x.abs, y1)\n\tz.neg = false\n\treturn z\n}\n\n// Rsh sets z = x \u003e\u003e n and returns z.\n// OBS: Different from original implementation it was using math.Big\nfunc (z *Int) Rsh(x *Int, n uint) *Int {\n\tif !x.neg {\n\t\tz.abs.Rsh(x.abs, n)\n\t\tz.neg = x.neg\n\t\treturn z\n\t}\n\n\t// REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1118-1126;drc=d57303e65f00b84b528ee682747dbe1fd3316d30\n\tt := NewInt(0).Sub(FromUint256(x.abs), NewInt(1))\n\tt = t.Rsh(t, n)\n\n\t_tmp := t.Add(t, NewInt(1))\n\tz.abs = _tmp.Abs()\n\tz.neg = true\n\n\treturn z\n}\n\n// Lsh sets z = x \u003c\u003c n and returns z.\nfunc (z *Int) Lsh(x *Int, n uint) *Int {\n\tz.abs.Lsh(x.abs, n)\n\tz.neg = x.neg\n\treturn z\n}\n" + }, + { + "name": "bitwise_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestOr(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx, y, want Int\n\t}{\n\t\t{\n\t\t\tname: \"all zeroes\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := New()\n\t\t\tgot.Or(\u0026tc.x, \u0026tc.y)\n\n\t\t\tif got.Neq(\u0026tc.want) {\n\t\t\t\tt.Errorf(\"Or(%v, %v) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAnd(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx, y, want Int\n\t}{\n\t\t{\n\t\t\tname: \"all zeroes\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := New()\n\t\t\tgot.And(\u0026tc.x, \u0026tc.y)\n\n\t\t\tif got.Neq(\u0026tc.want) {\n\t\t\t\tt.Errorf(\"And(%v, %v) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\tn uint\n\t\twant string\n\t}{\n\t\t{\"1024\", 0, \"1024\"},\n\t\t{\"1024\", 1, \"512\"},\n\t\t{\"1024\", 2, \"256\"},\n\t\t{\"1024\", 10, \"1\"},\n\t\t{\"1024\", 11, \"0\"},\n\t\t{\"18446744073709551615\", 0, \"18446744073709551615\"},\n\t\t{\"18446744073709551615\", 1, \"9223372036854775807\"},\n\t\t{\"18446744073709551615\", 62, \"3\"},\n\t\t{\"18446744073709551615\", 63, \"1\"},\n\t\t{\"18446744073709551615\", 64, \"0\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 0, \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 1, \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 128, \"340282366920938463463374607431768211455\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 255, \"1\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 256, \"0\"},\n\t\t{\"-1024\", 0, \"-1024\"},\n\t\t{\"-1024\", 1, \"-512\"},\n\t\t{\"-1024\", 2, \"-256\"},\n\t\t{\"-1024\", 10, \"-1\"},\n\t\t{\"-1024\", 10, \"-1\"},\n\t\t{\"-9223372036854775808\", 0, \"-9223372036854775808\"},\n\t\t{\"-9223372036854775808\", 1, \"-4611686018427387904\"},\n\t\t{\"-9223372036854775808\", 62, \"-2\"},\n\t\t{\"-9223372036854775808\", 63, \"-1\"},\n\t\t{\"-9223372036854775808\", 64, \"-1\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 0, \"-57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 1, \"-28948022309329048855892746252171976963317496166410141009864396001978282409984\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 253, \"-4\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 254, \"-2\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 255, \"-1\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 256, \"-1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Rsh(x, tc.n)\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"Rsh(%s, %d) = %v, want %v\", tc.x, tc.n, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n\nfunc TestLsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\tn uint\n\t\twant string\n\t}{\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"2\"},\n\t\t{\"1\", 2, \"4\"},\n\t\t{\"2\", 0, \"2\"},\n\t\t{\"2\", 1, \"4\"},\n\t\t{\"2\", 2, \"8\"},\n\t\t{\"-2\", 0, \"-2\"},\n\t\t{\"-4\", 0, \"-4\"},\n\t\t{\"-8\", 0, \"-8\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Lsh(x, tc.n)\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"Lsh(%s, %d) = %v, want %v\", tc.x, tc.n, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n" + }, + { + "name": "cmp.gno", + "body": "package int256\n\n// Eq returns true if z == x\nfunc (z *Int) Eq(x *Int) bool {\n\treturn (z.neg == x.neg) \u0026\u0026 z.abs.Eq(x.abs)\n}\n\n// Neq returns true if z != x\nfunc (z *Int) Neq(x *Int) bool {\n\treturn !z.Eq(x)\n}\n\n// Cmp compares x and y and returns:\n//\n//\t-1 if x \u003c y\n//\t 0 if x == y\n//\t+1 if x \u003e y\nfunc (z *Int) Cmp(x *Int) (r int) {\n\t// x cmp y == x cmp y\n\t// x cmp (-y) == x\n\t// (-x) cmp y == y\n\t// (-x) cmp (-y) == -(x cmp y)\n\tswitch {\n\tcase z == x:\n\t\t// nothing to do\n\tcase z.neg == x.neg:\n\t\tr = z.abs.Cmp(x.abs)\n\t\tif z.neg {\n\t\t\tr = -r\n\t\t}\n\tcase z.neg:\n\t\tr = -1\n\tdefault:\n\t\tr = 1\n\t}\n\treturn\n}\n\n// IsZero returns true if z == 0\nfunc (z *Int) IsZero() bool {\n\treturn z.abs.IsZero()\n}\n\n// IsNeg returns true if z \u003c 0\nfunc (z *Int) IsNeg() bool {\n\treturn z.neg\n}\n\n// Lt returns true if z \u003c x\nfunc (z *Int) Lt(x *Int) bool {\n\tif z.neg {\n\t\tif x.neg {\n\t\t\treturn z.abs.Gt(x.abs)\n\t\t} else {\n\t\t\treturn true\n\t\t}\n\t} else {\n\t\tif x.neg {\n\t\t\treturn false\n\t\t} else {\n\t\t\treturn z.abs.Lt(x.abs)\n\t\t}\n\t}\n}\n\n// Gt returns true if z \u003e x\nfunc (z *Int) Gt(x *Int) bool {\n\tif z.neg {\n\t\tif x.neg {\n\t\t\treturn z.abs.Lt(x.abs)\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t} else {\n\t\tif x.neg {\n\t\t\treturn true\n\t\t} else {\n\t\t\treturn z.abs.Gt(x.abs)\n\t\t}\n\t}\n}\n\n// Clone creates a new Int identical to z\nfunc (z *Int) Clone() *Int {\n\treturn \u0026Int{z.abs.Clone(), z.neg}\n}\n" + }, + { + "name": "cmp_test.gno", + "body": "package int256\n\nimport \"testing\"\n\nfunc TestEq(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", true},\n\t\t{\"0\", \"1\", false},\n\t\t{\"1\", \"0\", false},\n\t\t{\"-1\", \"0\", false},\n\t\t{\"0\", \"-1\", false},\n\t\t{\"1\", \"1\", true},\n\t\t{\"-1\", \"-1\", true},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Eq(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Eq(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestNeq(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", true},\n\t\t{\"1\", \"0\", true},\n\t\t{\"-1\", \"0\", true},\n\t\t{\"0\", \"-1\", true},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Neq(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Neq(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestCmp(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant int\n\t}{\n\t\t{\"0\", \"0\", 0},\n\t\t{\"0\", \"1\", -1},\n\t\t{\"1\", \"0\", 1},\n\t\t{\"-1\", \"0\", -1},\n\t\t{\"0\", \"-1\", 1},\n\t\t{\"1\", \"1\", 0},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Cmp(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Cmp(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", true},\n\t\t{\"-0\", true},\n\t\t{\"1\", false},\n\t\t{\"-1\", false},\n\t\t{\"10\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.IsZero()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"IsZero(%s) = %v, want %v\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", false},\n\t\t{\"-0\", true}, // TODO: should this be false?\n\t\t{\"1\", false},\n\t\t{\"-1\", true},\n\t\t{\"10\", false},\n\t\t{\"-10\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.IsNeg()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"IsNeg(%s) = %v, want %v\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestLt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", true},\n\t\t{\"1\", \"0\", false},\n\t\t{\"-1\", \"0\", true},\n\t\t{\"0\", \"-1\", false},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Lt(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Lt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestGt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", false},\n\t\t{\"1\", \"0\", true},\n\t\t{\"-1\", \"0\", false},\n\t\t{\"0\", \"-1\", true},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Gt(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Gt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t}{\n\t\t{\"0\"},\n\t\t{\"-0\"},\n\t\t{\"1\"},\n\t\t{\"-1\"},\n\t\t{\"10\"},\n\t\t{\"-10\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty := x.Clone()\n\n\t\tif x.Cmp(y) != 0 {\n\t\t\tt.Errorf(\"Clone(%s) = %v, want %v\", tc.x, y, x)\n\t\t}\n\t}\n}\n" + }, + { + "name": "conversion.gno", + "body": "package int256\n\nimport \"gno.land/p/demo/uint256\"\n\n// SetInt64 sets z to x and returns z.\nfunc (z *Int) SetInt64(x int64) *Int {\n\tz.initiateAbs()\n\n\tneg := false\n\tif x \u003c 0 {\n\t\tneg = true\n\t\tx = -x\n\t}\n\tif z.abs == nil {\n\t\tpanic(\"abs is nil\")\n\t}\n\tz.abs = z.abs.SetUint64(uint64(x))\n\tz.neg = neg\n\treturn z\n}\n\n// SetUint64 sets z to x and returns z.\nfunc (z *Int) SetUint64(x uint64) *Int {\n\tz.initiateAbs()\n\n\tif z.abs == nil {\n\t\tpanic(\"abs is nil\")\n\t}\n\tz.abs = z.abs.SetUint64(x)\n\tz.neg = false\n\treturn z\n}\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Int) Uint64() uint64 {\n\treturn z.abs.Uint64()\n}\n\n// Int64 returns the lower 64-bits of z\nfunc (z *Int) Int64() int64 {\n\t_abs := z.abs.Clone()\n\n\tif z.neg {\n\t\treturn -int64(_abs.Uint64())\n\t}\n\treturn int64(_abs.Uint64())\n}\n\n// Neg sets z to -x and returns z.)\nfunc (z *Int) Neg(x *Int) *Int {\n\tz.abs.Set(x.abs)\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t} else {\n\t\tz.neg = !x.neg\n\t}\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Int) Set(x *Int) *Int {\n\tz.abs.Set(x.abs)\n\tz.neg = x.neg\n\treturn z\n}\n\n// SetFromUint256 converts a uint256.Uint to Int and sets the value to z.\nfunc (z *Int) SetUint256(x *uint256.Uint) *Int {\n\tz.abs.Set(x)\n\tz.neg = false\n\treturn z\n}\n\n// OBS, differs from original mempooler int256\n// ToString returns the decimal representation of z.\nfunc (z *Int) ToString() string {\n\tif z == nil {\n\t\tpanic(\"int256: nil pointer to ToString()\")\n\t}\n\n\tt := z.abs.Dec()\n\tif z.neg {\n\t\treturn \"-\" + t\n\t}\n\n\treturn t\n}\n" + }, + { + "name": "conversion_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestSetInt64(t *testing.T) {\n\ttests := []struct {\n\t\tx int64\n\t\twant string\n\t}{\n\t\t{0, \"0\"},\n\t\t{1, \"1\"},\n\t\t{-1, \"-1\"},\n\t\t{9223372036854775807, \"9223372036854775807\"},\n\t\t{-9223372036854775808, \"-9223372036854775808\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tvar z Int\n\t\tz.SetInt64(tc.x)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"SetInt64(%d) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSetUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx uint64\n\t\twant string\n\t}{\n\t\t{0, \"0\"},\n\t\t{1, \"1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tvar z Int\n\t\tz.SetUint64(tc.x)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"SetUint64(%d) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant uint64\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"9223372036854775807\", 9223372036854775807},\n\t\t{\"9223372036854775808\", 9223372036854775808},\n\t\t{\"18446744073709551615\", 18446744073709551615},\n\t\t{\"18446744073709551616\", 0},\n\t\t{\"18446744073709551617\", 1},\n\t\t{\"-1\", 1},\n\t\t{\"-18446744073709551615\", 18446744073709551615},\n\t\t{\"-18446744073709551616\", 0},\n\t\t{\"-18446744073709551617\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\n\t\tgot := z.Uint64()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Uint64(%s) = %d, want %d\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestInt64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant int64\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"9223372036854775807\", 9223372036854775807},\n\t\t{\"18446744073709551616\", 0},\n\t\t{\"18446744073709551617\", 1},\n\t\t{\"-1\", -1},\n\t\t{\"-9223372036854775808\", -9223372036854775808},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\n\t\tgot := z.Int64()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Uint64(%s) = %d, want %d\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"-1\"},\n\t\t{\"-1\", \"1\"},\n\t\t{\"9223372036854775807\", \"-9223372036854775807\"},\n\t\t{\"-18446744073709551615\", \"18446744073709551615\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\t\tz.Neg(z)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Neg(%s) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSet(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"-1\"},\n\t\t{\"9223372036854775807\", \"9223372036854775807\"},\n\t\t{\"-18446744073709551615\", \"-18446744073709551615\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\t\tz.Set(z)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Set(%s) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSetUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"9223372036854775807\", \"9223372036854775807\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tgot := New()\n\n\t\tz := uint256.MustFromDecimal(tc.x)\n\t\tgot.SetUint256(z)\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"SetUint256(%s) = %s, want %s\", tc.x, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n\nfunc TestToString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tsetup func() *Int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Zero from subtraction\",\n\t\t\tsetup: func() *Int {\n\t\t\t\tminusThree := MustFromDecimal(\"-3\")\n\t\t\t\tthree := MustFromDecimal(\"3\")\n\t\t\t\treturn Zero().Add(minusThree, three)\n\t\t\t},\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"Zero from right shift\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn Zero().Rsh(One(), 1234)\n\t\t\t},\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"Positive number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"42\")\n\t\t\t},\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname: \"Negative number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"-42\")\n\t\t\t},\n\t\t\texpected: \"-42\",\n\t\t},\n\t\t{\n\t\t\tname: \"Large positive number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"115792089237316195423570985008687907853269984665640564039457584007913129639935\")\n\t\t\t},\n\t\t\texpected: \"115792089237316195423570985008687907853269984665640564039457584007913129639935\",\n\t\t},\n\t\t{\n\t\t\tname: \"Large negative number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\")\n\t\t\t},\n\t\t\texpected: \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := tt.setup()\n\t\t\tresult := z.ToString()\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"ToString() = %s, want %s\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "int256.gno", + "body": "// This package provides a 256-bit signed integer type, Int, and associated functions.\npackage int256\n\nimport (\n\t\"gno.land/p/demo/uint256\"\n)\n\nvar one = uint256.NewUint(1)\n\ntype Int struct {\n\tabs *uint256.Uint\n\tneg bool\n}\n\n// Zero returns a new Int set to 0.\nfunc Zero() *Int {\n\treturn NewInt(0)\n}\n\n// One returns a new Int set to 1.\nfunc One() *Int {\n\treturn NewInt(1)\n}\n\n// Sign returns:\n//\n//\t-1 if x \u003c 0\n//\t 0 if x == 0\n//\t+1 if x \u003e 0\nfunc (z *Int) Sign() int {\n\tz.initiateAbs()\n\n\tif z.abs.IsZero() {\n\t\treturn 0\n\t}\n\tif z.neg {\n\t\treturn -1\n\t}\n\treturn 1\n}\n\n// New returns a new Int set to 0.\nfunc New() *Int {\n\treturn \u0026Int{\n\t\tabs: new(uint256.Uint),\n\t}\n}\n\n// NewInt allocates and returns a new Int set to x.\nfunc NewInt(x int64) *Int {\n\treturn New().SetInt64(x)\n}\n\n// FromDecimal returns a new Int from a decimal string.\n// Returns a new Int and an error if the string is not a valid decimal.\nfunc FromDecimal(s string) (*Int, error) {\n\treturn new(Int).SetString(s)\n}\n\n// MustFromDecimal returns a new Int from a decimal string.\n// Panics if the string is not a valid decimal.\nfunc MustFromDecimal(s string) *Int {\n\tz, err := FromDecimal(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn z\n}\n\n// SetString sets s to the value of z and returns z and a boolean indicating success.\nfunc (z *Int) SetString(s string) (*Int, error) {\n\tneg := false\n\t// Remove max one leading +\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '+' {\n\t\tneg = false\n\t\ts = s[1:]\n\t}\n\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '-' {\n\t\tneg = true\n\t\ts = s[1:]\n\t}\n\tvar (\n\t\tabs *uint256.Uint\n\t\terr error\n\t)\n\tabs, err = uint256.FromDecimal(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn \u0026Int{\n\t\tabs,\n\t\tneg,\n\t}, nil\n}\n\n// FromUint256 is a convenience-constructor from uint256.Uint.\n// Returns a new Int and whether overflow occurred.\n// OBS: If u is `nil`, this method returns `nil, false`\nfunc FromUint256(x *uint256.Uint) *Int {\n\tif x == nil {\n\t\treturn nil\n\t}\n\tz := Zero()\n\n\tz.SetUint256(x)\n\treturn z\n}\n\n// OBS, differs from original mempooler int256\n// NilToZero sets z to 0 and return it if it's nil, otherwise it returns z\nfunc (z *Int) NilToZero() *Int {\n\tif z == nil {\n\t\treturn NewInt(0)\n\t}\n\treturn z\n}\n\n// initiateAbs sets default value for `z` or `z.abs` value if is nil\n// OBS: differs from mempooler int256. It checks not only `z.abs` but also `z`\nfunc (z *Int) initiateAbs() {\n\tif z == nil || z.abs == nil {\n\t\tz.abs = new(uint256.Uint)\n\t}\n}\n" + }, + { + "name": "int256_test.gno", + "body": "// ported from github.com/mempooler/int256\npackage int256\n\nimport \"testing\"\n\nfunc TestSign(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant int\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"-1\", -1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\t\tgot := z.Sign()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Sign(%s) = %d, want %d\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "json", + "path": "gno.land/p/demo/json", + "files": [ + { + "name": "LICENSE", + "body": "# MIT License\n\nCopyright (c) 2019 Pyzhov Stepan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "README.md", + "body": "# JSON Parser\n\nThe JSON parser is a package that provides functionality for parsing and processing JSON strings. This package accepts JSON strings as byte slices.\n\nCurrently, gno does not [support the `reflect` package](https://docs.gno.land/concepts/effective-gno#reflection-is-never-clear), so it cannot retrieve type information at runtime. Therefore, it is designed to infer and handle type information when parsing JSON strings using a state machine approach.\n\nAfter passing through the state machine, JSON strings are represented as the `Node` type. The `Node` type represents nodes for JSON data, including various types such as `ObjectNode`, `ArrayNode`, `StringNode`, `NumberNode`, `BoolNode`, and `NullNode`.\n\nThis package provides methods for manipulating, searching, and extracting the Node type.\n\n## State Machine\n\nTo parse JSON strings, a [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine) approach is used. The state machine transitions to the next state based on the current state and the input character while parsing the JSON string. Through this method, type information can be inferred and processed without reflect, and the amount of parser code can be significantly reduced.\n\nThe image below shows the state transitions of the state machine according to the states and input characters.\n\n```mermaid\nstateDiagram-v2\n [*] --\u003e __: Start\n __ --\u003e ST: String\n __ --\u003e MI: Number\n __ --\u003e ZE: Zero\n __ --\u003e IN: Integer\n __ --\u003e T1: Boolean (true)\n __ --\u003e F1: Boolean (false)\n __ --\u003e N1: Null\n __ --\u003e ec: Empty Object End\n __ --\u003e cc: Object End\n __ --\u003e bc: Array End\n __ --\u003e co: Object Begin\n __ --\u003e bo: Array Begin\n __ --\u003e cm: Comma\n __ --\u003e cl: Colon\n __ --\u003e OK: Success/End\n ST --\u003e OK: String Complete\n MI --\u003e OK: Number Complete\n ZE --\u003e OK: Zero Complete\n IN --\u003e OK: Integer Complete\n T1 --\u003e OK: True Complete\n F1 --\u003e OK: False Complete\n N1 --\u003e OK: Null Complete\n ec --\u003e OK: Empty Object Complete\n cc --\u003e OK: Object Complete\n bc --\u003e OK: Array Complete\n co --\u003e OB: Inside Object\n bo --\u003e AR: Inside Array\n cm --\u003e KE: Expecting New Key\n cm --\u003e VA: Expecting New Value\n cl --\u003e VA: Expecting Value\n OB --\u003e ST: String in Object (Key)\n OB --\u003e ec: Empty Object\n OB --\u003e cc: End Object\n AR --\u003e ST: String in Array\n AR --\u003e bc: End Array\n KE --\u003e ST: String as Key\n VA --\u003e ST: String as Value\n VA --\u003e MI: Number as Value\n VA --\u003e T1: True as Value\n VA --\u003e F1: False as Value\n VA --\u003e N1: Null as Value\n OK --\u003e [*]: End\n```\n\n## Examples\n\nThis package provides parsing functionality along with encoding and decoding functionality. The following examples demonstrate how to use this package.\n\n### Decoding\n\nDecoding (or Unmarshaling) is the functionality that converts an input byte slice JSON string into a `Node` type.\n\nThe converted `Node` type allows you to modify the JSON data or search and extract data that meets specific conditions.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n node, err := json.Unmarshal([]byte(`{\"foo\": \"var\"}`))\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n ufmt.Sprintf(\"node: %v\", node)\n}\n```\n\n### Encoding\n\nEncoding (or Marshaling) is the functionality that converts JSON data represented as a Node type into a byte slice JSON string.\n\n\u003e ⚠️ Caution: Converting a large `Node` type into a JSON string may _impact performance_. or might be cause _unexpected behavior_.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n node := ObjectNode(\"\", map[string]*Node{\n \"foo\": StringNode(\"foo\", \"bar\"),\n \"baz\": NumberNode(\"baz\", 100500),\n \"qux\": NullNode(\"qux\"),\n })\n\n b, err := json.Marshal(node)\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n ufmt.Sprintf(\"json: %s\", string(b))\n}\n```\n\n### Searching\n\nOnce the JSON data converted into a `Node` type, you can **search** and **extract** data that satisfy specific conditions. For example, you can find data with a specific type or data with a specific key.\n\nTo use this functionality, you can use methods in the `GetXXX` prefixed methods. The `MustXXX` methods also provide the same functionality as the former methods, but they will **panic** if data doesn't satisfies the condition.\n\nHere is an example of finding data with a specific key. For more examples, please refer to the [node.gno](node.gno) file.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n root, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n value, err := root.GetKey(\"foo\")\n if err != nil {\n ufmt.Errorf(\"error occurred while getting key, %s\", err)\n }\n\n if value.MustBool() != true {\n ufmt.Errorf(\"value is not true\")\n }\n\n value, err = root.GetKey(\"bar\")\n if err != nil {\n t.Errorf(\"error occurred while getting key, %s\", err)\n }\n\n _, err = root.GetKey(\"baz\")\n if err == nil {\n t.Errorf(\"key baz is not exist. must be failed\")\n }\n}\n```\n\n## Contributing\n\nPlease submit any issues or pull requests for this package through the GitHub repository at [gnolang/gno](\u003chttps://github.com/gnolang/gno\u003e).\n" + }, + { + "name": "buffer.gno", + "body": "package json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype buffer struct {\n\tdata []byte\n\tlength int\n\tindex int\n\n\tlast States\n\tstate States\n\tclass Classes\n}\n\n// newBuffer creates a new buffer with the given data\nfunc newBuffer(data []byte) *buffer {\n\treturn \u0026buffer{\n\t\tdata: data,\n\t\tlength: len(data),\n\t\tlast: GO,\n\t\tstate: GO,\n\t}\n}\n\n// first retrieves the first non-whitespace (or other escaped) character in the buffer.\nfunc (b *buffer) first() (byte, error) {\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tc := b.data[b.index]\n\n\t\tif !(c == whiteSpace || c == carriageReturn || c == newLine || c == tab) {\n\t\t\treturn c, nil\n\t\t}\n\t}\n\n\treturn 0, io.EOF\n}\n\n// current returns the byte of the current index.\nfunc (b *buffer) current() (byte, error) {\n\tif b.index \u003e= b.length {\n\t\treturn 0, io.EOF\n\t}\n\n\treturn b.data[b.index], nil\n}\n\n// next moves to the next byte and returns it.\nfunc (b *buffer) next() (byte, error) {\n\tb.index++\n\treturn b.current()\n}\n\n// step just moves to the next position.\nfunc (b *buffer) step() error {\n\t_, err := b.next()\n\treturn err\n}\n\n// move moves the index by the given position.\nfunc (b *buffer) move(pos int) error {\n\tnewIndex := b.index + pos\n\n\tif newIndex \u003e b.length {\n\t\treturn io.EOF\n\t}\n\n\tb.index = newIndex\n\n\treturn nil\n}\n\n// slice returns the slice from the current index to the given position.\nfunc (b *buffer) slice(pos int) ([]byte, error) {\n\tend := b.index + pos\n\n\tif end \u003e b.length {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn b.data[b.index:end], nil\n}\n\n// sliceFromIndices returns a slice of the buffer's data starting from 'start' up to (but not including) 'stop'.\nfunc (b *buffer) sliceFromIndices(start, stop int) []byte {\n\tif start \u003e b.length {\n\t\tstart = b.length\n\t}\n\n\tif stop \u003e b.length {\n\t\tstop = b.length\n\t}\n\n\treturn b.data[start:stop]\n}\n\n// skip moves the index to skip the given byte.\nfunc (b *buffer) skip(bs byte) error {\n\tfor b.index \u003c b.length {\n\t\tif b.data[b.index] == bs \u0026\u0026 !b.backslash() {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn io.EOF\n}\n\n// skipAndReturnIndex moves the buffer index forward by one and returns the new index.\nfunc (b *buffer) skipAndReturnIndex() (int, error) {\n\terr := b.step()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn b.index, nil\n}\n\n// skipUntil moves the buffer index forward until it encounters a byte contained in the endTokens set.\nfunc (b *buffer) skipUntil(endTokens map[byte]bool) (int, error) {\n\tfor b.index \u003c b.length {\n\t\tcurrentByte, err := b.current()\n\t\tif err != nil {\n\t\t\treturn b.index, err\n\t\t}\n\n\t\t// Check if the current byte is in the set of end tokens.\n\t\tif _, exists := endTokens[currentByte]; exists {\n\t\t\treturn b.index, nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn b.index, io.EOF\n}\n\n// significantTokens is a map where the keys are the significant characters in a JSON path.\n// The values in the map are all true, which allows us to use the map as a set for quick lookups.\nvar significantTokens = [256]bool{\n\tdot: true, // access properties of an object\n\tdollarSign: true, // root object\n\tatSign: true, // current object\n\tbracketOpen: true, // start of an array index or filter expression\n\tbracketClose: true, // end of an array index or filter expression\n}\n\n// filterTokens stores the filter expression tokens.\nvar filterTokens = [256]bool{\n\taesterisk: true, // wildcard\n\tandSign: true,\n\torSign: true,\n}\n\n// skipToNextSignificantToken advances the buffer index to the next significant character.\n// Significant characters are defined based on the JSON path syntax.\nfunc (b *buffer) skipToNextSignificantToken() {\n\tfor b.index \u003c b.length {\n\t\tcurrent := b.data[b.index]\n\n\t\tif significantTokens[current] {\n\t\t\tbreak\n\t\t}\n\n\t\tb.index++\n\t}\n}\n\n// backslash checks to see if the number of backslashes before the current index is odd.\n//\n// This is used to check if the current character is escaped. However, unlike the \"unescape\" function,\n// \"backslash\" only serves to check the number of backslashes.\nfunc (b *buffer) backslash() bool {\n\tif b.index == 0 {\n\t\treturn false\n\t}\n\n\tcount := 0\n\tfor i := b.index - 1; ; i-- {\n\t\tif b.data[i] != backSlash {\n\t\t\tbreak\n\t\t}\n\n\t\tcount++\n\n\t\tif i == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn count%2 != 0\n}\n\n// numIndex holds a map of valid numeric characters\nvar numIndex = [256]bool{\n\t'0': true,\n\t'1': true,\n\t'2': true,\n\t'3': true,\n\t'4': true,\n\t'5': true,\n\t'6': true,\n\t'7': true,\n\t'8': true,\n\t'9': true,\n\t'.': true,\n\t'e': true,\n\t'E': true,\n}\n\n// pathToken checks if the current token is a valid JSON path token.\nfunc (b *buffer) pathToken() error {\n\tvar stack []byte\n\n\tinToken := false\n\tinNumber := false\n\tfirst := b.index\n\n\tfor b.index \u003c b.length {\n\t\tc := b.data[b.index]\n\n\t\tswitch {\n\t\tcase c == doubleQuote || c == singleQuote:\n\t\t\tinToken = true\n\t\t\tif err := b.step(); err != nil {\n\t\t\t\treturn errors.New(\"error stepping through buffer\")\n\t\t\t}\n\n\t\t\tif err := b.skip(c); err != nil {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\t\tif b.index \u003e= b.length {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\tcase c == bracketOpen || c == parenOpen:\n\t\t\tinToken = true\n\t\t\tstack = append(stack, c)\n\n\t\tcase c == bracketClose || c == parenClose:\n\t\t\tinToken = true\n\t\t\tif len(stack) == 0 || (c == bracketClose \u0026\u0026 stack[len(stack)-1] != bracketOpen) || (c == parenClose \u0026\u0026 stack[len(stack)-1] != parenOpen) {\n\t\t\t\treturn errUnmatchedParenthesis\n\t\t\t}\n\n\t\t\tstack = stack[:len(stack)-1]\n\n\t\tcase pathStateContainsValidPathToken(c):\n\t\t\tinToken = true\n\n\t\tcase c == plus || c == minus:\n\t\t\tif inNumber || (b.index \u003e 0 \u0026\u0026 numIndex[b.data[b.index-1]]) {\n\t\t\t\tinToken = true\n\t\t\t} else if !inToken \u0026\u0026 (b.index+1 \u003c b.length \u0026\u0026 numIndex[b.data[b.index+1]]) {\n\t\t\t\tinToken = true\n\t\t\t\tinNumber = true\n\t\t\t} else if !inToken {\n\t\t\t\treturn errInvalidToken\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif len(stack) != 0 || inToken {\n\t\t\t\tinToken = true\n\t\t\t} else {\n\t\t\t\tgoto end\n\t\t\t}\n\t\t}\n\n\t\tb.index++\n\t}\n\nend:\n\tif len(stack) != 0 {\n\t\treturn errUnmatchedParenthesis\n\t}\n\n\tif first == b.index {\n\t\treturn errors.New(\"no token found\")\n\t}\n\n\tif inNumber \u0026\u0026 !numIndex[b.data[b.index-1]] {\n\t\tinNumber = false\n\t}\n\n\treturn nil\n}\n\nfunc pathStateContainsValidPathToken(c byte) bool {\n\tif significantTokens[c] {\n\t\treturn true\n\t}\n\n\tif filterTokens[c] {\n\t\treturn true\n\t}\n\n\tif numIndex[c] {\n\t\treturn true\n\t}\n\n\tif 'A' \u003c= c \u0026\u0026 c \u003c= 'Z' || 'a' \u003c= c \u0026\u0026 c \u003c= 'z' {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (b *buffer) numeric(token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(doubleQuote)\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\tif token {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\treturn nil\n\t\t}\n\n\t\tif b.state \u003c MI || b.state \u003e E3 {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\tif b.last != ZE \u0026\u0026 b.last != IN \u0026\u0026 b.last != FR \u0026\u0026 b.last != E3 {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) getClasses(c byte) Classes {\n\tif b.data[b.index] \u003e= 128 {\n\t\treturn C_ETC\n\t}\n\n\tif c == singleQuote {\n\t\treturn QuoteAsciiClasses[b.data[b.index]]\n\t}\n\n\treturn AsciiClasses[b.data[b.index]]\n}\n\nfunc (b *buffer) getState() States {\n\tb.last = b.state\n\n\tb.class = b.getClasses(doubleQuote)\n\tif b.class == __ {\n\t\treturn __\n\t}\n\n\tb.state = StateTransitionTable[b.last][b.class]\n\n\treturn b.state\n}\n\n// string parses a string token from the buffer.\nfunc (b *buffer) string(search byte, token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(search)\n\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\tbreak\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) word(bs []byte) error {\n\tvar c byte\n\n\tmax := len(bs)\n\tindex := 0\n\n\tfor ; b.index \u003c b.length \u0026\u0026 index \u003c max; b.index++ {\n\t\tc = b.data[b.index]\n\n\t\tif c != bs[index] {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tindex++\n\t\tif index \u003e= max {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != max {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc numberKind2f64(value interface{}) (result float64, err error) {\n\tswitch typed := value.(type) {\n\tcase float64:\n\t\tresult = typed\n\tcase float32:\n\t\tresult = float64(typed)\n\tcase int:\n\t\tresult = float64(typed)\n\tcase int8:\n\t\tresult = float64(typed)\n\tcase int16:\n\t\tresult = float64(typed)\n\tcase int32:\n\t\tresult = float64(typed)\n\tcase int64:\n\t\tresult = float64(typed)\n\tcase uint:\n\t\tresult = float64(typed)\n\tcase uint8:\n\t\tresult = float64(typed)\n\tcase uint16:\n\t\tresult = float64(typed)\n\tcase uint32:\n\t\tresult = float64(typed)\n\tcase uint64:\n\t\tresult = float64(typed)\n\tdefault:\n\t\terr = ufmt.Errorf(\"invalid number type: %T\", value)\n\t}\n\n\treturn\n}\n" + }, + { + "name": "buffer_test.gno", + "body": "package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBufferCurrent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\texpected byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid current byte\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata: []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex: 1,\n\t\t\t},\n\t\t\texpected: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata: []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex: 4,\n\t\t\t},\n\t\t\texpected: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.current()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.current() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"buffer.current() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferStep(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid step\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.step()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.step() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferNext(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\twant byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid next byte\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twant: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twant: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.next()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.next() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"buffer.next() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSlice(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tpos int\n\t\twant []byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid slice -- 0 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 0,\n\t\t\twant: nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 1 character\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 1,\n\t\t\twant: []byte(\"t\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 2 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 2,\n\t\t\twant: []byte(\"es\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 3 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 3,\n\t\t\twant: []byte(\"tes\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 4 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 4,\n\t\t\twant: []byte(\"test\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\tpos: 2,\n\t\t\twant: nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.slice(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.slice() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(got) != string(tt.want) {\n\t\t\t\tt.Errorf(\"buffer.slice() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferMove(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tpos int\n\t\twantErr bool\n\t\twantIdx int\n\t}{\n\t\t{\n\t\t\tname: \"Valid move\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 2,\n\t\t\twantErr: false,\n\t\t\twantIdx: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Move beyond length\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 4,\n\t\t\twantErr: true,\n\t\t\twantIdx: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.move(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.move() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif tt.buffer.index != tt.wantIdx {\n\t\t\t\tt.Errorf(\"buffer.move() index = %v, want %v\", tt.buffer.index, tt.wantIdx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSkip(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tb byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Skip byte\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Skip to EOF\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb: 'x',\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.skip(tt.b)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.skip() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipToNextSignificantToken(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\texpected int\n\t}{\n\t\t{\"No significant chars\", []byte(\"abc\"), 3},\n\t\t{\"One significant char at start\", []byte(\".abc\"), 0},\n\t\t{\"Significant char in middle\", []byte(\"ab.c\"), 2},\n\t\t{\"Multiple significant chars\", []byte(\"a$.c\"), 1},\n\t\t{\"Significant char at end\", []byte(\"abc$\"), 3},\n\t\t{\"Only significant chars\", []byte(\"$.\"), 0},\n\t\t{\"Empty string\", []byte(\"\"), 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.input)\n\t\t\tb.skipToNextSignificantToken()\n\t\t\tif b.index != tt.expected {\n\t\t\t\tt.Errorf(\"after skipToNextSignificantToken(), got index = %v, want %v\", b.index, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuffer(s string) *buffer {\n\treturn newBuffer([]byte(s))\n}\n\nfunc TestSkipAndReturnIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"StartOfString\", \"\", 0},\n\t\t{\"MiddleOfString\", \"abcdef\", 1},\n\t\t{\"EndOfString\", \"abc\", 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipAndReturnIndex()\n\t\t\tif err != nil \u0026\u0026 tt.input != \"\" { // Expect no error unless input is empty\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipUntil(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\ttokens map[byte]bool\n\t\texpected int\n\t}{\n\t\t{\"SkipToToken\", \"abcdefg\", map[byte]bool{'c': true}, 2},\n\t\t{\"SkipToEnd\", \"abcdefg\", map[byte]bool{'h': true}, 7},\n\t\t{\"SkipNone\", \"abcdefg\", map[byte]bool{'a': true}, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipUntil(tt.tokens)\n\t\t\tif err != nil \u0026\u0026 got != len(tt.input) { // Expect error only if reached end without finding token\n\t\t\t\tt.Errorf(\"skipUntil() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipUntil() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSliceFromIndices(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\tstart int\n\t\tend int\n\t\texpected string\n\t}{\n\t\t{\"FullString\", \"abcdefg\", 0, 7, \"abcdefg\"},\n\t\t{\"Substring\", \"abcdefg\", 2, 5, \"cde\"},\n\t\t{\"OutOfBounds\", \"abcdefg\", 5, 10, \"fg\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot := buf.sliceFromIndices(tt.start, tt.end)\n\t\t\tif string(got) != tt.expected {\n\t\t\t\tt.Errorf(\"sliceFromIndices() = %v, want %v\", string(got), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferToken(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpath string\n\t\tindex int\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Simple valid path\",\n\t\t\tpath: \"@.length\",\n\t\t\tindex: 8,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with array expr\",\n\t\t\tpath: \"@['foo'].0.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with array expr and simple fomula\",\n\t\t\tpath: \"@['foo'].[(@.length - 1)].*\",\n\t\t\tindex: 27,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with filter expr\",\n\t\t\tpath: \"@['foo'].[?(@.bar == 1 \u0026 @.baz \u003c @.length)].*\",\n\t\t\tindex: 45,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"addition of foo and bar\",\n\t\t\tpath: \"@.foo+@.bar\",\n\t\t\tindex: 11,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"logical AND of foo and bar\",\n\t\t\tpath: \"@.foo \u0026\u0026 @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"logical OR of foo and bar\",\n\t\t\tpath: \"@.foo || @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"accessing third element of foo\",\n\t\t\tpath: \"@.foo,3\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"accessing last element of array\",\n\t\t\tpath: \"@.length-1\",\n\t\t\tindex: 10,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"number 1\",\n\t\t\tpath: \"1\",\n\t\t\tindex: 1,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float\",\n\t\t\tpath: \"3.1e4\",\n\t\t\tindex: 5,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float with minus\",\n\t\t\tpath: \"3.1e-4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float with plus\",\n\t\t\tpath: \"3.1e+4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative number\",\n\t\t\tpath: \"-12345\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float\",\n\t\t\tpath: \"-3.1e4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float with minus\",\n\t\t\tpath: \"-3.1e-4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float with plus\",\n\t\t\tpath: \"-3.1e+4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string number\",\n\t\t\tpath: \"'12345'\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string with backslash\",\n\t\t\tpath: \"'foo \\\\'bar '\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string with inner double quotes\",\n\t\t\tpath: \"'foo \\\"bar \\\"'\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis 1\",\n\t\t\tpath: \"(@abc)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis 2\",\n\t\t\tpath: \"[()]\",\n\t\t\tindex: 4,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch\",\n\t\t\tpath: \"[(])\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch 2\",\n\t\t\tpath: \"(\",\n\t\t\tindex: 1,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch 3\",\n\t\t\tpath: \"())]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"bracket mismatch\",\n\t\t\tpath: \"[()\",\n\t\t\tindex: 3,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"bracket mismatch 2\",\n\t\t\tpath: \"()]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"path does not close bracket\",\n\t\t\tpath: \"@.foo[)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := newBuffer([]byte(tt.path))\n\n\t\t\terr := buf.pathToken()\n\t\t\tif tt.isErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err == nil \u0026\u0026 tt.isErr {\n\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t}\n\n\t\t\tif buf.index != tt.index {\n\t\t\t\tt.Errorf(\"Expected final index %d, got %d (token: `%s`) for path `%s`\", tt.index, buf.index, string(buf.data[buf.index]), tt.path)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferFirst(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\texpected byte\n\t}{\n\t\t{\n\t\t\tname: \"Valid first byte\",\n\t\t\tdata: []byte(\"test\"),\n\t\t\texpected: 't',\n\t\t},\n\t\t{\n\t\t\tname: \"Empty buffer\",\n\t\t\tdata: []byte(\"\"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Whitespace buffer\",\n\t\t\tdata: []byte(\" \"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace in middle\",\n\t\t\tdata: []byte(\"hello world\"),\n\t\t\texpected: 'h',\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.data)\n\n\t\t\tgot, err := b.first()\n\t\t\tif err != nil \u0026\u0026 tt.expected != 0 {\n\t\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Expected first byte to be %q, got %q\", tt.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "builder.gno", + "body": "package json\n\ntype NodeBuilder struct {\n\tnode *Node\n}\n\nfunc Builder() *NodeBuilder {\n\treturn \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n}\n\nfunc (b *NodeBuilder) WriteString(key, value string) *NodeBuilder {\n\tb.node.AppendObject(key, StringNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNumber(key string, value float64) *NodeBuilder {\n\tb.node.AppendObject(key, NumberNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteBool(key string, value bool) *NodeBuilder {\n\tb.node.AppendObject(key, BoolNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNull(key string) *NodeBuilder {\n\tb.node.AppendObject(key, NullNode(\"\"))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteObject(key string, fn func(*NodeBuilder)) *NodeBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tb.node.AppendObject(key, nestedBuilder.node)\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteArray(key string, fn func(*ArrayBuilder)) *NodeBuilder {\n\tarrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(arrayBuilder)\n\tb.node.AppendObject(key, ArrayNode(\"\", arrayBuilder.nodes))\n\treturn b\n}\n\nfunc (b *NodeBuilder) Node() *Node {\n\treturn b.node\n}\n\ntype ArrayBuilder struct {\n\tnodes []*Node\n}\n\nfunc (ab *ArrayBuilder) WriteString(value string) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, StringNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNumber(value float64) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NumberNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteInt(value int) *ArrayBuilder {\n\treturn ab.WriteNumber(float64(value))\n}\n\nfunc (ab *ArrayBuilder) WriteBool(value bool) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, BoolNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNull() *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NullNode(\"\"))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteObject(fn func(*NodeBuilder)) *ArrayBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tab.nodes = append(ab.nodes, nestedBuilder.node)\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteArray(fn func(*ArrayBuilder)) *ArrayBuilder {\n\tnestedArrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(nestedArrayBuilder)\n\tab.nodes = append(ab.nodes, ArrayNode(\"\", nestedArrayBuilder.nodes))\n\treturn ab\n}\n" + }, + { + "name": "builder_test.gno", + "body": "package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNodeBuilder(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuild func() *Node\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"plain object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteNumber(\"age\", 30).\n\t\t\t\t\tWriteBool(\"is_student\", false).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"age\":30,\"is_student\":false}`,\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteObject(\"address\", func(b *NodeBuilder) {\n\t\t\t\t\t\tb.WriteString(\"city\", \"New York\").\n\t\t\t\t\t\t\tWriteNumber(\"zipcode\", 10001)\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"address\":{\"city\":\"New York\",\"zipcode\":10001}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"null node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().WriteNull(\"foo\").Node()\n\t\t\t},\n\t\t\texpected: `{\"foo\":null}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"items\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteString(\"item2\").\n\t\t\t\t\t\t\tWriteString(\"item3\")\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"items\":[\"item1\",\"item2\",\"item3\"]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with objects\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"users\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\tb.WriteString(\"name\", \"Bob\").\n\t\t\t\t\t\t\t\tWriteNumber(\"age\", 25)\n\t\t\t\t\t\t}).\n\t\t\t\t\t\t\tWriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\t\tb.WriteString(\"name\", \"Carol\").\n\t\t\t\t\t\t\t\t\tWriteNumber(\"age\", 27)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"users\":[{\"name\":\"Bob\",\"age\":25},{\"name\":\"Carol\",\"age\":27}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with various types\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"values\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteNumber(123).\n\t\t\t\t\t\t\tWriteBool(true).\n\t\t\t\t\t\t\tWriteNull()\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"values\":[\"item1\",123,true,null]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode := tt.build()\n\t\t\tvalue, err := Marshal(node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t}\n\t\t\tif string(value) != tt.expected {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tt.expected, string(value))\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "decode.gno", + "body": "// ref: https://github.com/spyzhov/ajson/blob/master/decode.go\n\npackage json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// This limits the max nesting depth to prevent stack overflow.\n// This is permitted by https://tools.ietf.org/html/rfc7159#section-9\nconst maxNestingDepth = 10000\n\n// Unmarshal parses the JSON-encoded data and returns a Node.\n// The data must be a valid JSON-encoded value.\n//\n// Usage:\n//\n//\tnode, err := json.Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err != nil {\n//\t\tufmt.Println(err)\n//\t}\n//\tprintln(node) // {\"key\": \"value\"}\nfunc Unmarshal(data []byte) (*Node, error) {\n\tbuf := newBuffer(data)\n\n\tvar (\n\t\tstate States\n\t\tkey *string\n\t\tcurrent *Node\n\t\tnesting int\n\t\tuseKey = func() **string {\n\t\t\ttmp := cptrs(key)\n\t\t\tkey = nil\n\t\t\treturn \u0026tmp\n\t\t}\n\t\terr error\n\t)\n\n\tif _, err = buf.first(); err != nil {\n\t\treturn nil, io.EOF\n\t}\n\n\tfor {\n\t\tstate = buf.getState()\n\t\tif state == __ {\n\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t}\n\n\t\t// region state machine\n\t\tif state \u003e= GO {\n\t\t\tswitch buf.state {\n\t\t\tcase ST: // string\n\t\t\t\tif current != nil \u0026\u0026 current.IsObject() \u0026\u0026 key == nil {\n\t\t\t\t\t// key detected\n\t\t\t\t\tif key, err = getString(buf); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tbuf.state = CO\n\t\t\t\t} else {\n\t\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, String, nesting, useKey())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\terr = buf.string(doubleQuote, false)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\t\t\t\t\tbuf.state = OK\n\t\t\t\t}\n\n\t\t\tcase MI, ZE, IN: // number\n\t\t\t\tcurrent, err = processNumericNode(current, buf, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase T1, F1: // boolean\n\t\t\t\tliteral := falseLiteral\n\t\t\t\tif buf.state == T1 {\n\t\t\t\t\tliteral = trueLiteral\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Boolean, literal, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase N1: // null\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Null, nullLiteral, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// region action\n\t\t\tswitch state {\n\t\t\tcase ec, cc: // \u003cempty\u003e }\n\t\t\t\tif key != nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Object)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase bc: // ]\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Array)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase co, bo: // { [\n\t\t\t\tvalTyp, bState := Object, OB\n\t\t\t\tif state == bo {\n\t\t\t\t\tvalTyp, bState = Array, AR\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, valTyp, nesting, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.state = bState\n\n\t\t\tcase cm: // ,\n\t\t\t\tif current == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif !current.isContainer() {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif current.IsObject() {\n\t\t\t\t\tbuf.state = KE // key expected\n\t\t\t\t} else {\n\t\t\t\t\tbuf.state = VA // value expected\n\t\t\t\t}\n\n\t\t\tcase cl: // :\n\t\t\t\tif current == nil || !current.IsObject() || key == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tbuf.state = VA\n\n\t\t\tdefault:\n\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t}\n\t\t}\n\n\t\tif buf.step() != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif _, err = buf.first(); err != nil {\n\t\t\terr = nil\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif current == nil || buf.state != OK {\n\t\treturn nil, io.EOF\n\t}\n\n\troot := current.root()\n\tif !root.ready() {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn root, err\n}\n\n// UnmarshalSafe parses the JSON-encoded data and returns a Node.\nfunc UnmarshalSafe(data []byte) (*Node, error) {\n\tvar safe []byte\n\tsafe = append(safe, data...)\n\treturn Unmarshal(safe)\n}\n\n// processNumericNode creates a new node, processes a numeric value,\n// sets the node's borders, and moves to the previous node.\nfunc processNumericNode(current *Node, buf *buffer, key **string) (*Node, error) {\n\tvar err error\n\tcurrent, err = createNode(current, buf, Number, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = buf.numeric(false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcurrent.borders[1] = buf.index\n\tif current.prev != nil {\n\t\tcurrent = current.prev\n\t}\n\n\tbuf.index -= 1\n\tbuf.state = OK\n\n\treturn current, nil\n}\n\n// processLiteralNode creates a new node, processes a literal value,\n// sets the node's borders, and moves to the previous node.\nfunc processLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteralValue []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tcurrent, nesting, err = createLiteralNode(current, buf, literalType, literalValue, useKey, nesting)\n\tif err != nil {\n\t\treturn nil, nesting, err\n\t}\n\treturn current, nesting, nil\n}\n\n// isValidContainerType checks if the current node is a valid container (object or array).\n// The container must satisfy the following conditions:\n// 1. The current node must not be nil.\n// 2. The current node must be an object or array.\n// 3. The current node must not be ready.\nfunc isValidContainerType(current *Node, nodeType ValueType) bool {\n\tswitch nodeType {\n\tcase Object:\n\t\treturn current != nil \u0026\u0026 current.IsObject() \u0026\u0026 !current.ready()\n\tcase Array:\n\t\treturn current != nil \u0026\u0026 current.IsArray() \u0026\u0026 !current.ready()\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// getString extracts a string from the buffer and advances the buffer index past the string.\nfunc getString(b *buffer) (*string, error) {\n\tstart := b.index\n\tif err := b.string(doubleQuote, false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalue, ok := Unquote(b.data[start:b.index+1], doubleQuote)\n\tif !ok {\n\t\treturn nil, unexpectedTokenError(b.data, start)\n\t}\n\n\treturn \u0026value, nil\n}\n\n// createNode creates a new node and sets the key if it is not nil.\nfunc createNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tkey **string,\n) (*Node, error) {\n\tvar err error\n\tcurrent, err = NewNode(current, buf, nodeType, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn current, nil\n}\n\n// createNestedNode creates a new nested node (array or object) and sets the key if it is not nil.\nfunc createNestedNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tnesting int,\n\tkey **string,\n) (*Node, int, error) {\n\tvar err error\n\tif nesting, err = checkNestingDepth(nesting); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\tif current, err = createNode(current, buf, nodeType, key); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\treturn current, nesting, nil\n}\n\n// createLiteralNode creates a new literal node and sets the key if it is not nil.\n// The literal is a byte slice that represents a boolean or null value.\nfunc createLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteral []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tif current, err = createNode(current, buf, literalType, useKey); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tif err = buf.word(literal); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, false)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// updateNode updates the current node and returns the previous node.\nfunc updateNode(\n\tcurrent *Node, buf *buffer, nesting int, decreaseLevel bool,\n) (*Node, int) {\n\tcurrent.borders[1] = buf.index + 1\n\n\tprev := current.prev\n\tif prev == nil {\n\t\treturn current, nesting\n\t}\n\n\tcurrent = prev\n\tif decreaseLevel {\n\t\tnesting--\n\t}\n\n\treturn current, nesting\n}\n\n// updateNodeAndSetBufferState updates the current node and sets the buffer state to OK.\nfunc updateNodeAndSetBufferState(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnesting int,\n\ttyp ValueType,\n) (*Node, int, error) {\n\tif !isValidContainerType(current, typ) {\n\t\treturn nil, nesting, unexpectedTokenError(buf.data, buf.index)\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// checkNestingDepth checks if the nesting depth is within the maximum allowed depth.\nfunc checkNestingDepth(nesting int) (int, error) {\n\tif nesting \u003e= maxNestingDepth {\n\t\treturn nesting, errors.New(\"maximum nesting depth exceeded\")\n\t}\n\n\treturn nesting + 1, nil\n}\n\nfunc unexpectedTokenError(data []byte, index int) error {\n\treturn ufmt.Errorf(\"unexpected token at index %d. data %b\", index, data)\n}\n" + }, + { + "name": "decode_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\ntype testNode struct {\n\tname string\n\tinput []byte\n\tvalue []byte\n\t_type ValueType\n}\n\nfunc simpleValid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.input, err.Error())\n\t} else if root == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t} else if root.nodeType != test._type {\n\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t} else if !bytes.Equal(root.source(), test.value) {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t}\n}\n\nfunc simpleInvalid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): error expected, got '%s'\", test.name, root.source())\n\t} else if root != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is not nil\", test.name)\n\t}\n}\n\nfunc simpleCorrupted(name string) *testNode {\n\treturn \u0026testNode{name: name, input: []byte(name)}\n}\n\nfunc TestUnmarshal_StringSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"blank\", input: []byte(\"\\\"\\\"\"), _type: String, value: []byte(\"\\\"\\\"\")},\n\t\t{name: \"char\", input: []byte(\"\\\"c\\\"\"), _type: String, value: []byte(\"\\\"c\\\"\")},\n\t\t{name: \"word\", input: []byte(\"\\\"cat\\\"\"), _type: String, value: []byte(\"\\\"cat\\\"\")},\n\t\t{name: \"spaces\", input: []byte(\" \\\"good cat or dog\\\"\\r\\n \"), _type: String, value: []byte(\"\\\"good cat or dog\\\"\")},\n\t\t{name: \"backslash\", input: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\")},\n\t\t{name: \"backslash 2\", input: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NumericSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"1\", input: []byte(\"1\"), _type: Number, value: []byte(\"1\")},\n\t\t{name: \"-1\", input: []byte(\"-1\"), _type: Number, value: []byte(\"-1\")},\n\n\t\t{name: \"1234567890\", input: []byte(\"1234567890\"), _type: Number, value: []byte(\"1234567890\")},\n\t\t{name: \"-123\", input: []byte(\"-123\"), _type: Number, value: []byte(\"-123\")},\n\n\t\t{name: \"123.456\", input: []byte(\"123.456\"), _type: Number, value: []byte(\"123.456\")},\n\t\t{name: \"-123.456\", input: []byte(\"-123.456\"), _type: Number, value: []byte(\"-123.456\")},\n\n\t\t{name: \"1e3\", input: []byte(\"1e3\"), _type: Number, value: []byte(\"1e3\")},\n\t\t{name: \"1e+3\", input: []byte(\"1e+3\"), _type: Number, value: []byte(\"1e+3\")},\n\t\t{name: \"1e-3\", input: []byte(\"1e-3\"), _type: Number, value: []byte(\"1e-3\")},\n\t\t{name: \"-1e3\", input: []byte(\"-1e3\"), _type: Number, value: []byte(\"-1e3\")},\n\t\t{name: \"-1e-3\", input: []byte(\"-1e-3\"), _type: Number, value: []byte(\"-1e-3\")},\n\n\t\t{name: \"1.123e3456\", input: []byte(\"1.123e3456\"), _type: Number, value: []byte(\"1.123e3456\")},\n\t\t{name: \"1.123e-3456\", input: []byte(\"1.123e-3456\"), _type: Number, value: []byte(\"1.123e-3456\")},\n\t\t{name: \"-1.123e3456\", input: []byte(\"-1.123e3456\"), _type: Number, value: []byte(\"-1.123e3456\")},\n\t\t{name: \"-1.123e-3456\", input: []byte(\"-1.123e-3456\"), _type: Number, value: []byte(\"-1.123e-3456\")},\n\n\t\t{name: \"1E3\", input: []byte(\"1E3\"), _type: Number, value: []byte(\"1E3\")},\n\t\t{name: \"1E-3\", input: []byte(\"1E-3\"), _type: Number, value: []byte(\"1E-3\")},\n\t\t{name: \"-1E3\", input: []byte(\"-1E3\"), _type: Number, value: []byte(\"-1E3\")},\n\t\t{name: \"-1E-3\", input: []byte(\"-1E-3\"), _type: Number, value: []byte(\"-1E-3\")},\n\n\t\t{name: \"1.123E3456\", input: []byte(\"1.123E3456\"), _type: Number, value: []byte(\"1.123E3456\")},\n\t\t{name: \"1.123E-3456\", input: []byte(\"1.123E-3456\"), _type: Number, value: []byte(\"1.123E-3456\")},\n\t\t{name: \"-1.123E3456\", input: []byte(\"-1.123E3456\"), _type: Number, value: []byte(\"-1.123E3456\")},\n\t\t{name: \"-1.123E-3456\", input: []byte(\"-1.123E-3456\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\n\t\t{name: \"-1.123E-3456 with spaces\", input: []byte(\" \\r -1.123E-3456 \\t\\n\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(test.input)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.name, err.Error())\n\t\t\t} else if root == nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t\t\t} else if root.nodeType != test._type {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t\t\t} else if !bytes.Equal(root.source(), test.value) {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_StringSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"white NL\", input: []byte(\"\\\"foo\\nbar\\\"\")},\n\t\t{name: \"white R\", input: []byte(\"\\\"foo\\rbar\\\"\")},\n\t\t{name: \"white Tab\", input: []byte(\"\\\"foo\\tbar\\\"\")},\n\t\t{name: \"wrong quotes\", input: []byte(\"'cat'\")},\n\t\t{name: \"double string\", input: []byte(\"\\\"Hello\\\" \\\"World\\\"\")},\n\t\t{name: \"quotes in quotes\", input: []byte(\"\\\"good \\\"cat\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"{}\", input: []byte(\"{}\"), _type: Object, value: []byte(\"{}\")},\n\t\t{name: `{ \\r\\n }`, input: []byte(\"{ \\r\\n }\"), _type: Object, value: []byte(\"{ \\r\\n }\")},\n\t\t{name: `{\"key\":1}`, input: []byte(`{\"key\":1}`), _type: Object, value: []byte(`{\"key\":1}`)},\n\t\t{name: `{\"key\":true}`, input: []byte(`{\"key\":true}`), _type: Object, value: []byte(`{\"key\":true}`)},\n\t\t{name: `{\"key\":\"value\"}`, input: []byte(`{\"key\":\"value\"}`), _type: Object, value: []byte(`{\"key\":\"value\"}`)},\n\t\t{name: `{\"foo\":\"bar\",\"baz\":\"foo\"}`, input: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`), _type: Object, value: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`)},\n\t\t{name: \"spaces\", input: []byte(` { \"foo\" : \"bar\" , \"baz\" : \"foo\" } `), _type: Object, value: []byte(`{ \"foo\" : \"bar\" , \"baz\" : \"foo\" }`)},\n\t\t{name: \"nested\", input: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`), _type: Object, value: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`)},\n\t\t{name: \"array\", input: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`), _type: Object, value: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`)},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"{{{\\\"key\\\": \\\"foo\\\"{{{{\"),\n\t\tsimpleCorrupted(\"}\"),\n\t\tsimpleCorrupted(\"{ }}}}}}}\"),\n\t\tsimpleCorrupted(\" }\"),\n\t\tsimpleCorrupted(\"{,}\"),\n\t\tsimpleCorrupted(\"{:}\"),\n\t\tsimpleCorrupted(\"{100000}\"),\n\t\tsimpleCorrupted(\"{1:1}\"),\n\t\tsimpleCorrupted(\"{'1:2,3:4'}\"),\n\t\tsimpleCorrupted(`{\"d\"}`),\n\t\tsimpleCorrupted(`{\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":}`),\n\t\tsimpleCorrupted(`{:\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":bar}`),\n\t\tsimpleCorrupted(`{\"foo\":\"bar\",}`),\n\t\tsimpleCorrupted(`{}{}`),\n\t\tsimpleCorrupted(`{},{}`),\n\t\tsimpleCorrupted(`{[},{]}`),\n\t\tsimpleCorrupted(`{[,]}`),\n\t\tsimpleCorrupted(`{[]}`),\n\t\tsimpleCorrupted(`{}1`),\n\t\tsimpleCorrupted(`1{}`),\n\t\tsimpleCorrupted(`{\"x\"::1}`),\n\t\tsimpleCorrupted(`{null:null}`),\n\t\tsimpleCorrupted(`{\"foo:\"bar\"}`),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NullSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"nul\", input: []byte(\"nul\")},\n\t\t{name: \"nil\", input: []byte(\"nil\")},\n\t\t{name: \"nill\", input: []byte(\"nill\")},\n\t\t{name: \"NILL\", input: []byte(\"NILL\")},\n\t\t{name: \"Null\", input: []byte(\"Null\")},\n\t\t{name: \"NULL\", input: []byte(\"NULL\")},\n\t\t{name: \"spaces\", input: []byte(\"Nu ll\")},\n\t\t{name: \"null1\", input: []byte(\"null1\")},\n\t\t{name: \"double\", input: []byte(\"null null\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"lower true\", input: []byte(\"true\"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"lower false\", input: []byte(\"false\"), _type: Boolean, value: []byte(\"false\")},\n\t\t{name: \"spaces true\", input: []byte(\" true\\r\\n \"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"spaces false\", input: []byte(\" false\\r\\n \"), _type: Boolean, value: []byte(\"false\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"tru\"),\n\t\tsimpleCorrupted(\"fals\"),\n\t\tsimpleCorrupted(\"tre\"),\n\t\tsimpleCorrupted(\"fal se\"),\n\t\tsimpleCorrupted(\"true false\"),\n\t\tsimpleCorrupted(\"True\"),\n\t\tsimpleCorrupted(\"TRUE\"),\n\t\tsimpleCorrupted(\"False\"),\n\t\tsimpleCorrupted(\"FALSE\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"[]\", input: []byte(\"[]\"), _type: Array, value: []byte(\"[]\")},\n\t\t{name: \"[1]\", input: []byte(\"[1]\"), _type: Array, value: []byte(\"[1]\")},\n\t\t{name: \"[1,2,3]\", input: []byte(\"[1,2,3]\"), _type: Array, value: []byte(\"[1,2,3]\")},\n\t\t{name: \"[1, 2, 3]\", input: []byte(\"[1, 2, 3]\"), _type: Array, value: []byte(\"[1, 2, 3]\")},\n\t\t{name: \"[1,[2],3]\", input: []byte(\"[1,[2],3]\"), _type: Array, value: []byte(\"[1,[2],3]\")},\n\t\t{name: \"[[],[],[]]\", input: []byte(\"[[],[],[]]\"), _type: Array, value: []byte(\"[[],[],[]]\")},\n\t\t{name: \"[[[[[]]]]]\", input: []byte(\"[[[[[]]]]]\"), _type: Array, value: []byte(\"[[[[[]]]]]\")},\n\t\t{name: \"[true,null,1,\\\"foo\\\",[]]\", input: []byte(\"[true,null,1,\\\"foo\\\",[]]\"), _type: Array, value: []byte(\"[true,null,1,\\\"foo\\\",[]]\")},\n\t\t{name: \"spaces\", input: []byte(\"\\n\\r [\\n1\\n ]\\r\\n\"), _type: Array, value: []byte(\"[\\n1\\n ]\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"[,]\"),\n\t\tsimpleCorrupted(\"[]\\\\\"),\n\t\tsimpleCorrupted(\"[1,]\"),\n\t\tsimpleCorrupted(\"[[]\"),\n\t\tsimpleCorrupted(\"[]]\"),\n\t\tsimpleCorrupted(\"1[]\"),\n\t\tsimpleCorrupted(\"[]1\"),\n\t\tsimpleCorrupted(\"[[]1]\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\n// Examples from https://json.org/example.html\nfunc TestUnmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tvalue string\n\t}{\n\t\t{\n\t\t\tname: \"glossary\",\n\t\t\tvalue: `{\n\t\t\t\t\"glossary\": {\n\t\t\t\t\t\"title\": \"example glossary\",\n\t\t\t\t\t\"GlossDiv\": {\n\t\t\t\t\t\t\"title\": \"S\",\n\t\t\t\t\t\t\"GlossList\": {\n\t\t\t\t\t\t\t\"GlossEntry\": {\n\t\t\t\t\t\t\t\t\"ID\": \"SGML\",\n\t\t\t\t\t\t\t\t\"SortAs\": \"SGML\",\n\t\t\t\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\n\t\t\t\t\t\t\t\t\"Acronym\": \"SGML\",\n\t\t\t\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\n\t\t\t\t\t\t\t\t\"GlossDef\": {\n\t\t\t\t\t\t\t\t\t\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n\t\t\t\t\t\t\t\t\t\"GlossSeeAlso\": [\"GML\", \"XML\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"GlossSee\": \"markup\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"menu\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"id\": \"file\",\n\t\t\t\t\"value\": \"File\",\n\t\t\t\t\"popup\": {\n\t\t\t\t \"menuitem\": [\n\t\t\t\t\t{\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n\t\t\t\t\t{\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n\t\t\t\t\t{\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n\t\t\t\t ]\n\t\t\t\t}\n\t\t\t}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"widget\",\n\t\t\tvalue: `{\"widget\": {\n\t\t\t\t\"debug\": \"on\",\n\t\t\t\t\"window\": {\n\t\t\t\t\t\"title\": \"Sample Konfabulator Widget\",\n\t\t\t\t\t\"name\": \"main_window\",\n\t\t\t\t\t\"width\": 500,\n\t\t\t\t\t\"height\": 500\n\t\t\t\t},\n\t\t\t\t\"image\": { \n\t\t\t\t\t\"src\": \"Images/Sun.png\",\n\t\t\t\t\t\"name\": \"sun1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 250,\n\t\t\t\t\t\"alignment\": \"center\"\n\t\t\t\t},\n\t\t\t\t\"text\": {\n\t\t\t\t\t\"data\": \"Click Here\",\n\t\t\t\t\t\"size\": 36,\n\t\t\t\t\t\"style\": \"bold\",\n\t\t\t\t\t\"name\": \"text1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 100,\n\t\t\t\t\t\"alignment\": \"center\",\n\t\t\t\t\t\"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n\t\t\t\t}\n\t\t\t}} `,\n\t\t},\n\t\t{\n\t\t\tname: \"web-app\",\n\t\t\tvalue: `{\"web-app\": {\n\t\t\t\t\"servlet\": [ \n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxCDS\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.CDSServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t \"configGlossary:installationAt\": \"Philadelphia, PA\",\n\t\t\t\t\t \"configGlossary:adminEmail\": \"ksm@pobox.com\",\n\t\t\t\t\t \"configGlossary:poweredBy\": \"Cofax\",\n\t\t\t\t\t \"configGlossary:poweredByIcon\": \"/images/cofax.gif\",\n\t\t\t\t\t \"configGlossary:staticPath\": \"/content/static\",\n\t\t\t\t\t \"templateProcessorClass\": \"org.cofax.WysiwygTemplate\",\n\t\t\t\t\t \"templateLoaderClass\": \"org.cofax.FilesTemplateLoader\",\n\t\t\t\t\t \"templatePath\": \"templates\",\n\t\t\t\t\t \"templateOverridePath\": \"\",\n\t\t\t\t\t \"defaultListTemplate\": \"listTemplate.htm\",\n\t\t\t\t\t \"defaultFileTemplate\": \"articleTemplate.htm\",\n\t\t\t\t\t \"useJSP\": false,\n\t\t\t\t\t \"jspListTemplate\": \"listTemplate.jsp\",\n\t\t\t\t\t \"jspFileTemplate\": \"articleTemplate.jsp\",\n\t\t\t\t\t \"cachePackageTagsTrack\": 200,\n\t\t\t\t\t \"cachePackageTagsStore\": 200,\n\t\t\t\t\t \"cachePackageTagsRefresh\": 60,\n\t\t\t\t\t \"cacheTemplatesTrack\": 100,\n\t\t\t\t\t \"cacheTemplatesStore\": 50,\n\t\t\t\t\t \"cacheTemplatesRefresh\": 15,\n\t\t\t\t\t \"cachePagesTrack\": 200,\n\t\t\t\t\t \"cachePagesStore\": 100,\n\t\t\t\t\t \"cachePagesRefresh\": 10,\n\t\t\t\t\t \"cachePagesDirtyRead\": 10,\n\t\t\t\t\t \"searchEngineListTemplate\": \"forSearchEnginesList.htm\",\n\t\t\t\t\t \"searchEngineFileTemplate\": \"forSearchEngines.htm\",\n\t\t\t\t\t \"searchEngineRobotsDb\": \"WEB-INF/robots.db\",\n\t\t\t\t\t \"useDataStore\": true,\n\t\t\t\t\t \"dataStoreClass\": \"org.cofax.SqlDataStore\",\n\t\t\t\t\t \"redirectionClass\": \"org.cofax.SqlRedirection\",\n\t\t\t\t\t \"dataStoreName\": \"cofax\",\n\t\t\t\t\t \"dataStoreDriver\": \"com.microsoft.jdbc.sqlserver.SQLServerDriver\",\n\t\t\t\t\t \"dataStoreUrl\": \"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\",\n\t\t\t\t\t \"dataStoreUser\": \"sa\",\n\t\t\t\t\t \"dataStorePassword\": \"dataStoreTestQuery\",\n\t\t\t\t\t \"dataStoreTestQuery\": \"SET NOCOUNT ON;select test='test';\",\n\t\t\t\t\t \"dataStoreLogFile\": \"/usr/local/tomcat/logs/datastore.log\",\n\t\t\t\t\t \"dataStoreInitConns\": 10,\n\t\t\t\t\t \"dataStoreMaxConns\": 100,\n\t\t\t\t\t \"dataStoreConnUsageLimit\": 100,\n\t\t\t\t\t \"dataStoreLogLevel\": \"debug\",\n\t\t\t\t\t \"maxUrlLength\": 500}},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxEmail\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.EmailServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t\"mailHost\": \"mail1\",\n\t\t\t\t\t\"mailHostOverride\": \"mail2\"}},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxAdmin\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.AdminServlet\"},\n\t\t\t \n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"fileServlet\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.FileServlet\"},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxTools\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cms.CofaxToolsServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t \"templatePath\": \"toolstemplates/\",\n\t\t\t\t\t \"log\": 1,\n\t\t\t\t\t \"logLocation\": \"/usr/local/tomcat/logs/CofaxTools.log\",\n\t\t\t\t\t \"logMaxSize\": \"\",\n\t\t\t\t\t \"dataLog\": 1,\n\t\t\t\t\t \"dataLogLocation\": \"/usr/local/tomcat/logs/dataLog.log\",\n\t\t\t\t\t \"dataLogMaxSize\": \"\",\n\t\t\t\t\t \"removePageCache\": \"/content/admin/remove?cache=pages\u0026id=\",\n\t\t\t\t\t \"removeTemplateCache\": \"/content/admin/remove?cache=templates\u0026id=\",\n\t\t\t\t\t \"fileTransferFolder\": \"/usr/local/tomcat/webapps/content/fileTransferFolder\",\n\t\t\t\t\t \"lookInContext\": 1,\n\t\t\t\t\t \"adminGroupID\": 4,\n\t\t\t\t\t \"betaServer\": true}}],\n\t\t\t\t\"servlet-mapping\": {\n\t\t\t\t \"cofaxCDS\": \"/\",\n\t\t\t\t \"cofaxEmail\": \"/cofaxutil/aemail/*\",\n\t\t\t\t \"cofaxAdmin\": \"/admin/*\",\n\t\t\t\t \"fileServlet\": \"/static/*\",\n\t\t\t\t \"cofaxTools\": \"/tools/*\"},\n\t\t\t \n\t\t\t\t\"taglib\": {\n\t\t\t\t \"taglib-uri\": \"cofax.tld\",\n\t\t\t\t \"taglib-location\": \"/WEB-INF/tlds/cofax.tld\"}}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"SVG Viewer\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"header\": \"SVG Viewer\",\n\t\t\t\t\"items\": [\n\t\t\t\t\t{\"id\": \"Open\"},\n\t\t\t\t\t{\"id\": \"OpenNew\", \"label\": \"Open New\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},\n\t\t\t\t\t{\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},\n\t\t\t\t\t{\"id\": \"OriginalView\", \"label\": \"Original View\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Quality\"},\n\t\t\t\t\t{\"id\": \"Pause\"},\n\t\t\t\t\t{\"id\": \"Mute\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Find\", \"label\": \"Find...\"},\n\t\t\t\t\t{\"id\": \"FindAgain\", \"label\": \"Find Again\"},\n\t\t\t\t\t{\"id\": \"Copy\"},\n\t\t\t\t\t{\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},\n\t\t\t\t\t{\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSVG\", \"label\": \"View SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSource\", \"label\": \"View Source\"},\n\t\t\t\t\t{\"id\": \"SaveAs\", \"label\": \"Save As\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Help\"},\n\t\t\t\t\t{\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}\n\t\t\t\t]\n\t\t\t}}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := Unmarshal([]byte(test.value))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshalSafe(t *testing.T) {\n\tjson := []byte(`{ \"store\": {\n\t\t\"book\": [ \n\t\t { \"category\": \"reference\",\n\t\t\t\"author\": \"Nigel Rees\",\n\t\t\t\"title\": \"Sayings of the Century\",\n\t\t\t\"price\": 8.95\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"Evelyn Waugh\",\n\t\t\t\"title\": \"Sword of Honour\",\n\t\t\t\"price\": 12.99\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"Herman Melville\",\n\t\t\t\"title\": \"Moby Dick\",\n\t\t\t\"isbn\": \"0-553-21311-3\",\n\t\t\t\"price\": 8.99\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"J. R. R. Tolkien\",\n\t\t\t\"title\": \"The Lord of the Rings\",\n\t\t\t\"isbn\": \"0-395-19395-8\",\n\t\t\t\"price\": 22.99\n\t\t }\n\t\t],\n\t\t\"bicycle\": {\n\t\t \"color\": \"red\",\n\t\t \"price\": 19.95\n\t\t}\n\t }\n\t}`)\n\tsafe, err := UnmarshalSafe(json)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t} else if safe == nil {\n\t\tt.Errorf(\"Error on Unmarshal: safe is nil\")\n\t} else {\n\t\troot, err := Unmarshal(json)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t} else if root == nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t\t} else if !bytes.Equal(root.source(), safe.source()) {\n\t\t\tt.Errorf(\"Error on UnmarshalSafe: values not same\")\n\t\t}\n\t}\n}\n\n// BenchmarkGoStdUnmarshal-8 \t 61698\t 19350 ns/op\t 288 B/op\t 6 allocs/op\n// BenchmarkUnmarshal-8 \t 45620\t 26165 ns/op\t 21889 B/op\t 367 allocs/op\n//\n// type bench struct {\n// \tName string `json:\"name\"`\n// \tValue int `json:\"value\"`\n// }\n\n// func BenchmarkGoStdUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\terr := json.Unmarshal(data, \u0026bench{})\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n\n// func BenchmarkUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\t_, err := Unmarshal(data)\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n" + }, + { + "name": "encode.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Marshal returns the JSON encoding of a Node.\nfunc Marshal(node *Node) ([]byte, error) {\n\tvar (\n\t\tbuf bytes.Buffer\n\t\tsVal string\n\t\tbVal bool\n\t\tnVal float64\n\t\toVal []byte\n\t\terr error\n\t)\n\n\tif node == nil {\n\t\treturn nil, errors.New(\"node is nil\")\n\t}\n\n\tif !node.modified \u0026\u0026 !node.ready() {\n\t\treturn nil, errors.New(\"node is not ready\")\n\t}\n\n\tif !node.modified \u0026\u0026 node.ready() {\n\t\tbuf.Write(node.source())\n\t}\n\n\tif node.modified {\n\t\tswitch node.nodeType {\n\t\tcase Null:\n\t\t\tbuf.Write(nullLiteral)\n\n\t\tcase Number:\n\t\t\tnVal, err = node.GetNumeric()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnum := strconv.FormatFloat(nVal, 'f', -1, 64)\n\t\t\tbuf.WriteString(num)\n\n\t\tcase String:\n\t\t\tsVal, err = node.GetString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tquoted := ufmt.Sprintf(\"%s\", strconv.Quote(sVal))\n\t\t\tbuf.WriteString(quoted)\n\n\t\tcase Boolean:\n\t\t\tbVal, err = node.GetBool()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbStr := ufmt.Sprintf(\"%t\", bVal)\n\t\t\tbuf.WriteString(bStr)\n\n\t\tcase Array:\n\t\t\tbuf.WriteByte(bracketOpen)\n\n\t\t\tfor i := 0; i \u003c len(node.next); i++ {\n\t\t\t\tif i != 0 {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t}\n\n\t\t\t\telem, ok := node.next[strconv.Itoa(i)]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, ufmt.Errorf(\"array element %d is not found\", i)\n\t\t\t\t}\n\n\t\t\t\toVal, err = Marshal(elem)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(bracketClose)\n\n\t\tcase Object:\n\t\t\tbuf.WriteByte(curlyOpen)\n\n\t\t\tbVal = false\n\t\t\tfor k, v := range node.next {\n\t\t\t\tif bVal {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t} else {\n\t\t\t\t\tbVal = true\n\t\t\t\t}\n\n\t\t\t\tkey := ufmt.Sprintf(\"%s\", strconv.Quote(k))\n\t\t\t\tbuf.WriteString(key)\n\t\t\t\tbuf.WriteByte(colon)\n\n\t\t\t\toVal, err = Marshal(v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(curlyClose)\n\t\t}\n\t}\n\n\treturn buf.Bytes(), nil\n}\n" + }, + { + "name": "encode_test.gno", + "body": "package json\n\nimport \"testing\"\n\nfunc TestMarshal_Primitive(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t}{\n\t\t{\n\t\t\tname: \"null\",\n\t\t\tnode: NullNode(\"\"),\n\t\t},\n\t\t{\n\t\t\tname: \"true\",\n\t\t\tnode: BoolNode(\"\", true),\n\t\t},\n\t\t{\n\t\t\tname: \"false\",\n\t\t\tnode: BoolNode(\"\", false),\n\t\t},\n\t\t{\n\t\t\tname: `\"string\"`,\n\t\t\tnode: StringNode(\"\", \"string\"),\n\t\t},\n\t\t{\n\t\t\tname: `\"one \\\"encoded\\\" string\"`,\n\t\t\tnode: StringNode(\"\", `one \"encoded\" string`),\n\t\t},\n\t\t{\n\t\t\tname: `{\"foo\":\"bar\"}`,\n\t\t\tnode: ObjectNode(\"\", map[string]*Node{\n\t\t\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"42\",\n\t\t\tnode: NumberNode(\"\", 42),\n\t\t},\n\t\t{\n\t\t\tname: \"3.14\",\n\t\t\tnode: NumberNode(\"\", 3.14),\n\t\t},\n\t\t{\n\t\t\tname: `[1,2,3]`,\n\t\t\tnode: ArrayNode(\"\", []*Node{\n\t\t\t\tNumberNode(\"0\", 1),\n\t\t\t\tNumberNode(\"2\", 2),\n\t\t\t\tNumberNode(\"3\", 3),\n\t\t\t}),\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t} else if string(value) != test.name {\n\t\t\t\tt.Errorf(\"wrong result: '%s', expected '%s'\", value, test.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Object(t *testing.T) {\n\tnode := ObjectNode(\"\", map[string]*Node{\n\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\"baz\": NumberNode(\"baz\", 100500),\n\t\t\"qux\": NullNode(\"qux\"),\n\t})\n\n\tmustKey := []string{\"foo\", \"baz\", \"qux\"}\n\n\tvalue, err := Marshal(node)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\t// the order of keys in the map is not guaranteed\n\t// so we need to unmarshal the result and check the keys\n\tdecoded, err := Unmarshal(value)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\tfor _, key := range mustKey {\n\t\tif node, err := decoded.GetKey(key); err != nil {\n\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t} else {\n\t\t\tif node == nil {\n\t\t\t\tt.Errorf(\"node is nil\")\n\t\t\t} else if node.key == nil {\n\t\t\t\tt.Errorf(\"key is nil\")\n\t\t\t} else if *node.key != key {\n\t\t\t\tt.Errorf(\"wrong key: '%s', expected '%s'\", *node.key, key)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc valueNode(prev *Node, key string, typ ValueType, val interface{}) *Node {\n\tcurr := \u0026Node{\n\t\tprev: prev,\n\t\tdata: nil,\n\t\tkey: \u0026key,\n\t\tborders: [2]int{0, 0},\n\t\tvalue: val,\n\t\tmodified: true,\n\t}\n\n\tif val != nil {\n\t\tcurr.nodeType = typ\n\t}\n\n\treturn curr\n}\n\nfunc TestMarshal_Errors(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode func() (node *Node)\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"broken\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = Must(Unmarshal([]byte(`{}`)))\n\t\t\t\tnode.borders[1] = 0\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Numeric\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Number, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"String\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", String, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Bool\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Boolean, 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_1\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = ArrayNode(\"\", nil)\n\t\t\t\tnode.next[\"1\"] = NullNode(\"1\")\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_2\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ArrayNode(\"\", []*Node{valueNode(nil, \"\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Object\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ObjectNode(\"\", map[string]*Node{\"key\": valueNode(nil, \"key\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node())\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected error\")\n\t\t\t} else if len(value) != 0 {\n\t\t\t\tt.Errorf(\"wrong result\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Nil(t *testing.T) {\n\t_, err := Marshal(nil)\n\tif err == nil {\n\t\tt.Error(\"Expected error for nil node, but got nil\")\n\t}\n}\n\nfunc TestMarshal_NotModified(t *testing.T) {\n\tnode := \u0026Node{}\n\t_, err := Marshal(node)\n\tif err == nil {\n\t\tt.Error(\"Expected error for not modified node, but got nil\")\n\t}\n}\n\nfunc TestMarshalCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey: stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tnext: map[string]*Node{\n\t\t\t\"next\": nil,\n\t\t},\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey: stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tprev: node1,\n\t}\n\n\tnode1.next[\"next\"] = node2\n\n\t_, err := Marshal(node1)\n\tif err == nil {\n\t\tt.Error(\"Expected error for cycle reference, but got nil\")\n\t}\n}\n\nfunc TestMarshalNoCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey: stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tvalue: \"value1\",\n\t\tmodified: true,\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey: stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tvalue: \"value2\",\n\t\tmodified: true,\n\t}\n\n\t_, err := Marshal(node1)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n\n\t_, err = Marshal(node2)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n}\n\nfunc stringPtr(s string) *string {\n\treturn \u0026s\n}\n" + }, + { + "name": "errors.gno", + "body": "package json\n\nimport \"errors\"\n\nvar (\n\terrNilNode = errors.New(\"node is nil\")\n\terrNotArrayNode = errors.New(\"node is not array\")\n\terrNotBoolNode = errors.New(\"node is not boolean\")\n\terrNotNullNode = errors.New(\"node is not null\")\n\terrNotNumberNode = errors.New(\"node is not number\")\n\terrNotObjectNode = errors.New(\"node is not object\")\n\terrNotStringNode = errors.New(\"node is not string\")\n\terrInvalidToken = errors.New(\"invalid token\")\n\terrIndexNotFound = errors.New(\"index not found\")\n\terrInvalidAppend = errors.New(\"can't append value to non-appendable node\")\n\terrInvalidAppendCycle = errors.New(\"appending value to itself or its children or parents will cause a cycle\")\n\terrInvalidEscapeSequence = errors.New(\"invalid escape sequence\")\n\terrInvalidStringValue = errors.New(\"invalid string value\")\n\terrEmptyBooleanNode = errors.New(\"boolean node is empty\")\n\terrEmptyStringNode = errors.New(\"string node is empty\")\n\terrKeyRequired = errors.New(\"key is required for object\")\n\terrUnmatchedParenthesis = errors.New(\"mismatched bracket or parenthesis\")\n\terrUnmatchedQuotePath = errors.New(\"unmatched quote in path\")\n)\n\nvar (\n\terrInvalidStringInput = errors.New(\"invalid string input\")\n\terrMalformedBooleanValue = errors.New(\"malformed boolean value\")\n\terrEmptyByteSlice = errors.New(\"empty byte slice\")\n\terrInvalidExponentValue = errors.New(\"invalid exponent value\")\n\terrNonDigitCharacters = errors.New(\"non-digit characters found\")\n\terrNumericRangeExceeded = errors.New(\"numeric value exceeds the range limit\")\n\terrMultipleDecimalPoints = errors.New(\"multiple decimal points found\")\n)\n" + }, + { + "name": "escape.gno", + "body": "package json\n\nimport (\n\t\"unicode/utf8\"\n)\n\nconst (\n\tsupplementalPlanesOffset = 0x10000\n\thighSurrogateOffset = 0xD800\n\tlowSurrogateOffset = 0xDC00\n\tsurrogateEnd = 0xDFFF\n\tbasicMultilingualPlaneOffset = 0xFFFF\n\tbadHex = -1\n\n\tsingleUnicodeEscapeLen = 6\n\tsurrogatePairLen = 12\n)\n\nvar hexLookupTable = [256]int{\n\t'0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, '4': 0x4,\n\t'5': 0x5, '6': 0x6, '7': 0x7, '8': 0x8, '9': 0x9,\n\t'A': 0xA, 'B': 0xB, 'C': 0xC, 'D': 0xD, 'E': 0xE, 'F': 0xF,\n\t'a': 0xA, 'b': 0xB, 'c': 0xC, 'd': 0xD, 'e': 0xE, 'f': 0xF,\n\t// Fill unspecified index-value pairs with key and value of -1\n\t'G': -1, 'H': -1, 'I': -1, 'J': -1,\n\t'K': -1, 'L': -1, 'M': -1, 'N': -1,\n\t'O': -1, 'P': -1, 'Q': -1, 'R': -1,\n\t'S': -1, 'T': -1, 'U': -1, 'V': -1,\n\t'W': -1, 'X': -1, 'Y': -1, 'Z': -1,\n\t'g': -1, 'h': -1, 'i': -1, 'j': -1,\n\t'k': -1, 'l': -1, 'm': -1, 'n': -1,\n\t'o': -1, 'p': -1, 'q': -1, 'r': -1,\n\t's': -1, 't': -1, 'u': -1, 'v': -1,\n\t'w': -1, 'x': -1, 'y': -1, 'z': -1,\n}\n\nfunc h2i(c byte) int {\n\treturn hexLookupTable[c]\n}\n\n// Unescape takes an input byte slice, processes it to Unescape certain characters,\n// and writes the result into an output byte slice.\n//\n// it returns the processed slice and any error encountered during the Unescape operation.\nfunc Unescape(input, output []byte) ([]byte, error) {\n\t// ensure the output slice has enough capacity to hold the input slice.\n\tinputLen := len(input)\n\tif cap(output) \u003c inputLen {\n\t\toutput = make([]byte, inputLen)\n\t}\n\n\tinPos, outPos := 0, 0\n\n\tfor inPos \u003c len(input) {\n\t\tc := input[inPos]\n\t\tif c != backSlash {\n\t\t\toutput[outPos] = c\n\t\t\tinPos++\n\t\t\toutPos++\n\t\t} else {\n\t\t\t// process escape sequence\n\t\t\tinLen, outLen, err := processEscapedUTF8(input[inPos:], output[outPos:])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tinPos += inLen\n\t\t\toutPos += outLen\n\t\t}\n\t}\n\n\treturn output[:outPos], nil\n}\n\n// isSurrogatePair returns true if the rune is a surrogate pair.\n//\n// A surrogate pairs are used in UTF-16 encoding to encode characters\n// outside the Basic Multilingual Plane (BMP).\nfunc isSurrogatePair(r rune) bool {\n\treturn highSurrogateOffset \u003c= r \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// isHighSurrogate checks if the rune is a high surrogate (U+D800 to U+DBFF).\nfunc isHighSurrogate(r rune) bool {\n\treturn r \u003e= highSurrogateOffset \u0026\u0026 r \u003c= 0xDBFF\n}\n\n// isLowSurrogate checks if the rune is a low surrogate (U+DC00 to U+DFFF).\nfunc isLowSurrogate(r rune) bool {\n\treturn r \u003e= lowSurrogateOffset \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// combineSurrogates reconstruct the original unicode code points in the\n// supplemental plane by combinin the high and low surrogate.\n//\n// The hight surrogate in the range from U+D800 to U+DBFF,\n// and the low surrogate in the range from U+DC00 to U+DFFF.\n//\n// The formula to combine the surrogates is:\n// (high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000\nfunc combineSurrogates(high, low rune) rune {\n\treturn ((high - highSurrogateOffset) \u003c\u003c 10) + (low - lowSurrogateOffset) + supplementalPlanesOffset\n}\n\n// deocdeSingleUnicodeEscape decodes a unicode escape sequence (e.g., \\uXXXX) into a rune.\nfunc decodeSingleUnicodeEscape(b []byte) (rune, bool) {\n\tif len(b) \u003c 6 {\n\t\treturn utf8.RuneError, false\n\t}\n\n\t// convert hex to decimal\n\th1, h2, h3, h4 := h2i(b[2]), h2i(b[3]), h2i(b[4]), h2i(b[5])\n\tif h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex {\n\t\treturn utf8.RuneError, false\n\t}\n\n\treturn rune(h1\u003c\u003c12 + h2\u003c\u003c8 + h3\u003c\u003c4 + h4), true\n}\n\n// decodeUnicodeEscape decodes a Unicode escape sequence from a byte slice.\n// It handles both single Unicode escape sequences and surrogate pairs.\nfunc decodeUnicodeEscape(b []byte) (rune, int) {\n\t// decode the first Unicode escape sequence.\n\tr, ok := decodeSingleUnicodeEscape(b)\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is within the BMP and not a surrogate, return it\n\tif r \u003c= basicMultilingualPlaneOffset \u0026\u0026 !isSurrogatePair(r) {\n\t\treturn r, 6\n\t}\n\n\tif !isHighSurrogate(r) {\n\t\t// invalid surrogate pair.\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is a high surrogate, need to decode the next escape sequence.\n\n\t// ensure there are enough bytes for the next escape sequence.\n\tif len(b) \u003c surrogatePairLen {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// decode the second Unicode escape sequence.\n\tr2, ok := decodeSingleUnicodeEscape(b[singleUnicodeEscapeLen:])\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// check if the second rune is a low surrogate.\n\tif isLowSurrogate(r2) {\n\t\tcombined := combineSurrogates(r, r2)\n\t\treturn combined, surrogatePairLen\n\t}\n\treturn utf8.RuneError, -1\n}\n\nvar escapeByteSet = [256]byte{\n\t'\"': doubleQuote,\n\t'\\\\': backSlash,\n\t'/': slash,\n\t'b': backSpace,\n\t'f': formFeed,\n\t'n': newLine,\n\t'r': carriageReturn,\n\t't': tab,\n}\n\n// Unquote takes a byte slice and unquotes it by removing\n// the surrounding quotes and unescaping the contents.\nfunc Unquote(s []byte, border byte) (string, bool) {\n\ts, ok := unquoteBytes(s, border)\n\treturn string(s), ok\n}\n\n// unquoteBytes takes a byte slice and unquotes it by removing\nfunc unquoteBytes(s []byte, border byte) ([]byte, bool) {\n\tif len(s) \u003c 2 || s[0] != border || s[len(s)-1] != border {\n\t\treturn nil, false\n\t}\n\n\ts = s[1 : len(s)-1]\n\n\tr := 0\n\tfor r \u003c len(s) {\n\t\tc := s[r]\n\n\t\tif c == backSlash || c == border || c \u003c 0x20 {\n\t\t\tbreak\n\t\t}\n\n\t\tif c \u003c utf8.RuneSelf {\n\t\t\tr++\n\t\t\tcontinue\n\t\t}\n\n\t\trr, size := utf8.DecodeRune(s[r:])\n\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\tbreak\n\t\t}\n\n\t\tr += size\n\t}\n\n\tif r == len(s) {\n\t\treturn s, true\n\t}\n\n\tutfDoubleMax := utf8.UTFMax * 2\n\tb := make([]byte, len(s)+utfDoubleMax)\n\tw := copy(b, s[0:r])\n\n\tfor r \u003c len(s) {\n\t\tif w \u003e= len(b)-utf8.UTFMax {\n\t\t\tnb := make([]byte, utfDoubleMax+(2*len(b)))\n\t\t\tcopy(nb, b)\n\t\t\tb = nb\n\t\t}\n\n\t\tc := s[r]\n\t\tif c == backSlash {\n\t\t\tr++\n\t\t\tif r \u003e= len(s) {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tif s[r] == 'u' {\n\t\t\t\trr, res := decodeUnicodeEscape(s[r-1:])\n\t\t\t\tif res \u003c 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t\t\tr += 5\n\t\t\t} else {\n\t\t\t\tdecode := escapeByteSet[s[r]]\n\t\t\t\tif decode == 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tif decode == doubleQuote || decode == backSlash || decode == slash {\n\t\t\t\t\tdecode = s[r]\n\t\t\t\t}\n\n\t\t\t\tb[w] = decode\n\t\t\t\tr++\n\t\t\t\tw++\n\t\t\t}\n\t\t} else if c == border || c \u003c 0x20 {\n\t\t\treturn nil, false\n\t\t} else if c \u003c utf8.RuneSelf {\n\t\t\tb[w] = c\n\t\t\tr++\n\t\t\tw++\n\t\t} else {\n\t\t\trr, size := utf8.DecodeRune(s[r:])\n\n\t\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tr += size\n\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t}\n\t}\n\n\treturn b[:w], true\n}\n\n// processEscapedUTF8 converts escape sequences to UTF-8 characters.\n// It decodes Unicode escape sequences (\\uXXXX) to UTF-8 and\n// converts standard escape sequences (e.g., \\n) to their corresponding special characters.\nfunc processEscapedUTF8(in, out []byte) (int, int, error) {\n\tif len(in) \u003c 2 || in[0] != backSlash {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\tescapeSeqLen := 2\n\tescapeChar := in[1]\n\n\tif escapeChar != 'u' {\n\t\tval := escapeByteSet[escapeChar]\n\t\tif val == 0 {\n\t\t\treturn -1, -1, errInvalidEscapeSequence\n\t\t}\n\n\t\tout[0] = val\n\t\treturn escapeSeqLen, 1, nil\n\t}\n\n\tr, size := decodeUnicodeEscape(in)\n\tif size == -1 {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\toutLen := utf8.EncodeRune(out, r)\n\n\treturn size, outLen, nil\n}\n" + }, + { + "name": "escape_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestHexToInt(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tc byte\n\t\twant int\n\t}{\n\t\t{\"Digit 0\", '0', 0},\n\t\t{\"Digit 9\", '9', 9},\n\t\t{\"Uppercase A\", 'A', 10},\n\t\t{\"Uppercase F\", 'F', 15},\n\t\t{\"Lowercase a\", 'a', 10},\n\t\t{\"Lowercase f\", 'f', 15},\n\t\t{\"Invalid character1\", 'g', badHex},\n\t\t{\"Invalid character2\", 'G', badHex},\n\t\t{\"Invalid character3\", 'z', badHex},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := h2i(tt.c); got != tt.want {\n\t\t\t\tt.Errorf(\"h2i() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsSurrogatePair(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tr rune\n\t\texpected bool\n\t}{\n\t\t{\"high surrogate start\", 0xD800, true},\n\t\t{\"high surrogate end\", 0xDBFF, true},\n\t\t{\"low surrogate start\", 0xDC00, true},\n\t\t{\"low surrogate end\", 0xDFFF, true},\n\t\t{\"Non-surrogate\", 0x0000, false},\n\t\t{\"Non-surrogate 2\", 0xE000, false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif got := isSurrogatePair(tc.r); got != tc.expected {\n\t\t\t\tt.Errorf(\"isSurrogate() = %v, want %v\", got, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCombineSurrogates(t *testing.T) {\n\ttestCases := []struct {\n\t\thigh, low rune\n\t\texpected rune\n\t}{\n\t\t{0xD83D, 0xDC36, 0x1F436}, // 🐶 U+1F436 DOG FACE\n\t\t{0xD83D, 0xDE00, 0x1F600}, // 😀 U+1F600 GRINNING FACE\n\t\t{0xD83C, 0xDF03, 0x1F303}, // 🌃 U+1F303 NIGHT WITH STARS\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := combineSurrogates(tc.high, tc.low)\n\t\tif result != tc.expected {\n\t\t\tt.Errorf(\"combineSurrogates(%U, %U) = %U; want %U\", tc.high, tc.low, result, tc.expected)\n\t\t}\n\t}\n}\n\nfunc TestDecodeSingleUnicodeEscape(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput []byte\n\t\texpected rune\n\t\tisValid bool\n\t}{\n\t\t// valid unicode escape sequences\n\t\t{[]byte(`\\u0041`), 'A', true},\n\t\t{[]byte(`\\u03B1`), 'α', true},\n\t\t{[]byte(`\\u00E9`), 'é', true}, // valid non-English character\n\t\t{[]byte(`\\u0021`), '!', true}, // valid special character\n\t\t{[]byte(`\\uFF11`), '1', true},\n\t\t{[]byte(`\\uD83D`), 0xD83D, true},\n\t\t{[]byte(`\\uDE03`), 0xDE03, true},\n\n\t\t// invalid unicode escape sequences\n\t\t{[]byte(`\\u004`), utf8.RuneError, false}, // too short\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, false}, // invalid hex\n\t\t{[]byte(`\\u00G1`), utf8.RuneError, false}, // non-hex character\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult, isValid := decodeSingleUnicodeEscape(tc.input)\n\t\tif result != tc.expected || isValid != tc.isValid {\n\t\t\tt.Errorf(\"decodeSingleUnicodeEscape(%s) = (%U, %v); want (%U, %v)\", tc.input, result, isValid, tc.expected, tc.isValid)\n\t\t}\n\t}\n}\n\nfunc TestDecodeUnicodeEscape(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpected rune\n\t\tsize int\n\t}{\n\t\t{[]byte(`\\u0041`), 'A', 6},\n\t\t{[]byte(`\\uD83D\\uDE00`), 0x1F600, 12}, // 😀\n\t\t{[]byte(`\\uD834\\uDD1E`), 0x1D11E, 12}, // 𝄞\n\t\t{[]byte(`\\uFFFF`), '\\uFFFF', 6},\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, -1},\n\t\t{[]byte(`\\uD800`), utf8.RuneError, -1}, // single high surrogate\n\t\t{[]byte(`\\uDC00`), utf8.RuneError, -1}, // single low surrogate\n\t\t{[]byte(`\\uD800\\uDC00`), 0x10000, 12}, // First code point above U+FFFF\n\t\t{[]byte(`\\uDBFF\\uDFFF`), 0x10FFFF, 12}, // Maximum code point\n\t\t{[]byte(`\\uD83D\\u0041`), utf8.RuneError, -1}, // invalid surrogate pair\n\t}\n\n\tfor _, tc := range tests {\n\t\tr, size := decodeUnicodeEscape(tc.input)\n\t\tif r != tc.expected || size != tc.size {\n\t\t\tt.Errorf(\"decodeUnicodeEscape(%q) = (%U, %d); want (%U, %d)\", tc.input, r, size, tc.expected, tc.size)\n\t\t}\n\t}\n}\n\nfunc TestUnescapeToUTF8(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpectedIn int\n\t\texpectedOut int\n\t\tisError bool\n\t}{\n\t\t// valid escape sequences\n\t\t{[]byte(`\\n`), 2, 1, false},\n\t\t{[]byte(`\\t`), 2, 1, false},\n\t\t{[]byte(`\\u0041`), 6, 1, false},\n\t\t{[]byte(`\\u03B1`), 6, 2, false},\n\t\t{[]byte(`\\uD830\\uDE03`), 12, 4, false},\n\n\t\t// invalid escape sequences\n\t\t{[]byte(`\\`), -1, -1, true}, // incomplete escape sequence\n\t\t{[]byte(`\\x`), -1, -1, true}, // invalid escape character\n\t\t{[]byte(`\\u`), -1, -1, true}, // incomplete unicode escape sequence\n\t\t{[]byte(`\\u004`), -1, -1, true}, // invalid unicode escape sequence\n\t\t{[]byte(`\\uXYZW`), -1, -1, true}, // invalid unicode escape sequence\n\t\t{[]byte(`\\uD83D\\u0041`), -1, -1, true}, // invalid unicode escape sequence\n\t}\n\n\tfor _, tc := range tests {\n\t\tinput := make([]byte, len(tc.input))\n\t\tcopy(input, tc.input)\n\t\toutput := make([]byte, utf8.UTFMax)\n\t\tinLen, outLen, err := processEscapedUTF8(input, output)\n\t\tif (err != nil) != tc.isError {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = %v; want %v\", tc.input, err, tc.isError)\n\t\t}\n\n\t\tif inLen != tc.expectedIn || outLen != tc.expectedOut {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = (%d, %d); want (%d, %d)\", tc.input, inLen, outLen, tc.expectedIn, tc.expectedOut)\n\t\t}\n\t}\n}\n\nfunc TestUnescape(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\texpected []byte\n\t\tisError bool\n\t}{\n\t\t{\"NoEscape\", []byte(\"hello world\"), []byte(\"hello world\"), false},\n\t\t{\"SingleEscape\", []byte(\"hello\\\\nworld\"), []byte(\"hello\\nworld\"), false},\n\t\t{\"MultipleEscapes\", []byte(\"line1\\\\nline2\\\\r\\\\nline3\"), []byte(\"line1\\nline2\\r\\nline3\"), false},\n\t\t{\"UnicodeEscape\", []byte(\"snowman:\\\\u2603\"), []byte(\"snowman:\\u2603\"), false},\n\t\t{\"SurrogatePair\", []byte(\"emoji:\\\\uD83D\\\\uDE00\"), []byte(\"emoji:😀\"), false},\n\t\t{\"InvalidEscape\", []byte(\"hello\\\\xworld\"), nil, true},\n\t\t{\"IncompleteUnicode\", []byte(\"incomplete:\\\\u123\"), nil, true},\n\t\t{\"InvalidSurrogatePair\", []byte(\"invalid:\\\\uD83D\\\\u0041\"), nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := make([]byte, len(tc.input)*2) // Allocate extra space for possible expansion\n\t\t\tresult, err := Unescape(tc.input, output)\n\t\t\tif (err != nil) != tc.isError {\n\t\t\t\tt.Errorf(\"Unescape(%q) error = %v; want error = %v\", tc.input, err, tc.isError)\n\t\t\t}\n\n\t\t\tif !tc.isError \u0026\u0026 !bytes.Equal(result, tc.expected) {\n\t\t\t\tt.Errorf(\"Unescape(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnquoteBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\tborder byte\n\t\texpected []byte\n\t\tok bool\n\t}{\n\t\t{[]byte(\"\\\"hello\\\"\"), '\"', []byte(\"hello\"), true},\n\t\t{[]byte(\"'hello'\"), '\\'', []byte(\"hello\"), true},\n\t\t{[]byte(\"\\\"hello\"), '\"', nil, false},\n\t\t{[]byte(\"hello\\\"\"), '\"', nil, false},\n\t\t{[]byte(\"\\\"he\\\\\\\"llo\\\"\"), '\"', []byte(\"he\\\"llo\"), true},\n\t\t{[]byte(\"\\\"he\\\\nllo\\\"\"), '\"', []byte(\"he\\nllo\"), true},\n\t\t{[]byte(\"\\\"\\\"\"), '\"', []byte(\"\"), true},\n\t\t{[]byte(\"''\"), '\\'', []byte(\"\"), true},\n\t\t{[]byte(\"\\\"\\\\u0041\\\"\"), '\"', []byte(\"A\"), true},\n\t\t{[]byte(`\"Hello, 世界\"`), '\"', []byte(\"Hello, 世界\"), true},\n\t\t{[]byte(`\"Hello, \\x80\"`), '\"', nil, false},\n\t\t{[]byte(`\"invalid surrogate: \\uD83D\\u0041\"`), '\"', nil, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tresult, pass := unquoteBytes(tc.input, tc.border)\n\n\t\tif pass != tc.ok {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %v; want %v\", tc.input, pass, tc.ok)\n\t\t}\n\n\t\tif !bytes.Equal(result, tc.expected) {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t}\n\t}\n}\n" + }, + { + "name": "indent.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n)\n\n// indentGrowthFactor specifies the growth factor of indenting JSON input.\n// A factor no higher than 2 ensures that wasted space never exceeds 50%.\nconst indentGrowthFactor = 2\n\n// IndentJSON formats the JSON data with the specified indentation.\nfunc Indent(data []byte, indent string) ([]byte, error) {\n\tvar (\n\t\tout bytes.Buffer\n\t\tlevel int\n\t\tinArray bool\n\t\tarrayDepth int\n\t)\n\n\tfor i := 0; i \u003c len(data); i++ {\n\t\tc := data[i] // current character\n\n\t\tswitch c {\n\t\tcase bracketOpen:\n\t\t\tarrayDepth++\n\t\t\tif arrayDepth \u003e 1 {\n\t\t\t\tlevel++ // increase the level if it's nested array\n\t\t\t\tinArray = true\n\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// case of the top-level array\n\t\t\t\tinArray = true\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase bracketClose:\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tlevel--\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tarrayDepth--\n\t\t\tif arrayDepth == 0 {\n\t\t\t\tinArray = false\n\t\t\t}\n\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase curlyOpen:\n\t\t\t// check if the empty object or array\n\t\t\t// we don't need to apply the indent when it's empty containers.\n\t\t\tif i+1 \u003c len(data) \u0026\u0026 data[i+1] == curlyClose {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\ti++ // skip next character\n\t\t\t\tif err := out.WriteByte(data[i]); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tlevel++\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase curlyClose:\n\t\t\tlevel--\n\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase comma, colon:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else if c == colon {\n\t\t\t\tif err := out.WriteByte(' '); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out.Bytes(), nil\n}\n\nfunc writeNewlineAndIndent(out *bytes.Buffer, level int, indent string) error {\n\tif err := out.WriteByte('\\n'); err != nil {\n\t\treturn err\n\t}\n\n\tidt := strings.Repeat(indent, level*indentGrowthFactor)\n\tif _, err := out.WriteString(idt); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "indent_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestIndentJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\tindent string\n\t\texpected []byte\n\t}{\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tinput: []byte(`{}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(`{}`),\n\t\t},\n\t\t{\n\t\t\tname: \"empty array\",\n\t\t\tinput: []byte(`[]`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(`[]`),\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tinput: []byte(`{{}}`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(\"{\\n\\t\\t{}\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"nested array\",\n\t\t\tinput: []byte(`[[[]]]`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(\"[[\\n\\t\\t[\\n\\t\\t\\t\\t\\n\\t\\t]\\n]]\"),\n\t\t},\n\t\t{\n\t\t\tname: \"top-level array\",\n\t\t\tinput: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t},\n\t\t{\n\t\t\tname: \"array of arrays\",\n\t\t\tinput: []byte(`[\"apple\",[\"banana\",\"cherry\"],\"date\"]`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"[\\\"apple\\\",[\\n \\\"banana\\\",\\n \\\"cherry\\\"\\n],\\\"date\\\"]\"),\n\t\t},\n\n\t\t{\n\t\t\tname: \"nested array in object\",\n\t\t\tinput: []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"{\\n \\\"fruits\\\": [\\\"apple\\\",[\\n \\\"banana\\\",\\n \\\"cherry\\\"\\n ],\\\"date\\\"]\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"complex nested structure\",\n\t\t\tinput: []byte(`{\"data\":{\"array\":[1,2,3],\"bool\":true,\"nestedArray\":[[\"a\",\"b\"],\"c\"]}}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"{\\n \\\"data\\\": {\\n \\\"array\\\": [1,2,3],\\\"bool\\\": true,\\\"nestedArray\\\": [[\\n \\\"a\\\",\\n \\\"b\\\"\\n ],\\\"c\\\"]\\n }\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"custom ident character\",\n\t\t\tinput: []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent: \"*\",\n\t\t\texpected: []byte(\"{\\n**\\\"fruits\\\": [\\\"apple\\\",[\\n****\\\"banana\\\",\\n****\\\"cherry\\\"\\n**],\\\"date\\\"]\\n}\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual, err := Indent(tt.input, tt.indent)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"IndentJSON() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !bytes.Equal(actual, tt.expected) {\n\t\t\t\tt.Errorf(\"IndentJSON() = %q, want %q\", actual, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "internal.gno", + "body": "package json\n\n// Reference: https://github.com/freddierice/php_source/blob/467ed5d6edff72219afd3e644516f131118ef48e/ext/json/JSON_parser.c\n// Copyright (c) 2005 JSON.org\n\n// Go implementation is taken from: https://github.com/spyzhov/ajson/blob/master/internal/state.go\n\ntype (\n\tStates int8 // possible states of the parser\n\tClasses int8 // JSON string character types\n)\n\nconst __ = -1\n\n// enum classes\nconst (\n\tC_SPACE Classes = iota /* space */\n\tC_WHITE /* other whitespace */\n\tC_LCURB /* { */\n\tC_RCURB /* } */\n\tC_LSQRB /* [ */\n\tC_RSQRB /* ] */\n\tC_COLON /* : */\n\tC_COMMA /* , */\n\tC_QUOTE /* \" */\n\tC_BACKS /* \\ */\n\tC_SLASH /* / */\n\tC_PLUS /* + */\n\tC_MINUS /* - */\n\tC_POINT /* . */\n\tC_ZERO /* 0 */\n\tC_DIGIT /* 123456789 */\n\tC_LOW_A /* a */\n\tC_LOW_B /* b */\n\tC_LOW_C /* c */\n\tC_LOW_D /* d */\n\tC_LOW_E /* e */\n\tC_LOW_F /* f */\n\tC_LOW_L /* l */\n\tC_LOW_N /* n */\n\tC_LOW_R /* r */\n\tC_LOW_S /* s */\n\tC_LOW_T /* t */\n\tC_LOW_U /* u */\n\tC_ABCDF /* ABCDF */\n\tC_E /* E */\n\tC_ETC /* everything else */\n)\n\n// AsciiClasses array maps the 128 ASCII characters into character classes.\nvar AsciiClasses = [128]Classes{\n\t/*\n\t This array maps the 128 ASCII characters into character classes.\n\t The remaining Unicode characters should be mapped to C_ETC.\n\t Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_QUOTE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n// QuoteAsciiClasses is a HACK for single quote from AsciiClasses\nvar QuoteAsciiClasses = [128]Classes{\n\t/*\n\t This array maps the 128 ASCII characters into character classes.\n\t The remaining Unicode characters should be mapped to C_ETC.\n\t Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_QUOTE,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n/*\nThe state codes.\n*/\nconst (\n\tGO States = iota /* start */\n\tOK /* ok */\n\tOB /* object */\n\tKE /* key */\n\tCO /* colon */\n\tVA /* value */\n\tAR /* array */\n\tST /* string */\n\tES /* escape */\n\tU1 /* u1 */\n\tU2 /* u2 */\n\tU3 /* u3 */\n\tU4 /* u4 */\n\tMI /* minus */\n\tZE /* zero */\n\tIN /* integer */\n\tDT /* dot */\n\tFR /* fraction */\n\tE1 /* e */\n\tE2 /* ex */\n\tE3 /* exp */\n\tT1 /* tr */\n\tT2 /* tru */\n\tT3 /* true */\n\tF1 /* fa */\n\tF2 /* fal */\n\tF3 /* fals */\n\tF4 /* false */\n\tN1 /* nu */\n\tN2 /* nul */\n\tN3 /* null */\n)\n\n// List of action codes.\n// these constants are defining an action that should be performed under certain conditions.\nconst (\n\tcl States = -2 /* colon */\n\tcm States = -3 /* comma */\n\tqt States = -4 /* quote */\n\tbo States = -5 /* bracket open */\n\tco States = -6 /* curly bracket open */\n\tbc States = -7 /* bracket close */\n\tcc States = -8 /* curly bracket close */\n\tec States = -9 /* curly bracket empty */\n)\n\n// StateTransitionTable is the state transition table takes the current state and the current symbol, and returns either\n// a new state or an action. An action is represented as a negative number. A JSON text is accepted if at the end of the\n// text the state is OK and if the mode is DONE.\nvar StateTransitionTable = [31][31]States{\n\t/*\n\t The state transition table takes the current state and the current symbol,\n\t and returns either a new state or an action. An action is represented as a\n\t negative number. A JSON text is accepted if at the end of the text the\n\t state is OK and if the mode is DONE.\n\t white 1-9 ABCDF etc\n\t space | { } [ ] : , \" \\ / + - . 0 | a b c d e f l n r s t u | E |*/\n\t/*start GO*/ {GO, GO, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*ok OK*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*object OB*/ {OB, OB, __, ec, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*key KE*/ {KE, KE, __, __, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*colon CO*/ {CO, CO, __, __, __, __, cl, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*value VA*/ {VA, VA, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*array AR*/ {AR, AR, co, __, bo, bc, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*string ST*/ {ST, __, ST, ST, ST, ST, ST, ST, qt, ES, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST},\n\t/*escape ES*/ {__, __, __, __, __, __, __, __, ST, ST, ST, __, __, __, __, __, __, ST, __, __, __, ST, __, ST, ST, __, ST, U1, __, __, __},\n\t/*u1 U1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U2, U2, U2, U2, U2, U2, U2, U2, __, __, __, __, __, __, U2, U2, __},\n\t/*u2 U2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U3, U3, U3, U3, U3, U3, U3, U3, __, __, __, __, __, __, U3, U3, __},\n\t/*u3 U3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U4, U4, U4, U4, U4, U4, U4, U4, __, __, __, __, __, __, U4, U4, __},\n\t/*u4 U4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ST, ST, ST, ST, ST, ST, ST, ST, __, __, __, __, __, __, ST, ST, __},\n\t/*minus MI*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ZE, IN, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*zero ZE*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, __, __, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*int IN*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, IN, IN, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*dot DT*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, FR, FR, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*frac FR*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, FR, FR, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*e E1*/ {__, __, __, __, __, __, __, __, __, __, __, E2, E2, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*ex E2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*exp E3*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*tr T1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T2, __, __, __, __, __, __},\n\t/*tru T2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T3, __, __, __},\n\t/*true T3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*fa F1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F2, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*fal F2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F3, __, __, __, __, __, __, __, __},\n\t/*fals F3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F4, __, __, __, __, __},\n\t/*false F4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*nu N1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N2, __, __, __},\n\t/*nul N2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N3, __, __, __, __, __, __, __, __},\n\t/*null N3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __},\n}\n" + }, + { + "name": "node.gno", + "body": "package json\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Node represents a JSON node.\ntype Node struct {\n\tprev *Node // prev is the parent node of the current node.\n\tnext map[string]*Node // next is the child nodes of the current node.\n\tkey *string // key holds the key of the current node in the parent node.\n\tdata []byte // byte slice of JSON data\n\tvalue interface{} // value holds the value of the current node.\n\tnodeType ValueType // NodeType holds the type of the current node. (Object, Array, String, Number, Boolean, Null)\n\tindex *int // index holds the index of the current node in the parent array node.\n\tborders [2]int // borders stores the start and end index of the current node in the data.\n\tmodified bool // modified indicates the current node is changed or not.\n}\n\n// NewNode creates a new node instance with the given parent node, buffer, type, and key.\nfunc NewNode(prev *Node, b *buffer, typ ValueType, key **string) (*Node, error) {\n\tcurr := \u0026Node{\n\t\tprev: prev,\n\t\tdata: b.data,\n\t\tborders: [2]int{b.index, 0},\n\t\tkey: *key,\n\t\tnodeType: typ,\n\t\tmodified: false,\n\t}\n\n\tif typ == Object || typ == Array {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\tif prev != nil {\n\t\tif prev.IsArray() {\n\t\t\tsize := len(prev.next)\n\t\t\tcurr.index = \u0026size\n\n\t\t\tprev.next[strconv.Itoa(size)] = curr\n\t\t} else if prev.IsObject() {\n\t\t\tif key == nil {\n\t\t\t\treturn nil, errKeyRequired\n\t\t\t}\n\n\t\t\tprev.next[**key] = curr\n\t\t} else {\n\t\t\treturn nil, errors.New(\"invalid parent type\")\n\t\t}\n\t}\n\n\treturn curr, nil\n}\n\n// load retrieves the value of the current node.\nfunc (n *Node) load() interface{} {\n\treturn n.value\n}\n\n// Changed checks the current node is changed or not.\nfunc (n *Node) Changed() bool {\n\treturn n.modified\n}\n\n// Key returns the key of the current node.\nfunc (n *Node) Key() string {\n\tif n == nil || n.key == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *n.key\n}\n\n// HasKey checks the current node has the given key or not.\nfunc (n *Node) HasKey(key string) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\t_, ok := n.next[key]\n\treturn ok\n}\n\n// GetKey returns the value of the given key from the current object node.\nfunc (n *Node) GetKey(key string) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.Type() != Object {\n\t\treturn nil, ufmt.Errorf(\"target node is not object type. got: %s\", n.Type().String())\n\t}\n\n\tvalue, ok := n.next[key]\n\tif !ok {\n\t\treturn nil, ufmt.Errorf(\"key not found: %s\", key)\n\t}\n\n\treturn value, nil\n}\n\n// MustKey returns the value of the given key from the current object node.\nfunc (n *Node) MustKey(key string) *Node {\n\tval, err := n.GetKey(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// UniqueKeyLists traverses the current JSON nodes and collects all the unique keys.\nfunc (n *Node) UniqueKeyLists() []string {\n\tvar collectKeys func(*Node) []string\n\tcollectKeys = func(node *Node) []string {\n\t\tif node == nil || !node.IsObject() {\n\t\t\treturn nil\n\t\t}\n\n\t\tresult := make(map[string]bool)\n\t\tfor key, childNode := range node.next {\n\t\t\tresult[key] = true\n\t\t\tchildKeys := collectKeys(childNode)\n\t\t\tfor _, childKey := range childKeys {\n\t\t\t\tresult[childKey] = true\n\t\t\t}\n\t\t}\n\n\t\tkeys := make([]string, 0, len(result))\n\t\tfor key := range result {\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\treturn keys\n\t}\n\n\treturn collectKeys(n)\n}\n\n// Empty returns true if the current node is empty.\nfunc (n *Node) Empty() bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\treturn len(n.next) == 0\n}\n\n// Type returns the type (ValueType) of the current node.\nfunc (n *Node) Type() ValueType {\n\treturn n.nodeType\n}\n\n// Value returns the value of the current node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tval, err := root.MustKey(\"key\").Value()\n//\tif err != nil {\n//\t\tt.Errorf(\"Value returns error: %v\", err)\n//\t}\n//\n//\tresult: \"value\"\nfunc (n *Node) Value() (value interface{}, err error) {\n\tvalue = n.load()\n\n\tif value == nil {\n\t\tswitch n.nodeType {\n\t\tcase Null:\n\t\t\treturn nil, nil\n\n\t\tcase Number:\n\t\t\tvalue, err = strconv.ParseFloat(string(n.source()), 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase String:\n\t\t\tvar ok bool\n\t\t\tvalue, ok = Unquote(n.source(), doubleQuote)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", errInvalidStringValue\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase Boolean:\n\t\t\tif len(n.source()) == 0 {\n\t\t\t\treturn nil, errEmptyBooleanNode\n\t\t\t}\n\n\t\t\tb := n.source()[0]\n\t\t\tvalue = b == 't' || b == 'T'\n\t\t\tn.value = value\n\n\t\tcase Array:\n\t\t\telems := make([]*Node, len(n.next))\n\n\t\t\tfor _, e := range n.next {\n\t\t\t\telems[*e.index] = e\n\t\t\t}\n\n\t\t\tvalue = elems\n\t\t\tn.value = value\n\n\t\tcase Object:\n\t\t\tobj := make(map[string]*Node, len(n.next))\n\n\t\t\tfor k, v := range n.next {\n\t\t\t\tobj[k] = v\n\t\t\t}\n\n\t\t\tvalue = obj\n\t\t\tn.value = value\n\t\t}\n\t}\n\n\treturn value, nil\n}\n\n// Delete removes the current node from the parent node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err := root.MustKey(\"key\").Delete(); err != nil {\n//\t\tt.Errorf(\"Delete returns error: %v\", err)\n//\t}\n//\n//\tresult: {} (empty object)\nfunc (n *Node) Delete() error {\n\tif n == nil {\n\t\treturn errors.New(\"can't delete nil node\")\n\t}\n\n\tif n.prev == nil {\n\t\treturn nil\n\t}\n\n\treturn n.prev.remove(n)\n}\n\n// Size returns the size (length) of the current array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.Size() != 2 {\n//\t\tt.Errorf(\"ArrayNode returns wrong size: %d\", root.Size())\n//\t}\nfunc (n *Node) Size() int {\n\tif n == nil {\n\t\treturn 0\n\t}\n\n\treturn len(n.next)\n}\n\n// Index returns the index of the current node in the parent array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.MustIndex(1).Index() != 1 {\n//\t\tt.Errorf(\"Index returns wrong index: %d\", root.MustIndex(1).Index())\n//\t}\n//\n// We can also use the index to the byte slice of the JSON data directly.\n//\n// Example:\n//\n//\troot := Unmarshal([]byte(`[\"foo\", 1]`))\n//\tif root == nil {\n//\t\tt.Errorf(\"Unmarshal returns nil\")\n//\t}\n//\n//\tif string(root.MustIndex(1).source()) != \"1\" {\n//\t\tt.Errorf(\"source returns wrong result: %s\", root.MustIndex(1).source())\n//\t}\nfunc (n *Node) Index() int {\n\tif n == nil || n.index == nil {\n\t\treturn -1\n\t}\n\n\treturn *n.index\n}\n\n// MustIndex returns the array element at the given index.\n//\n// If the index is negative, it returns the index is from the end of the array.\n// Also, it panics if the index is not found.\n//\n// check the Index method for detailed usage.\nfunc (n *Node) MustIndex(expectIdx int) *Node {\n\tval, err := n.GetIndex(expectIdx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// GetIndex returns the array element at the given index.\n//\n// if the index is negative, it returns the index is from the end of the array.\nfunc (n *Node) GetIndex(idx int) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsArray() {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tif idx \u003e n.Size() {\n\t\treturn nil, errors.New(\"input index exceeds the array size\")\n\t}\n\n\tif idx \u003c 0 {\n\t\tidx += len(n.next)\n\t}\n\n\tchild, ok := n.next[strconv.Itoa(idx)]\n\tif !ok {\n\t\treturn nil, errIndexNotFound\n\t}\n\n\treturn child, nil\n}\n\n// DeleteIndex removes the array element at the given index.\nfunc (n *Node) DeleteIndex(idx int) error {\n\tnode, err := n.GetIndex(idx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn n.remove(node)\n}\n\n// NullNode creates a new null type node.\n//\n// Usage:\n//\n//\t_ := NullNode(\"\")\nfunc NullNode(key string) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: nil,\n\t\tnodeType: Null,\n\t\tmodified: true,\n\t}\n}\n\n// NumberNode creates a new number type node.\n//\n// Usage:\n//\n//\troot := NumberNode(\"\", 1)\n//\tif root == nil {\n//\t\tt.Errorf(\"NumberNode returns nil\")\n//\t}\nfunc NumberNode(key string, value float64) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: Number,\n\t\tmodified: true,\n\t}\n}\n\n// StringNode creates a new string type node.\n//\n// Usage:\n//\n//\troot := StringNode(\"\", \"foo\")\n//\tif root == nil {\n//\t\tt.Errorf(\"StringNode returns nil\")\n//\t}\nfunc StringNode(key string, value string) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: String,\n\t\tmodified: true,\n\t}\n}\n\n// BoolNode creates a new given boolean value node.\n//\n// Usage:\n//\n//\troot := BoolNode(\"\", true)\n//\tif root == nil {\n//\t\tt.Errorf(\"BoolNode returns nil\")\n//\t}\nfunc BoolNode(key string, value bool) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: Boolean,\n\t\tmodified: true,\n\t}\n}\n\n// ArrayNode creates a new array type node.\n//\n// If the given value is nil, it creates an empty array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\nfunc ArrayNode(key string, value []*Node) *Node {\n\tcurr := \u0026Node{\n\t\tkey: \u0026key,\n\t\tnodeType: Array,\n\t\tmodified: true,\n\t}\n\n\tcurr.next = make(map[string]*Node, len(value))\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor i, v := range value {\n\t\t\tidx := i\n\t\t\tcurr.next[strconv.Itoa(i)] = v\n\n\t\t\tv.prev = curr\n\t\t\tv.index = \u0026idx\n\t\t}\n\t}\n\n\treturn curr\n}\n\n// ObjectNode creates a new object type node.\n//\n// If the given value is nil, it creates an empty object node.\n//\n// next is a map of key and value pairs of the object.\nfunc ObjectNode(key string, value map[string]*Node) *Node {\n\tcurr := \u0026Node{\n\t\tnodeType: Object,\n\t\tkey: \u0026key,\n\t\tnext: value,\n\t\tmodified: true,\n\t}\n\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor key, val := range value {\n\t\t\tvkey := key\n\t\t\tval.prev = curr\n\t\t\tval.key = \u0026vkey\n\t\t}\n\t} else {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\treturn curr\n}\n\n// IsArray returns true if the current node is array type.\nfunc (n *Node) IsArray() bool {\n\treturn n.nodeType == Array\n}\n\n// IsObject returns true if the current node is object type.\nfunc (n *Node) IsObject() bool {\n\treturn n.nodeType == Object\n}\n\n// IsNull returns true if the current node is null type.\nfunc (n *Node) IsNull() bool {\n\treturn n.nodeType == Null\n}\n\n// IsBool returns true if the current node is boolean type.\nfunc (n *Node) IsBool() bool {\n\treturn n.nodeType == Boolean\n}\n\n// IsString returns true if the current node is string type.\nfunc (n *Node) IsString() bool {\n\treturn n.nodeType == String\n}\n\n// IsNumber returns true if the current node is number type.\nfunc (n *Node) IsNumber() bool {\n\treturn n.nodeType == Number\n}\n\n// ready checks the current node is ready or not.\n//\n// the meaning of ready is the current node is parsed and has a valid value.\nfunc (n *Node) ready() bool {\n\treturn n.borders[1] != 0\n}\n\n// source returns the source of the current node.\nfunc (n *Node) source() []byte {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified \u0026\u0026 n.data != nil {\n\t\treturn (n.data)[n.borders[0]:n.borders[1]]\n\t}\n\n\treturn nil\n}\n\n// root returns the root node of the current node.\nfunc (n *Node) root() *Node {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tcurr := n\n\tfor curr.prev != nil {\n\t\tcurr = curr.prev\n\t}\n\n\treturn curr\n}\n\n// GetNull returns the null value if current node is null type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"null\"))\n//\tval, err := root.GetNull()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNull returns error: %v\", err)\n//\t}\n//\tif val != nil {\n//\t\tt.Errorf(\"GetNull returns wrong result: %v\", val)\n//\t}\nfunc (n *Node) GetNull() (interface{}, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsNull() {\n\t\treturn nil, errNotNullNode\n\t}\n\n\treturn nil, nil\n}\n\n// MustNull returns the null value if current node is null type.\n//\n// It panics if the current node is not null type.\nfunc (n *Node) MustNull() interface{} {\n\tv, err := n.GetNull()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetNumeric returns the numeric (int/float) value if current node is number type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"10.5\"))\n//\tval, err := root.GetNumeric()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNumeric returns error: %v\", err)\n//\t}\n//\tprintln(val) // 10.5\nfunc (n *Node) GetNumeric() (float64, error) {\n\tif n == nil {\n\t\treturn 0, errNilNode\n\t}\n\n\tif n.nodeType != Number {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tv, ok := val.(float64)\n\tif !ok {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\treturn v, nil\n}\n\n// MustNumeric returns the numeric (int/float) value if current node is number type.\n//\n// It panics if the current node is not number type.\nfunc (n *Node) MustNumeric() float64 {\n\tv, err := n.GetNumeric()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetString returns the string value if current node is string type.\n//\n// Usage:\n//\n//\troot, err := Unmarshal([]byte(\"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n//\t}\n//\n//\tstr, err := root.GetString()\n//\tif err != nil {\n//\t\tt.Errorf(\"should retrieve string value: %s\", err)\n//\t}\n//\n//\tprintln(str) // \"foo\"\nfunc (n *Node) GetString() (string, error) {\n\tif n == nil {\n\t\treturn \"\", errEmptyStringNode\n\t}\n\n\tif !n.IsString() {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tv, ok := val.(string)\n\tif !ok {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\treturn v, nil\n}\n\n// MustString returns the string value if current node is string type.\n//\n// It panics if the current node is not string type.\nfunc (n *Node) MustString() string {\n\tv, err := n.GetString()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetBool returns the boolean value if current node is boolean type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"true\"))\n//\tval, err := root.GetBool()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetBool returns error: %v\", err)\n//\t}\n//\tprintln(val) // true\nfunc (n *Node) GetBool() (bool, error) {\n\tif n == nil {\n\t\treturn false, errNilNode\n\t}\n\n\tif n.nodeType != Boolean {\n\t\treturn false, errNotBoolNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tv, ok := val.(bool)\n\tif !ok {\n\t\treturn false, errNotBoolNode\n\t}\n\n\treturn v, nil\n}\n\n// MustBool returns the boolean value if current node is boolean type.\n//\n// It panics if the current node is not boolean type.\nfunc (n *Node) MustBool() bool {\n\tv, err := n.GetBool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetArray returns the array value if current node is array type.\n//\n// Usage:\n//\n//\t\troot := Must(Unmarshal([]byte(`[\"foo\", 1]`)))\n//\t\tarr, err := root.GetArray()\n//\t\tif err != nil {\n//\t\t\tt.Errorf(\"GetArray returns error: %v\", err)\n//\t\t}\n//\n//\t\tfor _, val := range arr {\n//\t\t\tprintln(val)\n//\t\t}\n//\n//\t result: \"foo\", 1\nfunc (n *Node) GetArray() ([]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.nodeType != Array {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.([]*Node)\n\tif !ok {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\treturn v, nil\n}\n\n// MustArray returns the array value if current node is array type.\n//\n// It panics if the current node is not array type.\nfunc (n *Node) MustArray() []*Node {\n\tv, err := n.GetArray()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendArray appends the given values to the current array node.\n//\n// If the current node is not array type, it returns an error.\n//\n// Example 1:\n//\n//\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n//\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n//\t\tt.Errorf(\"should not return error: %s\", err)\n//\t}\n//\n//\tresult: [{\"foo\":\"bar\"}, null]\n//\n// Example 2:\n//\n//\troot := Must(Unmarshal([]byte(`[\"bar\", \"baz\"]`)))\n//\terr := root.AppendArray(NumberNode(\"\", 1), StringNode(\"\", \"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n//\t }\n//\n//\tresult: [\"bar\", \"baz\", 1, \"foo\"]\nfunc (n *Node) AppendArray(value ...*Node) error {\n\tif !n.IsArray() {\n\t\treturn errInvalidAppend\n\t}\n\n\tfor _, val := range value {\n\t\tif err := n.append(nil, val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ArrayEach executes the callback for each element in the JSON array.\n//\n// Usage:\n//\n//\tjsonArrayNode.ArrayEach(func(i int, valueNode *Node) {\n//\t ufmt.Println(i, valueNode)\n//\t})\nfunc (n *Node) ArrayEach(callback func(i int, target *Node)) {\n\tif n == nil || !n.IsArray() {\n\t\treturn\n\t}\n\n\tfor idx := 0; idx \u003c len(n.next); idx++ {\n\t\telement, err := n.GetIndex(idx)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcallback(idx, element)\n\t}\n}\n\n// GetObject returns the object value if current node is object type.\n//\n// Usage:\n//\n//\troot := Must(Unmarshal([]byte(`{\"key\": \"value\"}`)))\n//\tobj, err := root.GetObject()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetObject returns error: %v\", err)\n//\t}\n//\n//\tresult: map[string]*Node{\"key\": StringNode(\"key\", \"value\")}\nfunc (n *Node) GetObject() (map[string]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsObject() {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.(map[string]*Node)\n\tif !ok {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\treturn v, nil\n}\n\n// MustObject returns the object value if current node is object type.\n//\n// It panics if the current node is not object type.\nfunc (n *Node) MustObject() map[string]*Node {\n\tv, err := n.GetObject()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendObject appends the given key and value to the current object node.\n//\n// If the current node is not object type, it returns an error.\nfunc (n *Node) AppendObject(key string, value *Node) error {\n\tif !n.IsObject() {\n\t\treturn errInvalidAppend\n\t}\n\n\tif err := n.append(\u0026key, value); err != nil {\n\t\treturn err\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ObjectEach executes the callback for each key-value pair in the JSON object.\n//\n// Usage:\n//\n//\tjsonObjectNode.ObjectEach(func(key string, valueNode *Node) {\n//\t ufmt.Println(key, valueNode)\n//\t})\nfunc (n *Node) ObjectEach(callback func(key string, value *Node)) {\n\tif n == nil || !n.IsObject() {\n\t\treturn\n\t}\n\n\tfor key, child := range n.next {\n\t\tcallback(key, child)\n\t}\n}\n\n// String converts the node to a string representation.\nfunc (n *Node) String() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified {\n\t\treturn string(n.source())\n\t}\n\n\tval, err := Marshal(n)\n\tif err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\n\treturn string(val)\n}\n\n// Path builds the path of the current node.\n//\n// For example:\n//\n//\t{ \"key\": { \"sub\": [ \"val1\", \"val2\" ] }}\n//\n// The path of \"val2\" is: $.key.sub[1]\nfunc (n *Node) Path() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tif n.prev == nil {\n\t\tsb.WriteString(\"$\")\n\t} else {\n\t\tsb.WriteString(n.prev.Path())\n\n\t\tif n.key != nil {\n\t\t\tsb.WriteString(\"['\" + n.Key() + \"']\")\n\t\t} else {\n\t\t\tsb.WriteString(\"[\" + strconv.Itoa(n.Index()) + \"]\")\n\t\t}\n\t}\n\n\treturn sb.String()\n}\n\n// mark marks the current node as modified.\nfunc (n *Node) mark() {\n\tnode := n\n\tfor node != nil \u0026\u0026 !node.modified {\n\t\tnode.modified = true\n\t\tnode = node.prev\n\t}\n}\n\n// isContainer checks the current node type is array or object.\nfunc (n *Node) isContainer() bool {\n\treturn n.IsArray() || n.IsObject()\n}\n\n// remove removes the value from the current container type node.\nfunc (n *Node) remove(v *Node) error {\n\tif !n.isContainer() {\n\t\treturn ufmt.Errorf(\n\t\t\t\"can't remove value from non-array or non-object node. got=%s\",\n\t\t\tn.Type().String(),\n\t\t)\n\t}\n\n\tif v.prev != n {\n\t\treturn errors.New(\"invalid parent node\")\n\t}\n\n\tn.mark()\n\tif n.IsArray() {\n\t\tdelete(n.next, strconv.Itoa(*v.index))\n\t\tn.dropIndex(*v.index)\n\t} else {\n\t\tdelete(n.next, *v.key)\n\t}\n\n\tv.prev = nil\n\treturn nil\n}\n\n// dropIndex rebase the index of current array node values.\nfunc (n *Node) dropIndex(idx int) {\n\tfor i := idx + 1; i \u003c= len(n.next); i++ {\n\t\tprv := i - 1\n\t\tif curr, ok := n.next[strconv.Itoa(i)]; ok {\n\t\t\tcurr.index = \u0026prv\n\t\t\tn.next[strconv.Itoa(prv)] = curr\n\t\t}\n\n\t\tdelete(n.next, strconv.Itoa(i))\n\t}\n}\n\n// append is a helper function to append the given value to the current container type node.\nfunc (n *Node) append(key *string, val *Node) error {\n\tif n.isSameOrParentNode(val) {\n\t\treturn errInvalidAppendCycle\n\t}\n\n\tif val.prev != nil {\n\t\tif err := val.prev.remove(val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tval.prev = n\n\tval.key = key\n\n\tif key == nil {\n\t\tsize := len(n.next)\n\t\tval.index = \u0026size\n\t\tn.next[strconv.Itoa(size)] = val\n\t} else {\n\t\tif old, ok := n.next[*key]; ok {\n\t\t\tif err := n.remove(old); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tn.next[*key] = val\n\t}\n\n\treturn nil\n}\n\nfunc (n *Node) isSameOrParentNode(nd *Node) bool {\n\treturn n == nd || n.isParentNode(nd)\n}\n\nfunc (n *Node) isParentNode(nd *Node) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\tfor curr := nd.prev; curr != nil; curr = curr.prev {\n\t\tif curr == n {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// cptrs returns the pointer of the given string value.\nfunc cptrs(cpy *string) *string {\n\tif cpy == nil {\n\t\treturn nil\n\t}\n\n\tval := *cpy\n\n\treturn \u0026val\n}\n\n// cptri returns the pointer of the given integer value.\nfunc cptri(i *int) *int {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\tval := *i\n\treturn \u0026val\n}\n\n// Must panics if the given node is not fulfilled the expectation.\n// Usage:\n//\n//\tnode := Must(Unmarshal([]byte(`{\"key\": \"value\"}`))\nfunc Must(root *Node, expect error) *Node {\n\tif expect != nil {\n\t\tpanic(expect)\n\t}\n\n\treturn root\n}\n" + }, + { + "name": "node_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tnilKey *string\n\tdummyKey = \"key\"\n)\n\ntype _args struct {\n\tprev *Node\n\tbuf *buffer\n\ttyp ValueType\n\tkey **string\n}\n\ntype simpleNode struct {\n\tname string\n\tnode *Node\n}\n\nfunc TestNode_CreateNewNode(t *testing.T) {\n\trel := \u0026dummyKey\n\n\ttests := []struct {\n\t\tname string\n\t\targs _args\n\t\texpectCurr *Node\n\t\texpectErr bool\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\tname: \"child for non container type\",\n\t\t\targs: _args{\n\t\t\t\tprev: BoolNode(\"\", true),\n\t\t\t\tbuf: newBuffer(make([]byte, 10)),\n\t\t\t\ttyp: Boolean,\n\t\t\t\tkey: \u0026rel,\n\t\t\t},\n\t\t\texpectCurr: nil,\n\t\t\texpectErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif tt.expectPanic {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tt.Errorf(\"%s panic occurred when not expected: %v\", tt.name, r)\n\t\t\t\t} else if tt.expectPanic {\n\t\t\t\t\tt.Errorf(\"%s expected panic but didn't occur\", tt.name)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tgot, err := NewNode(tt.args.prev, tt.args.buf, tt.args.typ, tt.args.key)\n\t\t\tif (err != nil) != tt.expectErr {\n\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.expectErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expectErr {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !compareNodes(got, tt.expectCurr) {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expectCurr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Value(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\t_type ValueType\n\t\texpected interface{}\n\t\terrExpected bool\n\t}{\n\t\t{name: \"null\", data: []byte(\"null\"), _type: Null, expected: nil},\n\t\t{name: \"1\", data: []byte(\"1\"), _type: Number, expected: float64(1)},\n\t\t{name: \".1\", data: []byte(\".1\"), _type: Number, expected: float64(.1)},\n\t\t{name: \"-.1e1\", data: []byte(\"-.1e1\"), _type: Number, expected: float64(-1)},\n\t\t{name: \"string\", data: []byte(\"\\\"foo\\\"\"), _type: String, expected: \"foo\"},\n\t\t{name: \"space\", data: []byte(\"\\\"foo bar\\\"\"), _type: String, expected: \"foo bar\"},\n\t\t{name: \"true\", data: []byte(\"true\"), _type: Boolean, expected: true},\n\t\t{name: \"invalid true\", data: []byte(\"tru\"), _type: Unknown, errExpected: true},\n\t\t{name: \"invalid false\", data: []byte(\"fals\"), _type: Unknown, errExpected: true},\n\t\t{name: \"false\", data: []byte(\"false\"), _type: Boolean, expected: false},\n\t\t{name: \"e1\", data: []byte(\"e1\"), _type: Unknown, errExpected: true},\n\t\t{name: \"1a\", data: []byte(\"1a\"), _type: Unknown, errExpected: true},\n\t\t{name: \"string error\", data: []byte(\"\\\"foo\\nbar\\\"\"), _type: String, errExpected: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcurr := \u0026Node{\n\t\t\t\tdata: tt.data,\n\t\t\t\tnodeType: tt._type,\n\t\t\t\tborders: [2]int{0, len(tt.data)},\n\t\t\t}\n\n\t\t\tgot, err := curr.Value()\n\t\t\tif err != nil {\n\t\t\t\tif !tt.errExpected {\n\t\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.errExpected)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Delete(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{\"foo\":\"bar\"}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tfoo := root.MustKey(\"foo\")\n\tif err := foo.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error while handling foo: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif value, err := Marshal(foo); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `\"bar\"` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif foo.prev != nil {\n\t\tt.Errorf(\"foo.prev should be nil\")\n\t}\n}\n\nfunc TestNode_ObjectNode(t *testing.T) {\n\tobjs := map[string]*Node{\n\t\t\"key1\": NullNode(\"null\"),\n\t\t\"key2\": NumberNode(\"answer\", 42),\n\t\t\"key3\": StringNode(\"string\", \"foobar\"),\n\t\t\"key4\": BoolNode(\"bool\", true),\n\t}\n\n\tnode := ObjectNode(\"test\", objs)\n\n\tif len(node.next) != len(objs) {\n\t\tt.Errorf(\"ObjectNode: want %v got %v\", len(objs), len(node.next))\n\t}\n\n\tfor k, v := range objs {\n\t\tif node.next[k] == nil {\n\t\t\tt.Errorf(\"ObjectNode: want %v got %v\", v, node.next[k])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendObject(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`{\"foo\":\"bar\",\"baz\":null}`))).AppendObject(\"biz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendArray should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.AppendObject(\"baz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendObject should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if isSameObject(string(value), `\"{\"foo\":\"bar\",\"baz\":null}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif err := root.AppendObject(\"biz\", NumberNode(\"\", 42)); err != nil {\n\t\tt.Errorf(\"AppendObject returns error: %v\", err)\n\t}\n\n\tval, err := Marshal(root)\n\tif err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif isSameObject(string(val), `\"{\"foo\":\"bar\",\"baz\":null,\"biz\":42}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(val))\n\t}\n}\n\nfunc TestNode_ArrayNode(t *testing.T) {\n\tarr := []*Node{\n\t\tNullNode(\"nil\"),\n\t\tNumberNode(\"num\", 42),\n\t\tStringNode(\"str\", \"foobar\"),\n\t\tBoolNode(\"bool\", true),\n\t}\n\n\tnode := ArrayNode(\"test\", arr)\n\n\tif len(node.next) != len(arr) {\n\t\tt.Errorf(\"ArrayNode: want %v got %v\", len(arr), len(node.next))\n\t}\n\n\tfor i, v := range arr {\n\t\tif node.next[strconv.Itoa(i)] == nil {\n\t\t\tt.Errorf(\"ArrayNode: want %v got %v\", v, node.next[strconv.Itoa(i)])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendArray(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`))).AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif err := root.AppendArray(\n\t\tNumberNode(\"\", 1),\n\t\tStringNode(\"\", \"foo\"),\n\t\tMust(Unmarshal([]byte(`[0,1,null,true,\"example\"]`))),\n\t\tMust(Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123}`))),\n\t); err != nil {\n\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null,1,\"foo\",[0,1,null,true,\"example\"],{\"foo\": true, \"bar\": null, \"baz\": 123}]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n}\n\n/******** value getter ********/\n\nfunc TestNode_GetBool(t *testing.T) {\n\troot, err := Unmarshal([]byte(`true`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetBool()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetBool(): %s\", err.Error())\n\t}\n\n\tif !value {\n\t\tt.Errorf(\"root.GetBool() is corrupted\")\n\t}\n}\n\nfunc TestNode_GetBool_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"literally null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetBool(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"true\", BoolNode(\"\", true)},\n\t\t{\"false\", BoolNode(\"\", false)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif !tt.node.IsBool() {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool_With_Unmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson []byte\n\t\twant bool\n\t}{\n\t\t{\"true\", []byte(\"true\"), true},\n\t\t{\"false\", []byte(\"false\"), true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.json)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\t\t}\n\n\t\t\tif root.IsBool() != tt.want {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar nullJson = []byte(`null`)\n\nfunc TestNode_GetNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue, err := root.GetNull()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting null, %s\", err)\n\t}\n\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNull_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"number node is null\", NumberNode(\"\", 42)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNull(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue := root.MustNull()\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNumeric_Float(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123.456`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123.456) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123.456, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_Scientific_Notation(t *testing.T) {\n\troot, err := Unmarshal([]byte(`1e3`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(1000) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 1000, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_With_Unmarshal(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"string node\", StringNode(\"\", \"123\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNumeric(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetString(t *testing.T) {\n\troot, err := Unmarshal([]byte(`\"123foobar 3456\"`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t}\n\n\tvalue, err := root.GetString()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetString(): %s\", err)\n\t}\n\n\tif value != \"123foobar 3456\" {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %s\", value))\n\t}\n}\n\nfunc TestNode_GetString_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetString(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t}{\n\t\t{\"foo\", []byte(`\"foo\"`)},\n\t\t{\"foo bar\", []byte(`\"foo bar\"`)},\n\t\t{\"\", []byte(`\"\"`)},\n\t\t{\"안녕하세요\", []byte(`\"안녕하세요\"`)},\n\t\t{\"こんにちは\", []byte(`\"こんにちは\"`)},\n\t\t{\"你好\", []byte(`\"你好\"`)},\n\t\t{\"one \\\"encoded\\\" string\", []byte(`\"one \\\"encoded\\\" string\"`)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\t\t}\n\n\t\t\tvalue := root.MustString()\n\t\t\tif value != tt.name {\n\t\t\t\tt.Errorf(\"value is not matched. expected: %s, got: %s\", tt.name, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_Array(t *testing.T) {\n\troot, err := Unmarshal([]byte(\" [1,[\\\"1\\\",[1,[1,2,3]]]]\\r\\n\"))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t}\n\n\tif root == nil {\n\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(\"Error on Unmarshal: wrong type\")\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err)\n\t} else if len(array) != 2 {\n\t\tt.Errorf(\"expected 2 elements, got %d\", len(array))\n\t} else if val, err := array[0].GetNumeric(); err != nil {\n\t\tt.Errorf(\"value of array[0] is not numeric. got: %v\", array[0].value)\n\t} else if val != 1 {\n\t\tt.Errorf(\"Error on array[0].GetNumeric(): expected to be '1', got: %v\", val)\n\t} else if val, err := array[1].GetArray(); err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err.Error())\n\t} else if len(val) != 2 {\n\t\tt.Errorf(\"Error on array[1].GetArray(): expected 2 elements, got %d\", len(val))\n\t} else if el, err := val[0].GetString(); err != nil {\n\t\tt.Errorf(\"error occurred while getting string, %s\", err.Error())\n\t} else if el != \"1\" {\n\t\tt.Errorf(\"Error on val[0].GetString(): expected to be '1', got: %s\", el)\n\t}\n}\n\nvar sampleArr = []byte(`[-1, 2, 3, 4, 5, 6]`)\n\nfunc TestNode_GetArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetArray(): %s\", err)\n\t}\n\n\tif len(array) != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"length is not matched. expected: 3, got: %d\", len(array)))\n\t}\n\n\tfor i, node := range array {\n\t\tfor j, val := range []int{-1, 2, 3, 4, 5, 6} {\n\t\t\tif i == j {\n\t\t\t\tif v, err := node.GetNumeric(); err != nil {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"Error on node.GetNumeric(): %s\", err))\n\t\t\t\t} else if v != float64(val) {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: %d, got: %v\", val, v))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNode_GetArray_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetArray(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(ufmt.Sprintf(\"Must be an array. got: %s\", root.Type().String()))\n\t}\n}\n\nfunc TestNode_ArrayEach(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected []int\n\t}{\n\t\t{\n\t\t\tname: \"empty array\",\n\t\t\tjson: `[]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"single element\",\n\t\t\tjson: `[42]`,\n\t\t\texpected: []int{42},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements\",\n\t\t\tjson: `[1, 2, 3, 4, 5]`,\n\t\t\texpected: []int{1, 2, 3, 4, 5},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements but all values are same\",\n\t\t\tjson: `[1, 1, 1, 1, 1]`,\n\t\t\texpected: []int{1, 1, 1, 1, 1},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements with non-numeric values\",\n\t\t\tjson: `[\"a\", \"b\", \"c\", \"d\", \"e\"]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"non-array node\",\n\t\t\tjson: `{\"not\": \"an array\"}`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"array containing numeric and non-numeric elements\",\n\t\t\tjson: `[\"1\", 2, 3, \"4\", 5, \"6\"]`,\n\t\t\texpected: []int{2, 3, 5},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tvar result []int // callback result\n\t\t\troot.ArrayEach(func(index int, element *Node) {\n\t\t\t\tif val, err := strconv.Atoi(element.String()); err == nil {\n\t\t\t\t\tresult = append(result, val)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d elements, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor i, val := range result {\n\t\t\t\tif val != tc.expected[i] {\n\t\t\t\t\tt.Errorf(\"%s: expected value at index %d to be %d, got %d\", tc.name, i, tc.expected[i], val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Key(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123, \"biz\": [1,2,3]}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tobj := root.MustObject()\n\tfor key, node := range obj {\n\t\tif key != node.Key() {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", node.Key(), key)\n\t\t}\n\t}\n\n\tkeys := []string{\"foo\", \"bar\", \"baz\", \"biz\"}\n\tfor _, key := range keys {\n\t\tif obj[key].Key() != key {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", obj[key].Key(), key)\n\t\t}\n\t}\n\n\t// TODO: resolve stack overflow\n\t// if root.MustKey(\"foo\").Clone().Key() != \"\" {\n\t// \tt.Errorf(\"wrong key found for cloned key\")\n\t// }\n\n\tif (*Node)(nil).Key() != \"\" {\n\t\tt.Errorf(\"wrong key found for nil node\")\n\t}\n}\n\nfunc TestNode_Size(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tsize := root.Size()\n\tif size != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 6. got: %v\", size))\n\t}\n\n\tif (*Node)(nil).Size() != 0 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 0. got: %v\", (*Node)(nil).Size()))\n\t}\n}\n\nfunc TestNode_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tarr := root.MustArray()\n\tfor i, node := range arr {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_Index_Fail(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant int\n\t}{\n\t\t{\"nil node\", (*Node)(nil), -1},\n\t\t{\"null node\", NullNode(\"\"), -1},\n\t\t{\"object node\", ObjectNode(\"\", nil), -1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Index(); got != tt.want {\n\t\t\t\tt.Errorf(\"Index() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetIndex(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`)))\n\texpected := []int{1, 2, 3, 4, 5, 6}\n\n\tif len(expected) != root.Size() {\n\t\tt.Errorf(\"length is not matched. expected: %d, got: %d\", len(expected), root.Size())\n\t}\n\n\t// TODO: if length exceeds, stack overflow occurs. need to fix\n\tfor i, v := range expected {\n\t\tval, err := root.GetIndex(i)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"error occurred while getting index %d, %s\", i, err)\n\t\t}\n\n\t\tif val.MustNumeric() != float64(v) {\n\t\t\tt.Errorf(\"value is not matched. expected: %d, got: %v\", v, val.MustNumeric())\n\t\t}\n\t}\n}\n\nfunc TestNode_GetIndex_InputIndex_Exceed_Original_Node_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\t_, err = root.GetIndex(10)\n\tif err == nil {\n\t\tt.Errorf(\"GetIndex should return error\")\n\t}\n}\n\nfunc TestNode_DeleteIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\texpected string\n\t\tindex int\n\t\tok bool\n\t}{\n\t\t{`null`, ``, 0, false},\n\t\t{`1`, ``, 0, false},\n\t\t{`{}`, ``, 0, false},\n\t\t{`{\"foo\":\"bar\"}`, ``, 0, false},\n\t\t{`true`, ``, 0, false},\n\t\t{`[]`, ``, 0, false},\n\t\t{`[]`, ``, -1, false},\n\t\t{`[1]`, `[]`, 0, true},\n\t\t{`[{}]`, `[]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, -1, true},\n\t\t{`[{}, [], 42]`, `[[], 42]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, 42]`, 1, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, 2, true},\n\t\t{`[{}, [], 42]`, ``, 10, false},\n\t\t{`[{}, [], 42]`, ``, -10, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot := Must(Unmarshal([]byte(tt.name)))\n\t\t\terr := root.DeleteIndex(tt.index)\n\t\t\tif err != nil \u0026\u0026 tt.ok {\n\t\t\t\tt.Errorf(\"DeleteIndex returns error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetKey(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tvalue, err := root.GetKey(\"foo\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\tif value.MustBool() != true {\n\t\tt.Errorf(\"value is not matched. expected: true, got: %v\", value.MustBool())\n\t}\n\n\tvalue, err = root.GetKey(\"bar\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\t_, err = root.GetKey(\"baz\")\n\tif err == nil {\n\t\tt.Errorf(\"key baz is not exist. must be failed\")\n\t}\n\n\tif value.MustNull() != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value.MustNull())\n\t}\n}\n\nfunc TestNode_GetKey_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetKey(\"\"); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetUniqueKeyList(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"simple foo/bar\",\n\t\t\tjson: `{\"foo\": true, \"bar\": null}`,\n\t\t\texpected: []string{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tjson: `{}`,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tjson: `{\n\t\t\t\t\"outer\": {\n\t\t\t\t\t\"inner\": {\n\t\t\t\t\t\t\"key\": \"value\"\n\t\t\t\t\t},\n\t\t\t\t\t\"array\": [1, 2, 3]\n\t\t\t\t},\n\t\t\t\t\"another\": \"item\"\n\t\t\t}`,\n\t\t\texpected: []string{\"outer\", \"inner\", \"key\", \"array\", \"another\"},\n\t\t},\n\t\t{\n\t\t\tname: \"complex object\",\n\t\t\tjson: `{\n\t\t\t\t\"Image\": {\n\t\t\t\t\t\"Width\": 800,\n\t\t\t\t\t\"Height\": 600,\n\t\t\t\t\t\"Title\": \"View from 15th Floor\",\n\t\t\t\t\t\"Thumbnail\": {\n\t\t\t\t\t\t\"Url\": \"http://www.example.com/image/481989943\",\n\t\t\t\t\t\t\"Height\": 125,\n\t\t\t\t\t\t\"Width\": 100\n\t\t\t\t\t},\n\t\t\t\t\t\"Animated\": false,\n\t\t\t\t\t\"IDs\": [116, 943, 234, 38793]\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpected: []string{\"Image\", \"Width\", \"Height\", \"Title\", \"Thumbnail\", \"Url\", \"Animated\", \"IDs\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tt.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error occurred while unmarshal\")\n\t\t\t}\n\n\t\t\tvalue := root.UniqueKeyLists()\n\t\t\tif len(value) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"%s length must be %v. got: %v. retrieved keys: %s\", tt.name, len(tt.expected), len(value), value)\n\t\t\t}\n\n\t\t\tfor _, key := range value {\n\t\t\t\tif !contains(tt.expected, key) {\n\t\t\t\t\tt.Errorf(\"EachKey() must be in %v. got: %v\", tt.expected, key)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TODO: resolve stack overflow\nfunc TestNode_IsEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\texpected bool\n\t}{\n\t\t{\"nil node\", (*Node)(nil), false}, // nil node is not empty.\n\t\t// {\"null node\", NullNode(\"\"), true},\n\t\t{\"empty object\", ObjectNode(\"\", nil), true},\n\t\t{\"empty array\", ArrayNode(\"\", nil), true},\n\t\t{\"non-empty object\", ObjectNode(\"\", map[string]*Node{\"foo\": BoolNode(\"foo\", true)}), false},\n\t\t{\"non-empty array\", ArrayNode(\"\", []*Node{BoolNode(\"0\", true)}), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Empty(); got != tt.expected {\n\t\t\t\tt.Errorf(\"%s = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Index_EmptyList(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tarray := root.MustArray()\n\tfor i, node := range array {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_GetObject(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true,\"bar\": null}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetObject()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetObject(): %s\", err.Error())\n\t}\n\n\tif _, ok := value[\"foo\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: foo\")\n\t}\n\n\tif _, ok := value[\"bar\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: bar\")\n\t}\n}\n\nfunc TestNode_GetObject_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"get object from null node\", NullNode(\"\")},\n\t\t{\"not object node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetObject(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ObjectEach(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected map[string]int\n\t}{\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tjson: `{}`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t\t{\n\t\t\tname: \"single key-value pair\",\n\t\t\tjson: `{\"key\": 42}`,\n\t\t\texpected: map[string]int{\"key\": 42},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple key-value pairs\",\n\t\t\tjson: `{\"one\": 1, \"two\": 2, \"three\": 3}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"two\": 2, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple key-value pairs with some non-numeric values\",\n\t\t\tjson: `{\"one\": 1, \"two\": \"2\", \"three\": 3, \"four\": \"4\"}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname: \"non-object node\",\n\t\t\tjson: `[\"not\", \"an\", \"object\"]`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tresult := make(map[string]int)\n\t\t\troot.ObjectEach(func(key string, value *Node) {\n\t\t\t\t// extract integer values from the object\n\t\t\t\tif val, err := strconv.Atoi(value.String()); err == nil {\n\t\t\t\t\tresult[key] = val\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d key-value pairs, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor key, val := range tc.expected {\n\t\t\t\tif result[key] != val {\n\t\t\t\t\tt.Errorf(\"%s: expected value for key %s to be %d, got %d\", tc.name, key, val, result[key])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ExampleMust(t *testing.T) {\n\tdata := []byte(`{\n \"Image\": {\n \"Width\": 800,\n \"Height\": 600,\n \"Title\": \"View from 15th Floor\",\n \"Thumbnail\": {\n \"Url\": \"http://www.example.com/image/481989943\",\n \"Height\": 125,\n \"Width\": 100\n },\n \"Animated\" : false,\n \"IDs\": [116, 943, 234, 38793]\n }\n }`)\n\n\troot := Must(Unmarshal(data))\n\tif root.Size() != 1 {\n\t\tt.Errorf(\"root.Size() must be 1. got: %v\", root.Size())\n\t}\n\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n\t// Output:\n\t// Object has 1 inheritors inside\n}\n\n// Calculate AVG price from different types of objects, JSON from: https://goessner.net/articles/JsonPath/index.html#e3\nfunc TestExampleUnmarshal(t *testing.T) {\n\tdata := []byte(`{ \"store\": {\n \"book\": [ \n { \"category\": \"reference\",\n \"author\": \"Nigel Rees\",\n \"title\": \"Sayings of the Century\",\n \"price\": 8.95\n },\n { \"category\": \"fiction\",\n \"author\": \"Evelyn Waugh\",\n \"title\": \"Sword of Honour\",\n \"price\": 12.99\n },\n { \"category\": \"fiction\",\n \"author\": \"Herman Melville\",\n \"title\": \"Moby Dick\",\n \"isbn\": \"0-553-21311-3\",\n \"price\": 8.99\n },\n { \"category\": \"fiction\",\n \"author\": \"J. R. R. Tolkien\",\n \"title\": \"The Lord of the Rings\",\n \"isbn\": \"0-395-19395-8\",\n \"price\": 22.99\n }\n ],\n \"bicycle\": { \"color\": \"red\",\n \"price\": 19.95\n },\n \"tools\": null\n }\n}`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred when unmarshal\")\n\t}\n\n\tstore := root.MustKey(\"store\").MustObject()\n\n\tvar prices float64\n\tsize := 0\n\tfor _, objects := range store {\n\t\tif objects.IsArray() \u0026\u0026 objects.Size() \u003e 0 {\n\t\t\tsize += objects.Size()\n\t\t\tfor _, object := range objects.MustArray() {\n\t\t\t\tprices += object.MustKey(\"price\").MustNumeric()\n\t\t\t}\n\t\t} else if objects.IsObject() \u0026\u0026 objects.HasKey(\"price\") {\n\t\t\tsize++\n\t\t\tprices += objects.MustKey(\"price\").MustNumeric()\n\t\t}\n\t}\n\n\tresult := int(prices / float64(size))\n\tufmt.Sprintf(\"AVG price: %d\", result)\n}\n\nfunc TestNode_ExampleMust_panic(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\tdata := []byte(`{]`)\n\troot := Must(Unmarshal(data))\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n}\n\nfunc TestNode_Path(t *testing.T) {\n\tdata := []byte(`{\n \"Image\": {\n \"Width\": 800,\n \"Height\": 600,\n \"Title\": \"View from 15th Floor\",\n \"Thumbnail\": {\n \"Url\": \"http://www.example.com/image/481989943\",\n \"Height\": 125,\n \"Width\": 100\n },\n \"Animated\" : false,\n \"IDs\": [116, 943, 234, 38793]\n }\n }`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tif root.Path() != \"$\" {\n\t\tt.Errorf(\"Wrong root.Path()\")\n\t}\n\n\telement := root.MustKey(\"Image\").MustKey(\"Thumbnail\").MustKey(\"Url\")\n\tif element.Path() != \"$['Image']['Thumbnail']['Url']\" {\n\t\tt.Errorf(\"Wrong path found: %s\", element.Path())\n\t}\n\n\tif (*Node)(nil).Path() != \"\" {\n\t\tt.Errorf(\"Wrong (nil).Path()\")\n\t}\n}\n\nfunc TestNode_Path2(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"Node with key\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tkey: func() *string { s := \"key\"; return \u0026s }(),\n\t\t\t},\n\t\t\twant: \"$['key']\",\n\t\t},\n\t\t{\n\t\t\tname: \"Node with index\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tindex: func() *int { i := 1; return \u0026i }(),\n\t\t\t},\n\t\t\twant: \"$[1]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Path(); got != tt.want {\n\t\t\t\tt.Errorf(\"Path() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Root(t *testing.T) {\n\troot := \u0026Node{}\n\tchild := \u0026Node{prev: root}\n\tgrandChild := \u0026Node{prev: child}\n\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant *Node\n\t}{\n\t\t{\n\t\t\tname: \"Root node\",\n\t\t\tnode: root,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Child node\",\n\t\t\tnode: child,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Grandchild node\",\n\t\t\tnode: grandChild,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Node is nil\",\n\t\t\tnode: nil,\n\t\t\twant: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.root(); got != tt.want {\n\t\t\t\tt.Errorf(\"root() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc contains(slice []string, item string) bool {\n\tfor _, a := range slice {\n\t\tif a == item {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ignore the sequence of keys by ordering them.\n// need to avoid import encoding/json and reflect package.\n// because gno does not support them for now.\n// TODO: use encoding/json to compare the result after if possible in gno.\nfunc isSameObject(a, b string) bool {\n\taPairs := strings.Split(strings.Trim(a, \"{}\"), \",\")\n\tbPairs := strings.Split(strings.Trim(b, \"{}\"), \",\")\n\n\taMap := make(map[string]string)\n\tbMap := make(map[string]string)\n\tfor _, pair := range aPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\taMap[key] = value\n\t}\n\tfor _, pair := range bPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\tbMap[key] = value\n\t}\n\n\taKeys := make([]string, 0, len(aMap))\n\tbKeys := make([]string, 0, len(bMap))\n\tfor k := range aMap {\n\t\taKeys = append(aKeys, k)\n\t}\n\n\tfor k := range bMap {\n\t\tbKeys = append(bKeys, k)\n\t}\n\n\tsort.Strings(aKeys)\n\tsort.Strings(bKeys)\n\n\tif len(aKeys) != len(bKeys) {\n\t\treturn false\n\t}\n\n\tfor i := range aKeys {\n\t\tif aKeys[i] != bKeys[i] {\n\t\t\treturn false\n\t\t}\n\n\t\tif aMap[aKeys[i]] != bMap[bKeys[i]] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc compareNodes(n1, n2 *Node) bool {\n\tif n1 == nil || n2 == nil {\n\t\treturn n1 == n2\n\t}\n\n\tif n1.key != n2.key {\n\t\treturn false\n\t}\n\n\tif !bytes.Equal(n1.data, n2.data) {\n\t\treturn false\n\t}\n\n\tif n1.index != n2.index {\n\t\treturn false\n\t}\n\n\tif n1.borders != n2.borders {\n\t\treturn false\n\t}\n\n\tif n1.modified != n2.modified {\n\t\treturn false\n\t}\n\n\tif n1.nodeType != n2.nodeType {\n\t\treturn false\n\t}\n\n\tif !compareNodes(n1.prev, n2.prev) {\n\t\treturn false\n\t}\n\n\tif len(n1.next) != len(n2.next) {\n\t\treturn false\n\t}\n\n\tfor k, v := range n1.next {\n\t\tif !compareNodes(v, n2.next[k]) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n" + }, + { + "name": "parser.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n)\n\nconst (\n\tunescapeStackBufSize = 64\n\tabsMinInt64 = 1 \u003c\u003c 63\n\tmaxInt64 = absMinInt64 - 1\n\tmaxUint64 = 1\u003c\u003c64 - 1\n)\n\n// PaseStringLiteral parses a string from the given byte slice.\nfunc ParseStringLiteral(data []byte) (string, error) {\n\tvar buf [unescapeStackBufSize]byte\n\n\tbf, err := Unescape(data, buf[:])\n\tif err != nil {\n\t\treturn \"\", errInvalidStringInput\n\t}\n\n\treturn string(bf), nil\n}\n\n// ParseBoolLiteral parses a boolean value from the given byte slice.\nfunc ParseBoolLiteral(data []byte) (bool, error) {\n\tswitch {\n\tcase bytes.Equal(data, trueLiteral):\n\t\treturn true, nil\n\tcase bytes.Equal(data, falseLiteral):\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, errMalformedBooleanValue\n\t}\n}\n" + }, + { + "name": "parser_test.gno", + "body": "package json\n\nimport \"testing\"\n\nfunc TestParseStringLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t\tisError bool\n\t}{\n\t\t{`\"Hello, World!\"`, \"\\\"Hello, World!\\\"\", false},\n\t\t{`\\uFF11`, \"\\uFF11\", false},\n\t\t{`\\uFFFF`, \"\\uFFFF\", false},\n\t\t{`true`, \"true\", false},\n\t\t{`false`, \"false\", false},\n\t\t{`\\uDF00`, \"\", true},\n\t}\n\n\tfor i, tt := range tests {\n\t\ts, err := ParseStringLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif s != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%s, but actual=%s\", i, tt.expected, s)\n\t\t}\n\t}\n}\n\nfunc TestParseBoolLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected bool\n\t\tisError bool\n\t}{\n\t\t{`true`, true, false},\n\t\t{`false`, false, false},\n\t\t{`TRUE`, false, true},\n\t\t{`FALSE`, false, true},\n\t\t{`foo`, false, true},\n\t\t{`\"true\"`, false, true},\n\t\t{`\"false\"`, false, true},\n\t}\n\n\tfor i, tt := range tests {\n\t\tb, err := ParseBoolLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif b != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%t, but actual=%t\", i, tt.expected, b)\n\t\t}\n\t}\n}\n" + }, + { + "name": "path.gno", + "body": "package json\n\nimport (\n\t\"errors\"\n)\n\n// ParsePath takes a JSONPath string and returns a slice of strings representing the path segments.\nfunc ParsePath(path string) ([]string, error) {\n\tbuf := newBuffer([]byte(path))\n\tresult := make([]string, 0)\n\n\tfor {\n\t\tb, err := buf.current()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch {\n\t\tcase b == dollarSign || b == atSign:\n\t\t\tresult = append(result, string(b))\n\t\t\tbuf.step()\n\n\t\tcase b == dot:\n\t\t\tbuf.step()\n\n\t\t\tif next, _ := buf.current(); next == dot {\n\t\t\t\tbuf.step()\n\t\t\t\tresult = append(result, \"..\")\n\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t} else {\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t}\n\n\t\tcase b == bracketOpen:\n\t\t\tstart := buf.index\n\t\t\tbuf.step()\n\n\t\t\tfor {\n\t\t\t\tif buf.index \u003e= buf.length || buf.data[buf.index] == bracketClose {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tbuf.step()\n\t\t\t}\n\n\t\t\tif buf.index \u003e= buf.length {\n\t\t\t\treturn nil, errors.New(\"unexpected end of path\")\n\t\t\t}\n\n\t\t\tsegment := string(buf.sliceFromIndices(start+1, buf.index))\n\t\t\tresult = append(result, segment)\n\n\t\t\tbuf.step()\n\n\t\tdefault:\n\t\t\tbuf.step()\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// extractNextSegment extracts the segment from the current index\n// to the next significant character and adds it to the resulting slice.\nfunc extractNextSegment(buf *buffer, result *[]string) {\n\tstart := buf.index\n\tbuf.skipToNextSignificantToken()\n\n\tif buf.index \u003c= start {\n\t\treturn\n\t}\n\n\tsegment := string(buf.sliceFromIndices(start, buf.index))\n\tif segment != \"\" {\n\t\t*result = append(*result, segment)\n\t}\n}\n" + }, + { + "name": "path_test.gno", + "body": "package json\n\nimport \"testing\"\n\nfunc TestParseJSONPath(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpath string\n\t\texpected []string\n\t}{\n\t\t{name: \"Empty string path\", path: \"\", expected: []string{}},\n\t\t{name: \"Root only path\", path: \"$\", expected: []string{\"$\"}},\n\t\t{name: \"Root with dot path\", path: \"$.\", expected: []string{\"$\"}},\n\t\t{name: \"All objects in path\", path: \"$..\", expected: []string{\"$\", \"..\"}},\n\t\t{name: \"Only children in path\", path: \"$.*\", expected: []string{\"$\", \"*\"}},\n\t\t{name: \"All objects' children in path\", path: \"$..*\", expected: []string{\"$\", \"..\", \"*\"}},\n\t\t{name: \"Simple dot notation path\", path: \"$.root.element\", expected: []string{\"$\", \"root\", \"element\"}},\n\t\t{name: \"Complex dot notation path with wildcard\", path: \"$.root.*.element\", expected: []string{\"$\", \"root\", \"*\", \"element\"}},\n\t\t{name: \"Path with array wildcard\", path: \"$.phoneNumbers[*].type\", expected: []string{\"$\", \"phoneNumbers\", \"*\", \"type\"}},\n\t\t{name: \"Path with filter expression\", path: \"$.store.book[?(@.price \u003c 10)].title\", expected: []string{\"$\", \"store\", \"book\", \"?(@.price \u003c 10)\", \"title\"}},\n\t\t{name: \"Path with formula\", path: \"$..phoneNumbers..('ty' + 'pe')\", expected: []string{\"$\", \"..\", \"phoneNumbers\", \"..\", \"('ty' + 'pe')\"}},\n\t\t{name: \"Simple bracket notation path\", path: \"$['root']['element']\", expected: []string{\"$\", \"'root'\", \"'element'\"}},\n\t\t{name: \"Complex bracket notation path with wildcard\", path: \"$['root'][*]['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Bracket notation path with integer index\", path: \"$['store']['book'][0]['title']\", expected: []string{\"$\", \"'store'\", \"'book'\", \"0\", \"'title'\"}},\n\t\t{name: \"Complex path with wildcard in bracket notation\", path: \"$['root'].*['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot after bracket\", path: \"$.['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot before bracket\", path: \"$['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Single character path with root\", path: \"$.a\", expected: []string{\"$\", \"a\"}},\n\t\t{name: \"Multiple characters path with root\", path: \"$.abc\", expected: []string{\"$\", \"abc\"}},\n\t\t{name: \"Multiple segments path with root\", path: \"$.a.b.c\", expected: []string{\"$\", \"a\", \"b\", \"c\"}},\n\t\t{name: \"Multiple segments path with wildcard and root\", path: \"$.a.*.c\", expected: []string{\"$\", \"a\", \"*\", \"c\"}},\n\t\t{name: \"Multiple segments path with filter and root\", path: \"$.a[?(@.b == 'c')].d\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\"}},\n\t\t{name: \"Complex path with multiple filters\", path: \"$.a[?(@.b == 'c')].d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Complex path with multiple filters and wildcards\", path: \"$.a[?(@.b == 'c')].*.d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"*\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Path with array index and root\", path: \"$.a[0].b\", expected: []string{\"$\", \"a\", \"0\", \"b\"}},\n\t\t{name: \"Path with multiple array indices and root\", path: \"$.a[0].b[1].c\", expected: []string{\"$\", \"a\", \"0\", \"b\", \"1\", \"c\"}},\n\t\t{name: \"Path with array index, wildcard and root\", path: \"$.a[0].*.c\", expected: []string{\"$\", \"a\", \"0\", \"*\", \"c\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treult, _ := ParsePath(tt.path)\n\t\t\tif !isEqualSlice(reult, tt.expected) {\n\t\t\t\tt.Errorf(\"ParsePath(%s) expected: %v, got: %v\", tt.path, tt.expected, reult)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc isEqualSlice(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n" + }, + { + "name": "token.gno", + "body": "package json\n\nconst (\n\tbracketOpen = '['\n\tbracketClose = ']'\n\tparenOpen = '('\n\tparenClose = ')'\n\tcurlyOpen = '{'\n\tcurlyClose = '}'\n\tcomma = ','\n\tdot = '.'\n\tcolon = ':'\n\tbackTick = '`'\n\tsingleQuote = '\\''\n\tdoubleQuote = '\"'\n\temptyString = \"\"\n\twhiteSpace = ' '\n\tplus = '+'\n\tminus = '-'\n\taesterisk = '*'\n\tbang = '!'\n\tquestion = '?'\n\tnewLine = '\\n'\n\ttab = '\\t'\n\tcarriageReturn = '\\r'\n\tformFeed = '\\f'\n\tbackSpace = '\\b'\n\tslash = '/'\n\tbackSlash = '\\\\'\n\tunderScore = '_'\n\tdollarSign = '$'\n\tatSign = '@'\n\tandSign = '\u0026'\n\torSign = '|'\n)\n\nvar (\n\ttrueLiteral = []byte(\"true\")\n\tfalseLiteral = []byte(\"false\")\n\tnullLiteral = []byte(\"null\")\n)\n\ntype ValueType int\n\nconst (\n\tNotExist ValueType = iota\n\tString\n\tNumber\n\tFloat\n\tObject\n\tArray\n\tBoolean\n\tNull\n\tUnknown\n)\n\nfunc (v ValueType) String() string {\n\tswitch v {\n\tcase NotExist:\n\t\treturn \"not-exist\"\n\tcase String:\n\t\treturn \"string\"\n\tcase Number:\n\t\treturn \"number\"\n\tcase Object:\n\t\treturn \"object\"\n\tcase Array:\n\t\treturn \"array\"\n\tcase Boolean:\n\t\treturn \"boolean\"\n\tcase Null:\n\t\treturn \"null\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "int32", + "path": "gno.land/p/demo/math_eval/int32", + "files": [ + { + "name": "int32.gno", + "body": "// eval/int32 is a evaluator for int32 expressions.\n// This code is heavily forked from https://github.com/dengsgo/math-engine\n// which is licensed under Apache 2.0:\n// https://raw.githubusercontent.com/dengsgo/math-engine/298e2b57b7e7350d0f67bd036916efd5709abe25/LICENSE\npackage int32\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tIdentifier = iota\n\tNumber // numbers\n\tOperator // +, -, *, /, etc.\n\tVariable // x, y, z, etc. (one-letter only)\n)\n\ntype expression interface {\n\tString() string\n}\n\ntype expressionRaw struct {\n\texpression string\n\tType int\n\tFlag int\n\tOffset int\n}\n\ntype parser struct {\n\tInput string\n\tch byte\n\toffset int\n\terr error\n}\n\ntype expressionNumber struct {\n\tVal int\n\tStr string\n}\n\ntype expressionVariable struct {\n\tVal int\n\tStr string\n}\n\ntype expressionOperation struct {\n\tOp string\n\tLhs,\n\tRhs expression\n}\n\ntype ast struct {\n\trawexpressions []*expressionRaw\n\tsource string\n\tcurrentexpression *expressionRaw\n\tcurrentIndex int\n\tdepth int\n\terr error\n}\n\n// Parse takes an expression string, e.g. \"1+2\" and returns\n// a parsed expression. If there is an error it will return.\nfunc Parse(s string) (ar expression, err error) {\n\ttoks, err := lexer(s)\n\tif err != nil {\n\t\treturn\n\t}\n\tast, err := newAST(toks, s)\n\tif err != nil {\n\t\treturn\n\t}\n\tar, err = ast.parseExpression()\n\treturn\n}\n\n// Eval takes a parsed expression and a map of variables (or nil). The parsed\n// expression is evaluated using any variables and returns the\n// resulting int and/or error.\nfunc Eval(expr expression, variables map[string]int) (res int, err error) {\n\tif err != nil {\n\t\treturn\n\t}\n\tvar l, r int\n\tswitch expr.(type) {\n\tcase expressionVariable:\n\t\tast := expr.(expressionVariable)\n\t\tok := false\n\t\tif variables != nil {\n\t\t\tres, ok = variables[ast.Str]\n\t\t}\n\t\tif !ok {\n\t\t\terr = ufmt.Errorf(\"variable '%s' not found\", ast.Str)\n\t\t}\n\t\treturn\n\tcase expressionOperation:\n\t\tast := expr.(expressionOperation)\n\t\tl, err = Eval(ast.Lhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tr, err = Eval(ast.Rhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tswitch ast.Op {\n\t\tcase \"+\":\n\t\t\tres = l + r\n\t\tcase \"-\":\n\t\t\tres = l - r\n\t\tcase \"*\":\n\t\t\tres = l * r\n\t\tcase \"/\":\n\t\t\tif r == 0 {\n\t\t\t\terr = ufmt.Errorf(\"violation of arithmetic specification: a division by zero in Eval: [%d/%d]\", l, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tres = l / r\n\t\tcase \"%\":\n\t\t\tif r == 0 {\n\t\t\t\tres = 0\n\t\t\t} else {\n\t\t\t\tres = l % r\n\t\t\t}\n\t\tcase \"^\":\n\t\t\tres = l ^ r\n\t\tcase \"\u003e\u003e\":\n\t\t\tres = l \u003e\u003e r\n\t\tcase \"\u003c\u003c\":\n\t\t\tres = l \u003c\u003c r\n\t\tcase \"\u003e\":\n\t\t\tif l \u003e r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u003c\":\n\t\t\tif l \u003c r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u0026\":\n\t\t\tres = l \u0026 r\n\t\tcase \"|\":\n\t\t\tres = l | r\n\t\tdefault:\n\n\t\t}\n\tcase expressionNumber:\n\t\tres = expr.(expressionNumber).Val\n\t}\n\n\treturn\n}\n\nfunc expressionError(s string, pos int) string {\n\tr := strings.Repeat(\"-\", len(s)) + \"\\n\"\n\ts += \"\\n\"\n\tfor i := 0; i \u003c pos; i++ {\n\t\ts += \" \"\n\t}\n\ts += \"^\\n\"\n\treturn r + s + r\n}\n\nfunc (n expressionVariable) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionVariable: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (n expressionNumber) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionNumber: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (b expressionOperation) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionOperation: (%s %s %s)\",\n\t\tb.Op,\n\t\tb.Lhs.String(),\n\t\tb.Rhs.String(),\n\t)\n}\n\nfunc newAST(toks []*expressionRaw, s string) (*ast, error) {\n\ta := \u0026ast{\n\t\trawexpressions: toks,\n\t\tsource: s,\n\t}\n\tif a.rawexpressions == nil || len(a.rawexpressions) == 0 {\n\t\treturn a, errors.New(\"empty token\")\n\t} else {\n\t\ta.currentIndex = 0\n\t\ta.currentexpression = a.rawexpressions[0]\n\t}\n\treturn a, nil\n}\n\nfunc (a *ast) parseExpression() (expression, error) {\n\ta.depth++ // called depth\n\tlhs := a.parsePrimary()\n\tr := a.parseBinOpRHS(0, lhs)\n\ta.depth--\n\tif a.depth == 0 \u0026\u0026 a.currentIndex != len(a.rawexpressions) \u0026\u0026 a.err == nil {\n\t\treturn r, ufmt.Errorf(\"bad expression, reaching the end or missing the operator\\n%s\",\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t}\n\treturn r, nil\n}\n\nfunc (a *ast) getNextexpressionRaw() *expressionRaw {\n\ta.currentIndex++\n\tif a.currentIndex \u003c len(a.rawexpressions) {\n\t\ta.currentexpression = a.rawexpressions[a.currentIndex]\n\t\treturn a.currentexpression\n\t}\n\treturn nil\n}\n\nfunc (a *ast) getTokPrecedence() int {\n\tswitch a.currentexpression.expression {\n\tcase \"/\", \"%\", \"*\":\n\t\treturn 100\n\tcase \"\u003c\u003c\", \"\u003e\u003e\":\n\t\treturn 80\n\tcase \"+\", \"-\":\n\t\treturn 75\n\tcase \"\u003c\", \"\u003e\":\n\t\treturn 70\n\tcase \"\u0026\":\n\t\treturn 60\n\tcase \"^\":\n\t\treturn 50\n\tcase \"|\":\n\t\treturn 40\n\t}\n\treturn -1\n}\n\nfunc (a *ast) parseNumber() expressionNumber {\n\tf64, err := strconv.Atoi(a.currentexpression.expression)\n\tif err != nil {\n\t\ta.err = ufmt.Errorf(\"%v\\nwant '(' or '0-9' but get '%s'\\n%s\",\n\t\t\terr.Error(),\n\t\t\ta.currentexpression.expression,\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\treturn expressionNumber{}\n\t}\n\tn := expressionNumber{\n\t\tVal: f64,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parseVariable() expressionVariable {\n\tn := expressionVariable{\n\t\tVal: 0,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parsePrimary() expression {\n\tswitch a.currentexpression.Type {\n\tcase Variable:\n\t\treturn a.parseVariable()\n\tcase Number:\n\t\treturn a.parseNumber()\n\tcase Operator:\n\t\tif a.currentexpression.expression == \"(\" {\n\t\t\tt := a.getNextexpressionRaw()\n\t\t\tif t == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\te, _ := a.parseExpression()\n\t\t\tif e == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif a.currentexpression.expression != \")\" {\n\t\t\t\ta.err = ufmt.Errorf(\"want ')' but get %s\\n%s\",\n\t\t\t\t\ta.currentexpression.expression,\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\ta.getNextexpressionRaw()\n\t\t\treturn e\n\t\t} else if a.currentexpression.expression == \"-\" {\n\t\t\tif a.getNextexpressionRaw() == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '0-9' but get '-'\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tbin := expressionOperation{\n\t\t\t\tOp: \"-\",\n\t\t\t\tLhs: expressionNumber{},\n\t\t\t\tRhs: a.parsePrimary(),\n\t\t\t}\n\t\t\treturn bin\n\t\t} else {\n\t\t\treturn a.parseNumber()\n\t\t}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (a *ast) parseBinOpRHS(execPrec int, lhs expression) expression {\n\tfor {\n\t\ttokPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c execPrec {\n\t\t\treturn lhs\n\t\t}\n\t\tbinOp := a.currentexpression.expression\n\t\tif a.getNextexpressionRaw() == nil {\n\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\treturn nil\n\t\t}\n\t\trhs := a.parsePrimary()\n\t\tif rhs == nil {\n\t\t\treturn nil\n\t\t}\n\t\tnextPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c nextPrec {\n\t\t\trhs = a.parseBinOpRHS(tokPrec+1, rhs)\n\t\t\tif rhs == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tlhs = expressionOperation{\n\t\t\tOp: binOp,\n\t\t\tLhs: lhs,\n\t\t\tRhs: rhs,\n\t\t}\n\t}\n}\n\nfunc lexer(s string) ([]*expressionRaw, error) {\n\tp := \u0026parser{\n\t\tInput: s,\n\t\terr: nil,\n\t\tch: s[0],\n\t}\n\ttoks := p.parse()\n\tif p.err != nil {\n\t\treturn nil, p.err\n\t}\n\treturn toks, nil\n}\n\nfunc (p *parser) parse() []*expressionRaw {\n\ttoks := make([]*expressionRaw, 0)\n\tfor {\n\t\ttok := p.nextTok()\n\t\tif tok == nil {\n\t\t\tbreak\n\t\t}\n\t\ttoks = append(toks, tok)\n\t}\n\treturn toks\n}\n\nfunc (p *parser) nextTok() *expressionRaw {\n\tif p.offset \u003e= len(p.Input) || p.err != nil {\n\t\treturn nil\n\t}\n\tvar err error\n\tfor p.isWhitespace(p.ch) \u0026\u0026 err == nil {\n\t\terr = p.nextCh()\n\t}\n\tstart := p.offset\n\tvar tok *expressionRaw\n\tswitch p.ch {\n\tcase\n\t\t'(',\n\t\t')',\n\t\t'+',\n\t\t'-',\n\t\t'*',\n\t\t'/',\n\t\t'^',\n\t\t'\u0026',\n\t\t'|',\n\t\t'%':\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: string(p.ch),\n\t\t\tType: Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\terr = p.nextCh()\n\tcase '\u003e', '\u003c':\n\t\ttokS := string(p.ch)\n\t\tbb, be := p.nextChPeek()\n\t\tif be == nil \u0026\u0026 string(bb) == tokS {\n\t\t\ttokS += string(p.ch)\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: tokS,\n\t\t\tType: Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\tif len(tokS) \u003e 1 {\n\t\t\tp.nextCh()\n\t\t}\n\t\terr = p.nextCh()\n\tcase\n\t\t'0',\n\t\t'1',\n\t\t'2',\n\t\t'3',\n\t\t'4',\n\t\t'5',\n\t\t'6',\n\t\t'7',\n\t\t'8',\n\t\t'9':\n\t\tfor p.isDigitNum(p.ch) \u0026\u0026 p.nextCh() == nil {\n\t\t\tif (p.ch == '-' || p.ch == '+') \u0026\u0026 p.Input[p.offset-1] != 'e' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: strings.ReplaceAll(p.Input[start:p.offset], \"_\", \"\"),\n\t\t\tType: Number,\n\t\t}\n\t\ttok.Offset = start\n\tdefault:\n\t\tif p.isChar(p.ch) {\n\t\t\ttok = \u0026expressionRaw{\n\t\t\t\texpression: string(p.ch),\n\t\t\t\tType: Variable,\n\t\t\t}\n\t\t\ttok.Offset = start\n\t\t\terr = p.nextCh()\n\t\t} else if p.ch != ' ' {\n\t\t\tp.err = ufmt.Errorf(\"symbol error: unknown '%v', pos [%v:]\\n%s\",\n\t\t\t\tstring(p.ch),\n\t\t\t\tstart,\n\t\t\t\texpressionError(p.Input, start))\n\t\t}\n\t}\n\treturn tok\n}\n\nfunc (p *parser) nextChPeek() (byte, error) {\n\toffset := p.offset + 1\n\tif offset \u003c len(p.Input) {\n\t\treturn p.Input[offset], nil\n\t}\n\treturn byte(0), errors.New(\"no byte\")\n}\n\nfunc (p *parser) nextCh() error {\n\tp.offset++\n\tif p.offset \u003c len(p.Input) {\n\t\tp.ch = p.Input[p.offset]\n\t\treturn nil\n\t}\n\treturn errors.New(\"EOF\")\n}\n\nfunc (p *parser) isWhitespace(c byte) bool {\n\treturn c == ' ' ||\n\t\tc == '\\t' ||\n\t\tc == '\\n' ||\n\t\tc == '\\v' ||\n\t\tc == '\\f' ||\n\t\tc == '\\r'\n}\n\nfunc (p *parser) isDigitNum(c byte) bool {\n\treturn '0' \u003c= c \u0026\u0026 c \u003c= '9' || c == '.' || c == '_' || c == 'e' || c == '-' || c == '+'\n}\n\nfunc (p *parser) isChar(c byte) bool {\n\treturn 'a' \u003c= c \u0026\u0026 c \u003c= 'z' || 'A' \u003c= c \u0026\u0026 c \u003c= 'Z'\n}\n\nfunc (p *parser) isWordChar(c byte) bool {\n\treturn p.isChar(c) || '0' \u003c= c \u0026\u0026 c \u003c= '9'\n}\n" + }, + { + "name": "int32_test.gno", + "body": "package int32\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\tttt := []struct {\n\t\texp string\n\t\tres int\n\t}{\n\t\t{\"1\", 1},\n\t\t{\"--1\", 1},\n\t\t{\"1+2\", 3},\n\t\t{\"-1+2\", 1},\n\t\t{\"-(1+2)\", -3},\n\t\t{\"-(1+2)*5\", -15},\n\t\t{\"-(1+2)*5/3\", -5},\n\t\t{\"1+(-(1+2)*5/3)\", -4},\n\t\t{\"3^4\", 3 ^ 4},\n\t\t{\"8%2\", 8 % 2},\n\t\t{\"8%3\", 8 % 3},\n\t\t{\"8|3\", 8 | 3},\n\t\t{\"10%2\", 0},\n\t\t{\"(4 + 3)/2-1+11*15\", (4+3)/2 - 1 + 11*15},\n\t\t{\n\t\t\t\"(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64)\",\n\t\t\t(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64),\n\t\t},\n\t\t{\n\t\t\t\"(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64)\",\n\t\t\t(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64),\n\t\t},\n\t\t{\"((0000+1)*0000)\", 0},\n\t}\n\tfor _, tc := range ttt {\n\t\tt.Run(tc.exp, func(t *testing.T) {\n\t\t\texp, err := Parse(tc.exp)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"%s:\\n%s\", tc.exp, err.Error())\n\t\t\t} else {\n\t\t\t\tres, errEval := Eval(exp, nil)\n\t\t\t\tif errEval != nil {\n\t\t\t\t\tt.Errorf(\"eval error: %s\", errEval.Error())\n\t\t\t\t} else if res != tc.res {\n\t\t\t\t\tt.Errorf(\"%s:\\nexpected %d, got %d\", tc.exp, tc.res, res)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVariables(t *testing.T) {\n\tfn := func(x, y int) int {\n\t\treturn 1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\n\t}\n\texpr := \"1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\"\n\texp, err := Parse(expr)\n\tif err != nil {\n\t\tt.Errorf(\"could not parse: %s\", err.Error())\n\t}\n\tvariables := make(map[string]int)\n\tfor i := 0; i \u003c 10; i++ {\n\t\tvariables[\"x\"] = i\n\t\tvariables[\"y\"] = 2\n\t\tres, errEval := Eval(exp, variables)\n\t\tif errEval != nil {\n\t\t\tt.Errorf(\"could not evaluate: %s\", err.Error())\n\t\t}\n\t\texpected := fn(variables[\"x\"], variables[\"y\"])\n\t\tif res != expected {\n\t\t\tt.Errorf(\"expected: %d, actual: %d\", expected, res)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "membstore", + "path": "gno.land/p/demo/membstore", + "files": [ + { + "name": "members.gno", + "body": "package membstore\n\nimport (\n\t\"std\"\n)\n\n// MemberStore defines the member storage abstraction\ntype MemberStore interface {\n\t// Members returns all members in the store\n\tMembers(offset, count uint64) []Member\n\n\t// Size returns the current size of the store\n\tSize() int\n\n\t// IsMember returns a flag indicating if the given address\n\t// belongs to a member\n\tIsMember(address std.Address) bool\n\n\t// TotalPower returns the total voting power of the member store\n\tTotalPower() uint64\n\n\t// Member returns the requested member\n\tMember(address std.Address) (Member, error)\n\n\t// AddMember adds a member to the store\n\tAddMember(member Member) error\n\n\t// UpdateMember updates the member in the store.\n\t// If updating a member's voting power to 0,\n\t// the member will be removed\n\tUpdateMember(address std.Address, member Member) error\n}\n\n// Member holds the relevant member information\ntype Member struct {\n\tAddress std.Address // bech32 gno address of the member (unique)\n\tVotingPower uint64 // the voting power of the member\n}\n" + }, + { + "name": "membstore.gno", + "body": "package membstore\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrAlreadyMember = errors.New(\"address is already a member\")\n\tErrMissingMember = errors.New(\"address is not a member\")\n\tErrInvalidAddressUpdate = errors.New(\"invalid address update\")\n\tErrNotGovDAO = errors.New(\"caller not correct govdao instance\")\n)\n\n// maxRequestMembers is the maximum number of\n// paginated members that can be requested\nconst maxRequestMembers = 50\n\ntype Option func(*MembStore)\n\n// WithInitialMembers initializes the member store\n// with an initial member list\nfunc WithInitialMembers(members []Member) Option {\n\treturn func(store *MembStore) {\n\t\tfor _, m := range members {\n\t\t\tmemberAddr := m.Address.String()\n\n\t\t\t// Check if the member already exists\n\t\t\tif store.members.Has(memberAddr) {\n\t\t\t\tpanic(ufmt.Errorf(\"%s, %s\", memberAddr, ErrAlreadyMember))\n\t\t\t}\n\n\t\t\tstore.members.Set(memberAddr, m)\n\t\t\tstore.totalVotingPower += m.VotingPower\n\t\t}\n\t}\n}\n\n// WithDAOPkgPath initializes the member store\n// with a dao package path guard\nfunc WithDAOPkgPath(daoPkgPath string) Option {\n\treturn func(store *MembStore) {\n\t\tstore.daoPkgPath = daoPkgPath\n\t}\n}\n\n// MembStore implements the dao.MembStore abstraction\ntype MembStore struct {\n\tdaoPkgPath string // active dao pkg path, if any\n\tmembers *avl.Tree // std.Address -\u003e Member\n\ttotalVotingPower uint64 // cached value for quick lookups\n}\n\n// NewMembStore creates a new member store\nfunc NewMembStore(opts ...Option) *MembStore {\n\tm := \u0026MembStore{\n\t\tmembers: avl.NewTree(), // empty set\n\t\tdaoPkgPath: \"\", // no dao guard\n\t\ttotalVotingPower: 0,\n\t}\n\n\t// Apply the options\n\tfor _, opt := range opts {\n\t\topt(m)\n\t}\n\n\treturn m\n}\n\n// AddMember adds member to the member store `m`.\n// It fails if the caller is not GovDAO or\n// if the member is already present\nfunc (m *MembStore) AddMember(member Member) error {\n\tif !m.isCallerDAORealm() {\n\t\treturn ErrNotGovDAO\n\t}\n\n\t// Check if the member exists\n\tif m.IsMember(member.Address) {\n\t\treturn ErrAlreadyMember\n\t}\n\n\t// Add the member\n\tm.members.Set(member.Address.String(), member)\n\n\t// Update the total voting power\n\tm.totalVotingPower += member.VotingPower\n\n\treturn nil\n}\n\n// UpdateMember updates the member with the given address.\n// Updating fails if the caller is not GovDAO.\nfunc (m *MembStore) UpdateMember(address std.Address, member Member) error {\n\tif !m.isCallerDAORealm() {\n\t\treturn ErrNotGovDAO\n\t}\n\n\t// Get the member\n\toldMember, err := m.Member(address)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if this is a removal request\n\tif member.VotingPower == 0 {\n\t\tm.members.Remove(address.String())\n\n\t\t// Update the total voting power\n\t\tm.totalVotingPower -= oldMember.VotingPower\n\n\t\treturn nil\n\t}\n\n\t// Check that the member wouldn't be\n\t// overwriting an existing one\n\tisAddressUpdate := address != member.Address\n\tif isAddressUpdate \u0026\u0026 m.IsMember(member.Address) {\n\t\treturn ErrInvalidAddressUpdate\n\t}\n\n\t// Remove the old member info\n\t// in case the address changed\n\tif address != member.Address {\n\t\tm.members.Remove(address.String())\n\t}\n\n\t// Save the new member info\n\tm.members.Set(member.Address.String(), member)\n\n\t// Update the total voting power\n\tdifference := member.VotingPower - oldMember.VotingPower\n\tm.totalVotingPower += difference\n\n\treturn nil\n}\n\n// IsMember returns a flag indicating if the given\n// address belongs to a member of the member store\nfunc (m *MembStore) IsMember(address std.Address) bool {\n\t_, exists := m.members.Get(address.String())\n\n\treturn exists\n}\n\n// Member returns the member associated with the given address\nfunc (m *MembStore) Member(address std.Address) (Member, error) {\n\tmember, exists := m.members.Get(address.String())\n\tif !exists {\n\t\treturn Member{}, ErrMissingMember\n\t}\n\n\treturn member.(Member), nil\n}\n\n// Members returns a paginated list of members from\n// the member store. If the store is empty, an empty slice\n// is returned instead\nfunc (m *MembStore) Members(offset, count uint64) []Member {\n\t// Calculate the left and right bounds\n\tif count \u003c 1 || offset \u003e= uint64(m.members.Size()) {\n\t\treturn []Member{}\n\t}\n\n\t// Limit the maximum number of returned members\n\tif count \u003e maxRequestMembers {\n\t\tcount = maxRequestMembers\n\t}\n\n\t// Gather the members\n\tmembers := make([]Member, 0)\n\tm.members.IterateByOffset(\n\t\tint(offset),\n\t\tint(count),\n\t\tfunc(_ string, val interface{}) bool {\n\t\t\tmember := val.(Member)\n\n\t\t\t// Save the member\n\t\t\tmembers = append(members, member)\n\n\t\t\treturn false\n\t\t})\n\n\treturn members\n}\n\n// Size returns the number of active members in the member store\nfunc (m *MembStore) Size() int {\n\treturn m.members.Size()\n}\n\n// TotalPower returns the total voting power\n// of the member store\nfunc (m *MembStore) TotalPower() uint64 {\n\treturn m.totalVotingPower\n}\n\n// isCallerDAORealm returns a flag indicating if the\n// current caller context is the active DAO Realm.\n// We need to include a dao guard, even if the\n// executor guarantees it, because\n// the API of the member store is public and callable\n// by anyone who has a reference to the member store instance.\nfunc (m *MembStore) isCallerDAORealm() bool {\n\treturn m.daoPkgPath == \"\" || std.CurrentRealm().PkgPath() == m.daoPkgPath\n}\n" + }, + { + "name": "membstore_test.gno", + "body": "package membstore\n\nimport (\n\t\"testing\"\n\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateMembers generates dummy govdao members\nfunc generateMembers(t *testing.T, count int) []Member {\n\tt.Helper()\n\n\tmembers := make([]Member, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tmembers = append(members, Member{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"member %d\", i)),\n\t\t\tVotingPower: 10,\n\t\t})\n\t}\n\n\treturn members\n}\n\nfunc TestMembStore_GetMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"member not found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\t_, err := m.Member(testutils.TestAddress(\"random\"))\n\t\tuassert.ErrorIs(t, err, ErrMissingMember)\n\t})\n\n\tt.Run(\"valid member fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\t_, err := m.Member(members[0].Address)\n\t\tuassert.NoError(t, err)\n\t})\n}\n\nfunc TestMembStore_GetMembers(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no members\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tmembers := m.Members(0, 10)\n\t\tuassert.Equal(t, 0, len(members))\n\t})\n\n\tt.Run(\"proper pagination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumMembers = maxRequestMembers * 2\n\t\t\thalfRange = numMembers / 2\n\n\t\t\tmembers = generateMembers(t, numMembers)\n\t\t\tm = NewMembStore(WithInitialMembers(members))\n\n\t\t\tverifyMembersPresent = func(members, fetchedMembers []Member) {\n\t\t\t\tfor _, fetchedMember := range fetchedMembers {\n\t\t\t\t\tfor _, member := range members {\n\t\t\t\t\t\tif member.Address != fetchedMember.Address {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tuassert.Equal(t, member.VotingPower, fetchedMember.VotingPower)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\turequire.Equal(t, numMembers, m.Size())\n\n\t\tfetchedMembers := m.Members(0, uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedMembers))\n\n\t\t// Verify the members\n\t\tverifyMembersPresent(members, fetchedMembers)\n\n\t\t// Fetch the other half\n\t\tfetchedMembers = m.Members(uint64(halfRange), uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedMembers))\n\n\t\t// Verify the members\n\t\tverifyMembersPresent(members, fetchedMembers)\n\t})\n}\n\nfunc TestMembStore_IsMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"non-existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tuassert.False(t, m.IsMember(testutils.TestAddress(\"random\")))\n\t})\n\n\tt.Run(\"existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 50)\n\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\tfor _, member := range members {\n\t\t\tuassert.True(t, m.IsMember(member.Address))\n\t\t}\n\t})\n}\n\nfunc TestMembStore_AddMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller not govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore(WithDAOPkgPath(\"gno.land/r/gov/dao\"))\n\n\t\t// Attempt to add a member\n\t\tmember := generateMembers(t, 1)[0]\n\t\tuassert.ErrorIs(t, m.AddMember(member), ErrNotGovDAO)\n\t})\n\n\tt.Run(\"member already exists\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\t// Attempt to add a member\n\t\tuassert.ErrorIs(t, m.AddMember(members[0]), ErrAlreadyMember)\n\t})\n\n\tt.Run(\"new member added\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create an empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath))\n\n\t\t// Attempt to add a member\n\t\turequire.NoError(t, m.AddMember(members[0]))\n\n\t\t// Make sure the member is added\n\t\tuassert.True(t, m.IsMember(members[0].Address))\n\t})\n}\n\nfunc TestMembStore_Size(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tuassert.Equal(t, 0, m.Size())\n\t})\n\n\tt.Run(\"non-empty govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 50)\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\tuassert.Equal(t, len(members), m.Size())\n\t})\n}\n\nfunc TestMembStore_UpdateMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller not govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore(WithDAOPkgPath(\"gno.land/r/gov/dao\"))\n\n\t\t// Attempt to update a member\n\t\tmember := generateMembers(t, 1)[0]\n\t\tuassert.ErrorIs(t, m.UpdateMember(member.Address, member), ErrNotGovDAO)\n\t})\n\n\tt.Run(\"non-existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create an empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath))\n\n\t\t// Attempt to update a member\n\t\tuassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), ErrMissingMember)\n\t})\n\n\tt.Run(\"overwrite member attempt\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 2)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\t// Attempt to update a member\n\t\tuassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), ErrInvalidAddressUpdate)\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\toldVotingPower := m.totalVotingPower\n\t\turequire.Equal(t, members[0].VotingPower, oldVotingPower)\n\n\t\tvotingPower := uint64(300)\n\t\tmembers[0].VotingPower = votingPower\n\n\t\t// Attempt to update a member\n\t\tuassert.NoError(t, m.UpdateMember(members[0].Address, members[0]))\n\t\tuassert.Equal(t, votingPower, m.Members(0, 10)[0].VotingPower)\n\t\turequire.Equal(t, votingPower, m.totalVotingPower)\n\t})\n\n\tt.Run(\"member removed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\tvotingPower := uint64(0)\n\t\tmembers[0].VotingPower = votingPower\n\n\t\t// Attempt to update a member\n\t\tuassert.NoError(t, m.UpdateMember(members[0].Address, members[0]))\n\n\t\t// Make sure the member was removed\n\t\tuassert.False(t, m.IsMember(members[0].Address))\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "ownable", + "path": "gno.land/p/demo/ownable", + "files": [ + { + "name": "errors.gno", + "body": "package ownable\n\nimport \"errors\"\n\nvar (\n\tErrUnauthorized = errors.New(\"ownable: caller is not owner\")\n\tErrInvalidAddress = errors.New(\"ownable: new owner address is invalid\")\n)\n" + }, + { + "name": "ownable.gno", + "body": "package ownable\n\nimport \"std\"\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\n// Ownable is meant to be used as a top-level object to make your contract ownable OR\n// being embedded in a Gno object to manage per-object ownership.\ntype Ownable struct {\n\towner std.Address\n}\n\nfunc New() *Ownable {\n\treturn \u0026Ownable{\n\t\towner: std.PrevRealm().Addr(),\n\t}\n}\n\nfunc NewWithAddress(addr std.Address) *Ownable {\n\treturn \u0026Ownable{\n\t\towner: addr,\n\t}\n}\n\n// TransferOwnership transfers ownership of the Ownable struct to a new address\nfunc (o *Ownable) TransferOwnership(newOwner std.Address) error {\n\terr := o.CallerIsOwner()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !newOwner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tprevOwner := o.owner\n\to.owner = newOwner\n\tstd.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", string(prevOwner),\n\t\t\"to\", string(newOwner),\n\t)\n\n\treturn nil\n}\n\n// DropOwnership removes the owner, effectively disabling any owner-related actions\n// Top-level usage: disables all only-owner actions/functions,\n// Embedded usage: behaves like a burn functionality, removing the owner from the struct\nfunc (o *Ownable) DropOwnership() error {\n\terr := o.CallerIsOwner()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tprevOwner := o.owner\n\to.owner = \"\"\n\n\tstd.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", string(prevOwner),\n\t\t\"to\", \"\",\n\t)\n\n\treturn nil\n}\n\n// Owner returns the owner address from Ownable\nfunc (o Ownable) Owner() std.Address {\n\treturn o.owner\n}\n\n// CallerIsOwner checks if the caller of the function is the Realm's owner\nfunc (o Ownable) CallerIsOwner() error {\n\tif std.PrevRealm().Addr() == o.owner {\n\t\treturn nil\n\t}\n\n\treturn ErrUnauthorized\n}\n\n// AssertCallerIsOwner panics if the caller is not the owner\nfunc (o Ownable) AssertCallerIsOwner() {\n\tif std.PrevRealm().Addr() != o.owner {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n" + }, + { + "name": "ownable_test.gno", + "body": "package ownable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n)\n\nfunc TestNew(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // TODO(bug): should not be needed\n\n\to := New()\n\tgot := o.Owner()\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestNewWithAddress(t *testing.T) {\n\to := NewWithAddress(alice)\n\n\tgot := o.Owner()\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestTransferOwnership(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.TransferOwnership(bob)\n\tif err != nil {\n\t\tt.Fatalf(\"TransferOwnership failed, %v\", err)\n\t}\n\n\tgot := o.Owner()\n\tif bob != got {\n\t\tt.Fatalf(\"Expected: %s, got: %s\", bob, got)\n\t}\n}\n\nfunc TestCallerIsOwner(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\tunauthorizedCaller := bob\n\n\tstd.TestSetRealm(std.NewUserRealm(unauthorizedCaller))\n\tstd.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed\n\n\terr := o.CallerIsOwner()\n\tuassert.Error(t, err) // XXX: IsError(..., unauthorizedCaller)\n}\n\nfunc TestDropOwnership(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.DropOwnership()\n\tuassert.NoError(t, err, \"DropOwnership failed\")\n\n\towner := o.Owner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n}\n\n// Errors\n\nfunc TestErrUnauthorized(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // TODO(bug): should not be needed\n\n\to := New()\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob) // TODO(bug): should not be needed\n\n\terr := o.TransferOwnership(alice)\n\tif err != ErrUnauthorized {\n\t\tt.Fatalf(\"Should've been ErrUnauthorized, was %v\", err)\n\t}\n\n\terr = o.DropOwnership()\n\tuassert.ErrorContains(t, err, ErrUnauthorized.Error())\n}\n\nfunc TestErrInvalidAddress(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.TransferOwnership(\"\")\n\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\n\terr = o.TransferOwnership(\"10000000001000000000100000000010000000001000000000\")\n\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "seqid", + "path": "gno.land/p/demo/seqid", + "files": [ + { + "name": "README.md", + "body": "# seqid\n\n```\npackage seqid // import \"gno.land/p/demo/seqid\"\n\nPackage seqid provides a simple way to have sequential IDs which will be ordered\ncorrectly when inserted in an AVL tree.\n\nSample usage:\n\n var id seqid.ID\n var users avl.Tree\n\n func NewUser() {\n \tusers.Set(id.Next().Binary(), \u0026User{ ... })\n }\n\nTYPES\n\ntype ID uint64\n An ID is a simple sequential ID generator.\n\nfunc FromBinary(b string) (ID, bool)\n FromBinary creates a new ID from the given string.\n\nfunc (i ID) Binary() string\n Binary returns a big-endian binary representation of the ID, suitable to be\n used as an AVL key.\n\nfunc (i *ID) Next() ID\n Next advances the ID i. It will panic if increasing ID would overflow.\n\nfunc (i *ID) TryNext() (ID, bool)\n TryNext increases i by 1 and returns its value. It returns true if\n successful, or false if the increment would result in an overflow.\n```\n" + }, + { + "name": "seqid.gno", + "body": "// Package seqid provides a simple way to have sequential IDs which will be\n// ordered correctly when inserted in an AVL tree.\n//\n// Sample usage:\n//\n//\tvar id seqid.ID\n//\tvar users avl.Tree\n//\n//\tfunc NewUser() {\n//\t\tusers.Set(id.Next().String(), \u0026User{ ... })\n//\t}\npackage seqid\n\nimport (\n\t\"encoding/binary\"\n\n\t\"gno.land/p/demo/cford32\"\n)\n\n// An ID is a simple sequential ID generator.\ntype ID uint64\n\n// Next advances the ID i.\n// It will panic if increasing ID would overflow.\nfunc (i *ID) Next() ID {\n\tnext, ok := i.TryNext()\n\tif !ok {\n\t\tpanic(\"seqid: next ID overflows uint64\")\n\t}\n\treturn next\n}\n\nconst maxID ID = 1\u003c\u003c64 - 1\n\n// TryNext increases i by 1 and returns its value.\n// It returns true if successful, or false if the increment would result in\n// an overflow.\nfunc (i *ID) TryNext() (ID, bool) {\n\tif *i == maxID {\n\t\t// Addition will overflow.\n\t\treturn 0, false\n\t}\n\t*i++\n\treturn *i, true\n}\n\n// Binary returns a big-endian binary representation of the ID,\n// suitable to be used as an AVL key.\nfunc (i ID) Binary() string {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(buf, uint64(i))\n\treturn string(buf)\n}\n\n// String encodes i using cford32's compact encoding. For more information,\n// see the documentation for package [gno.land/p/demo/cford32].\n//\n// The result of String will be a 7-byte string for IDs [0,2^34), and a\n// 13-byte string for all values following that. All generated string IDs\n// follow the same lexicographic order as their number values; that is, for any\n// two IDs (x, y) such that x \u003c y, x.String() \u003c y.String().\n// As such, this string representation is suitable to be used as an AVL key.\nfunc (i ID) String() string {\n\treturn string(cford32.PutCompact(uint64(i)))\n}\n\n// FromBinary creates a new ID from the given string, expected to be a binary\n// big-endian encoding of an ID (such as that of [ID.Binary]).\n// The second return value is true if the conversion was successful.\nfunc FromBinary(b string) (ID, bool) {\n\tif len(b) != 8 {\n\t\treturn 0, false\n\t}\n\treturn ID(binary.BigEndian.Uint64([]byte(b))), true\n}\n\n// FromString creates a new ID from the given string, expected to be a string\n// representation using cford32, such as that returned by [ID.String].\n//\n// The encoding scheme used by cford32 allows the same ID to have many\n// different representations (though the one returned by [ID.String] is only\n// one, deterministic and safe to be used in AVL). The encoding scheme is\n// \"human-centric\" and is thus case insensitive, and maps some ambiguous\n// characters to be the same, ie. L = I = 1, O = 0. For this reason, when\n// parsing user input to retrieve a key (encoded as a string), always sanitize\n// it first using FromString, then run String(), instead of using the user's\n// input directly.\nfunc FromString(b string) (ID, error) {\n\tn, err := cford32.Uint64([]byte(b))\n\treturn ID(n), err\n}\n" + }, + { + "name": "seqid_test.gno", + "body": "package seqid\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestID(t *testing.T) {\n\tvar i ID\n\n\tfor j := 0; j \u003c 100; j++ {\n\t\ti.Next()\n\t}\n\tif i != 100 {\n\t\tt.Fatalf(\"invalid: wanted %d got %d\", 100, i)\n\t}\n}\n\nfunc TestID_Overflow(t *testing.T) {\n\ti := ID(maxID)\n\n\tdefer func() {\n\t\terr := recover()\n\t\tif !strings.Contains(fmt.Sprint(err), \"next ID overflows\") {\n\t\t\tt.Errorf(\"did not overflow\")\n\t\t}\n\t}()\n\n\ti.Next()\n}\n\nfunc TestID_Binary(t *testing.T) {\n\tvar i ID\n\tprev := i.Binary()\n\n\tfor j := 0; j \u003c 1000; j++ {\n\t\tcur := i.Next().Binary()\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %x \u003e prev %x\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n}\n\nfunc TestID_String(t *testing.T) {\n\tvar i ID\n\tprev := i.String()\n\n\tfor j := 0; j \u003c 1000; j++ {\n\t\tcur := i.Next().String()\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %s \u003e prev %s\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n\n\t// Test for when cford32 switches over to the long encoding.\n\ti = 1\u003c\u003c34 - 512\n\tfor j := 0; j \u003c 1024; j++ {\n\t\tcur := i.Next().String()\n\t\t// println(cur)\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %s \u003e prev %s\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "memeland", + "path": "gno.land/p/demo/memeland", + "files": [ + { + "name": "memeland.gno", + "body": "package memeland\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nconst (\n\tDATE_CREATED = \"DATE_CREATED\"\n\tUPVOTES = \"UPVOTES\"\n)\n\ntype Post struct {\n\tID string\n\tData string\n\tAuthor std.Address\n\tTimestamp time.Time\n\tUpvoteTracker *avl.Tree // address \u003e struct{}{}\n}\n\ntype Memeland struct {\n\t*ownable.Ownable\n\tPosts []*Post\n\tMemeCounter seqid.ID\n}\n\nfunc NewMemeland() *Memeland {\n\treturn \u0026Memeland{\n\t\tOwnable: ownable.New(),\n\t\tPosts: make([]*Post, 0),\n\t}\n}\n\n// PostMeme - Adds a new post\nfunc (m *Memeland) PostMeme(data string, timestamp int64) string {\n\tif data == \"\" || timestamp \u003c= 0 {\n\t\tpanic(\"timestamp or data cannot be empty\")\n\t}\n\n\t// Generate ID\n\tid := m.MemeCounter.Next().String()\n\n\tnewPost := \u0026Post{\n\t\tID: id,\n\t\tData: data,\n\t\tAuthor: std.PrevRealm().Addr(),\n\t\tTimestamp: time.Unix(timestamp, 0),\n\t\tUpvoteTracker: avl.NewTree(),\n\t}\n\n\tm.Posts = append(m.Posts, newPost)\n\treturn id\n}\n\nfunc (m *Memeland) Upvote(id string) string {\n\tpost := m.getPost(id)\n\tif post == nil {\n\t\tpanic(\"post with specified ID does not exist\")\n\t}\n\n\tcaller := std.PrevRealm().Addr().String()\n\n\tif _, exists := post.UpvoteTracker.Get(caller); exists {\n\t\tpanic(\"user has already upvoted this post\")\n\t}\n\n\tpost.UpvoteTracker.Set(caller, struct{}{})\n\n\treturn \"upvote successful\"\n}\n\n// GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination\nfunc (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\tif len(m.Posts) == 0 {\n\t\treturn \"[]\"\n\t}\n\n\tif page \u003c 1 {\n\t\tpanic(\"page number cannot be less than 1\")\n\t}\n\n\t// No empty pages\n\tif pageSize \u003c 1 {\n\t\tpanic(\"page size cannot be less than 1\")\n\t}\n\n\t// No pages larger than 10\n\tif pageSize \u003e 10 {\n\t\tpanic(\"page size cannot be larger than 10\")\n\t}\n\n\t// Need to pass in a sort parameter\n\tif sortBy == \"\" {\n\t\tpanic(\"sort order cannot be empty\")\n\t}\n\n\tvar filteredPosts []*Post\n\n\tstart := time.Unix(startTimestamp, 0)\n\tend := time.Unix(endTimestamp, 0)\n\n\t// Filtering posts\n\tfor _, p := range m.Posts {\n\t\tif !p.Timestamp.Before(start) \u0026\u0026 !p.Timestamp.After(end) {\n\t\t\tfilteredPosts = append(filteredPosts, p)\n\t\t}\n\t}\n\n\tswitch sortBy {\n\t// Sort by upvote descending\n\tcase UPVOTES:\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].UpvoteTracker.Size() \u003e filteredPosts[j].UpvoteTracker.Size()\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tcase DATE_CREATED:\n\t\t// Sort by timestamp, beginning with newest\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].Timestamp.After(filteredPosts[j].Timestamp)\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tdefault:\n\t\tpanic(\"sort order can only be \\\"UPVOTES\\\" or \\\"DATE_CREATED\\\"\")\n\t}\n\n\t// Pagination\n\tstartIndex := (page - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\n\t// If page does not contain any posts\n\tif startIndex \u003e= len(filteredPosts) {\n\t\treturn \"[]\"\n\t}\n\n\t// If page contains fewer posts than the page size\n\tif endIndex \u003e len(filteredPosts) {\n\t\tendIndex = len(filteredPosts)\n\t}\n\n\t// Return JSON representation of paginated and sorted posts\n\treturn PostsToJSONString(filteredPosts[startIndex:endIndex])\n}\n\n// RemovePost allows the owner to remove a post with a specific ID\nfunc (m *Memeland) RemovePost(id string) string {\n\tif id == \"\" {\n\t\tpanic(\"id cannot be empty\")\n\t}\n\n\tif err := m.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor i, post := range m.Posts {\n\t\tif post.ID == id {\n\t\t\tm.Posts = append(m.Posts[:i], m.Posts[i+1:]...)\n\t\t\treturn id\n\t\t}\n\t}\n\n\tpanic(\"post with specified id does not exist\")\n}\n\n// PostsToJSONString converts a slice of Post structs into a JSON string\nfunc PostsToJSONString(posts []*Post) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"[\")\n\n\tfor i, post := range posts {\n\t\tif i \u003e 0 {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\n\t\tsb.WriteString(PostToJSONString(post))\n\t}\n\tsb.WriteString(\"]\")\n\n\treturn sb.String()\n}\n\n// PostToJSONString returns a Post formatted as a JSON string\nfunc PostToJSONString(post *Post) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"{\")\n\tsb.WriteString(`\"id\":\"` + post.ID + `\",`)\n\tsb.WriteString(`\"data\":\"` + escapeString(post.Data) + `\",`)\n\tsb.WriteString(`\"author\":\"` + escapeString(post.Author.String()) + `\",`)\n\tsb.WriteString(`\"timestamp\":\"` + strconv.Itoa(int(post.Timestamp.Unix())) + `\",`)\n\tsb.WriteString(`\"upvotes\":` + strconv.Itoa(post.UpvoteTracker.Size()))\n\tsb.WriteString(\"}\")\n\n\treturn sb.String()\n}\n\n// escapeString escapes quotes in a string for JSON compatibility.\nfunc escapeString(s string) string {\n\treturn strings.ReplaceAll(s, `\"`, `\\\"`)\n}\n\nfunc (m *Memeland) getPost(id string) *Post {\n\tfor _, p := range m.Posts {\n\t\tif p.ID == id {\n\t\t\treturn p\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PostSorter is a flexible sorter for the *Post slice\ntype PostSorter struct {\n\tPosts []*Post\n\tLessF func(i, j int) bool\n}\n\nfunc (p PostSorter) Len() int {\n\treturn len(p.Posts)\n}\n\nfunc (p PostSorter) Swap(i, j int) {\n\tp.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i]\n}\n\nfunc (p PostSorter) Less(i, j int) bool {\n\treturn p.LessF(i, j)\n}\n" + }, + { + "name": "memeland_test.gno", + "body": "package memeland\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestPostMeme(t *testing.T) {\n\tm := NewMemeland()\n\tid := m.PostMeme(\"Test meme data\", time.Now().Unix())\n\tuassert.NotEqual(t, \"\", string(id), \"Expected valid ID, got empty string\")\n}\n\nfunc TestGetPostsInRangePagination(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttestCases := []struct {\n\t\tpage int\n\t\tpageSize int\n\t\texpectedNumOfPosts int\n\t}{\n\t\t{page: 1, pageSize: 1, expectedNumOfPosts: 1}, // one per page\n\t\t{page: 2, pageSize: 1, expectedNumOfPosts: 1}, // one on second page\n\t\t{page: 1, pageSize: numOfPosts, expectedNumOfPosts: numOfPosts}, // all posts on single page\n\t\t{page: 12, pageSize: 1, expectedNumOfPosts: 0}, // empty page\n\t\t{page: 1, pageSize: numOfPosts + 1, expectedNumOfPosts: numOfPosts}, // page with fewer posts than its size\n\t\t{page: 5, pageSize: numOfPosts / 5, expectedNumOfPosts: 1}, // evenly distribute posts per page\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(ufmt.Sprintf(\"Page%d_Size%d\", tc.page, tc.pageSize), func(t *testing.T) {\n\t\t\tresult := m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), tc.page, tc.pageSize, \"DATE_CREATED\")\n\n\t\t\t// Count posts by how many times id: shows up in JSON string\n\t\t\tpostCount := strings.Count(result, `\"id\":\"`)\n\t\t\tuassert.Equal(t, tc.expectedNumOfPosts, postCount)\n\t\t})\n\t}\n}\n\nfunc TestGetPostsInRangeByTimestamp(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(), // end at latest post\n\t\t1, // first page\n\t\tnumOfPosts, // all memes on the page\n\t\t\"DATE_CREATED\", // sort by newest first\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if data is there\n\tfor _, expData := range memeData {\n\t\tcheck := strings.Contains(jsonStr, expData)\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s in the JSON string, but counld't find it\", expData))\n\t}\n\n\t// Check if ordering is correct, sort by created date\n\tfor i := 0; i \u003c len(memeData)-2; i++ {\n\t\tcheck := strings.Index(jsonStr, memeData[i]) \u003e= strings.Index(jsonStr, memeData[i+1])\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s, but was at %d, and %d\", memeData[i], memeData[i+1], i, i+1))\n\t}\n}\n\nfunc TestGetPostsInRangeByUpvote(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tmemeData1 := \"Meme #1\"\n\tmemeData2 := \"Meme #2\"\n\n\t// Create posts at specific times for testing\n\tid1 := m.PostMeme(memeData1, now.Unix())\n\tid2 := m.PostMeme(memeData2, now.Add(time.Minute).Unix())\n\n\tm.Upvote(id1)\n\tm.Upvote(id2)\n\n\t// Change caller so avoid double upvote panic\n\tstd.TestSetOrigCaller(testutils.TestAddress(\"alice\"))\n\tm.Upvote(id1)\n\n\t// Final upvote count:\n\t// Meme #1 - 2 upvote\n\t// Meme #2 - 1 upvotes\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-time.Minute)\n\tafterLatest := now.Add(time.Hour)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(), // end at latest post\n\t\t1, // first page\n\t\t2, // all memes on the page\n\t\t\"UPVOTES\", // sort by upvote\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if ordering is correct\n\tcheck := strings.Index(jsonStr, \"Meme #1\") \u003c= strings.Index(jsonStr, \"Meme #2\")\n\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s\", memeData1, memeData2))\n}\n\nfunc TestBadSortBy(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttests := []struct {\n\t\tname string\n\t\tsortBy string\n\t\twantPanic string\n\t}{\n\t\t{\n\t\t\tname: \"Empty sortBy\",\n\t\t\tsortBy: \"\",\n\t\t\twantPanic: \"runtime error: index out of range\",\n\t\t},\n\t\t{\n\t\t\tname: \"Wrong sortBy\",\n\t\t\tsortBy: \"random string\",\n\t\t\twantPanic: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\t// Panics should be caught\n\t\t\t_ = m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), 1, 1, tc.sortBy)\n\t\t})\n\t}\n}\n\nfunc TestNoPosts(t *testing.T) {\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\n\tjsonStr := m.GetPostsInRange(0, now, 1, 1, \"DATE_CREATED\")\n\n\tuassert.Equal(t, jsonStr, \"[]\")\n}\n\nfunc TestUpvote(t *testing.T) {\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\tpostID := m.PostMeme(\"Test meme data\", now)\n\n\t// Initial upvote count should be 0\n\tpost := m.getPost(postID)\n\tuassert.Equal(t, 0, post.UpvoteTracker.Size())\n\n\t// Upvote the post\n\tupvoteResult := m.Upvote(postID)\n\tuassert.Equal(t, \"upvote successful\", upvoteResult)\n\n\t// Retrieve the post again and check the upvote count\n\tpost = m.getPost(postID)\n\tuassert.Equal(t, 1, post.UpvoteTracker.Size())\n}\n\nfunc TestDelete(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tstd.TestSetOrigCaller(alice)\n\n\t// Alice is admin\n\tm := NewMemeland()\n\n\t// Set caller to Bob\n\tbob := testutils.TestAddress(\"bob\")\n\tstd.TestSetOrigCaller(bob)\n\n\t// Bob adds post to Memeland\n\tnow := time.Now()\n\tpostID := m.PostMeme(\"Meme #1\", now.Unix())\n\n\t// Alice removes Bob's post\n\tstd.TestSetOrigCaller(alice)\n\n\tid := m.RemovePost(postID)\n\tuassert.Equal(t, postID, id, \"post IDs not matching\")\n\tuassert.Equal(t, 0, len(m.Posts), \"there should be 0 posts after removing\")\n}\n\nfunc TestDeleteByNonAdmin(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tstd.TestSetOrigCaller(alice)\n\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now()\n\tpostID := m.PostMeme(\"Meme #1\", now.Unix())\n\n\t// Bob will try to delete meme posted by Alice, which should fail\n\tbob := testutils.TestAddress(\"bob\")\n\tstd.TestSetOrigCaller(bob)\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t}\n\t}()\n\n\t// Should panic - caught by defer\n\tm.RemovePost(postID)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "merkle", + "path": "gno.land/p/demo/merkle", + "files": [ + { + "name": "README.md", + "body": "# p/demo/merkle\n\nThis package implement a merkle tree that is complient with [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n## [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n```javascript\nconst { MerkleTree } = require(\"merkletreejs\");\nconst SHA256 = require(\"crypto-js/sha256\");\n\nlet leaves = [];\nfor (let i = 0; i \u003c 10; i++) {\n leaves.push(SHA256(`node_${i}`));\n}\n\nconst tree = new MerkleTree(leaves, SHA256);\nconst root = tree.getRoot().toString(\"hex\");\n\nconsole.log(root); // cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\n```\n" + }, + { + "name": "merkle.gno", + "body": "package merkle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\ntype Hashable interface {\n\tBytes() []byte\n}\n\ntype nodes []Node\n\ntype Node struct {\n\thash []byte\n\n\tposition uint8\n}\n\nfunc NewNode(hash []byte, position uint8) Node {\n\treturn Node{\n\t\thash: hash,\n\t\tposition: position,\n\t}\n}\n\nfunc (n Node) Position() uint8 {\n\treturn n.position\n}\n\nfunc (n Node) Hash() string {\n\treturn hex.EncodeToString(n.hash[:])\n}\n\ntype Tree struct {\n\tlayers []nodes\n}\n\n// Root return the merkle root of the tree\nfunc (t *Tree) Root() string {\n\tfor _, l := range t.layers {\n\t\tif len(l) == 1 {\n\t\t\treturn l[0].Hash()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// NewTree create a new Merkle Tree\nfunc NewTree(data []Hashable) *Tree {\n\ttree := \u0026Tree{}\n\n\tleaves := make([]Node, len(data))\n\n\tfor i, d := range data {\n\t\thash := sha256.Sum256(d.Bytes())\n\t\tleaves[i] = Node{hash: hash[:]}\n\t}\n\n\ttree.layers = []nodes{nodes(leaves)}\n\n\tvar buff bytes.Buffer\n\tfor len(leaves) \u003e 1 {\n\t\tlevel := make([]Node, 0, len(leaves)/2+1)\n\t\tfor i := 0; i \u003c len(leaves); i += 2 {\n\t\t\tbuff.Reset()\n\n\t\t\tif i \u003c len(leaves)-1 {\n\t\t\t\tbuff.Write(leaves[i].hash)\n\t\t\t\tbuff.Write(leaves[i+1].hash)\n\t\t\t\thash := sha256.Sum256(buff.Bytes())\n\t\t\t\tlevel = append(level, Node{\n\t\t\t\t\thash: hash[:],\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tlevel = append(level, leaves[i])\n\t\t\t}\n\t\t}\n\t\tleaves = level\n\t\ttree.layers = append(tree.layers, level)\n\t}\n\treturn tree\n}\n\n// Proof return a MerkleProof\nfunc (t *Tree) Proof(data Hashable) ([]Node, error) {\n\ttargetHash := sha256.Sum256(data.Bytes())\n\ttargetIndex := -1\n\n\tfor i, layer := range t.layers[0] {\n\t\tif bytes.Equal(targetHash[:], layer.hash) {\n\t\t\ttargetIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetIndex == -1 {\n\t\treturn nil, errors.New(\"target not found\")\n\t}\n\n\tproofs := make([]Node, 0, len(t.layers))\n\n\tfor _, layer := range t.layers {\n\t\tvar pairIndex int\n\n\t\tif targetIndex%2 == 0 {\n\t\t\tpairIndex = targetIndex + 1\n\t\t} else {\n\t\t\tpairIndex = targetIndex - 1\n\t\t}\n\t\tif pairIndex \u003c len(layer) {\n\t\t\tproofs = append(proofs, Node{\n\t\t\t\thash: layer[pairIndex].hash,\n\t\t\t\tposition: uint8(targetIndex) % 2,\n\t\t\t})\n\t\t}\n\t\ttargetIndex /= 2\n\t}\n\treturn proofs, nil\n}\n\n// Verify if a merkle proof is valid\nfunc (t *Tree) Verify(leaf Hashable, proofs []Node) bool {\n\treturn Verify(t.Root(), leaf, proofs)\n}\n\n// Verify if a merkle proof is valid\nfunc Verify(root string, leaf Hashable, proofs []Node) bool {\n\thash := sha256.Sum256(leaf.Bytes())\n\n\tfor i := 0; i \u003c len(proofs); i += 1 {\n\t\tvar h []byte\n\t\tif proofs[i].position == 0 {\n\t\t\th = append(hash[:], proofs[i].hash...)\n\t\t} else {\n\t\t\th = append(proofs[i].hash, hash[:]...)\n\t\t}\n\t\thash = sha256.Sum256(h)\n\t}\n\treturn hex.EncodeToString(hash[:]) == root\n}\n" + }, + { + "name": "merkle_test.gno", + "body": "package merkle\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype testData struct {\n\tcontent string\n}\n\nfunc (d testData) Bytes() []byte {\n\treturn []byte(d.content)\n}\n\nfunc TestMerkleTree(t *testing.T) {\n\ttests := []struct {\n\t\tsize int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tsize: 1,\n\t\t\texpected: \"cf9f824bce7f5bc63d557b23591f58577f53fe29f974a615bdddbd0140f912f4\",\n\t\t},\n\t\t{\n\t\t\tsize: 3,\n\t\t\texpected: \"1a4a5f0fa267244bf9f74a63fdf2a87eed5e97e4bd104a9e94728c8fb5442177\",\n\t\t},\n\t\t{\n\t\t\tsize: 10,\n\t\t\texpected: \"cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\",\n\t\t},\n\t\t{\n\t\t\tsize: 1000,\n\t\t\texpected: \"fa533d2efdf12be26bc410dfa42936ac63361324e35e9b1ff54d422a1dd2388b\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar leaves []Hashable\n\t\tfor i := 0; i \u003c test.size; i++ {\n\t\t\tleaves = append(leaves, testData{fmt.Sprintf(\"node_%d\", i)})\n\t\t}\n\n\t\ttree := NewTree(leaves)\n\n\t\tif tree == nil {\n\t\t\tt.Error(\"Merkle tree creation failed\")\n\t\t}\n\n\t\troot := tree.Root()\n\n\t\tif root != test.expected {\n\t\t\tt.Fatalf(\"merkle.Tree.Root(), expected: %s; got: %s\", test.expected, root)\n\t\t}\n\n\t\tfor _, leaf := range leaves {\n\t\t\tproofs, err := tree.Proof(leaf)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to proof leaf: %v, on tree: %v\", leaf, test)\n\t\t\t}\n\n\t\t\tok := Verify(root, leaf, proofs)\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"failed to verify leaf: %v, on tree: %v\", leaf, tree)\n\t\t\t}\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "microblog", + "path": "gno.land/p/demo/microblog", + "files": [ + { + "name": "microblog.gno", + "body": "package microblog\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrNotFound = errors.New(\"not found\")\n\tStatusNotFound = \"404\"\n)\n\ntype Microblog struct {\n\tTitle string\n\tPrefix string // i.e. r/gnoland/blog:\n\tPages avl.Tree // author (string) -\u003e Page\n}\n\nfunc NewMicroblog(title string, prefix string) (m *Microblog) {\n\treturn \u0026Microblog{\n\t\tTitle: title,\n\t\tPrefix: prefix,\n\t\tPages: avl.Tree{},\n\t}\n}\n\nfunc (m *Microblog) GetPages() []*Page {\n\tvar (\n\t\tpages = make([]*Page, m.Pages.Size())\n\t\tindex = 0\n\t)\n\n\tm.Pages.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpages[index] = value.(*Page)\n\t\tindex++\n\t\treturn false\n\t})\n\n\tsort.Sort(byLastPosted(pages))\n\n\treturn pages\n}\n\nfunc (m *Microblog) NewPost(text string) error {\n\tauthor := std.GetOrigCaller()\n\t_, found := m.Pages.Get(author.String())\n\tif !found {\n\t\t// make a new page for the new author\n\t\tm.Pages.Set(author.String(), \u0026Page{\n\t\t\tAuthor: author,\n\t\t\tCreatedAt: time.Now(),\n\t\t})\n\t}\n\n\tpage, err := m.GetPage(author.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn page.NewPost(text)\n}\n\nfunc (m *Microblog) GetPage(author string) (*Page, error) {\n\tsilo, found := m.Pages.Get(author)\n\tif !found {\n\t\treturn nil, ErrNotFound\n\t}\n\treturn silo.(*Page), nil\n}\n\ntype Page struct {\n\tID int\n\tAuthor std.Address\n\tCreatedAt time.Time\n\tLastPosted time.Time\n\tPosts avl.Tree // time -\u003e Post\n}\n\n// byLastPosted implements sort.Interface for []Page based on\n// the LastPosted field.\ntype byLastPosted []*Page\n\nfunc (a byLastPosted) Len() int { return len(a) }\nfunc (a byLastPosted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }\n\nfunc (p *Page) NewPost(text string) error {\n\tnow := time.Now()\n\tp.LastPosted = now\n\tp.Posts.Set(ufmt.Sprintf(\"%s%d\", now.Format(time.RFC3339), p.Posts.Size()), \u0026Post{\n\t\tID: p.Posts.Size(),\n\t\tText: text,\n\t\tCreatedAt: now,\n\t})\n\treturn nil\n}\n\nfunc (p *Page) GetPosts() []*Post {\n\tposts := make([]*Post, p.Posts.Size())\n\ti := 0\n\tp.Posts.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpostParsed := value.(*Post)\n\t\tposts[i] = postParsed\n\t\ti++\n\t\treturn false\n\t})\n\treturn posts\n}\n\n// Post lists the specific update\ntype Post struct {\n\tID int\n\tCreatedAt time.Time\n\tText string\n}\n\nfunc (p *Post) String() string {\n\treturn \"\u003e \" + strings.ReplaceAll(p.Text, \"\\n\", \"\\n\u003e\\n\u003e\") + \"\\n\u003e\\n\u003e *\" + p.CreatedAt.Format(time.RFC1123) + \"*\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "nestedpkg", + "path": "gno.land/p/demo/nestedpkg", + "files": [ + { + "name": "nestedpkg.gno", + "body": "// Package nestedpkg provides helpers for package-path based access control.\n// It is useful for upgrade patterns relying on namespaces.\npackage nestedpkg\n\n// To test this from a realm and have std.CurrentRealm/PrevRealm work correctly,\n// this file is tested from gno.land/r/demo/tests/nestedpkg_test.gno\n// XXX: move test to ths directory once we support testing a package and\n// specifying values for both PrevRealm and CurrentRealm.\n\nimport (\n\t\"std\"\n\t\"strings\"\n)\n\n// IsCallerSubPath checks if the caller realm is located in a subfolder of the current realm.\nfunc IsCallerSubPath() bool {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(prev, cur)\n}\n\n// AssertCallerIsSubPath panics if IsCallerSubPath returns false.\nfunc AssertCallerIsSubPath() {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(prev, cur) {\n\t\tpanic(\"call restricted to nested packages. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// IsCallerParentPath checks if the caller realm is located in a parent location of the current realm.\nfunc IsCallerParentPath() bool {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(cur, prev)\n}\n\n// AssertCallerIsParentPath panics if IsCallerParentPath returns false.\nfunc AssertCallerIsParentPath() {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(cur, prev) {\n\t\tpanic(\"call restricted to parent packages. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// IsSameNamespace checks if the caller realm and the current realm are in the same namespace.\nfunc IsSameNamespace() bool {\n\tvar (\n\t\tcur = nsFromPath(std.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(std.PrevRealm().PkgPath()) + \"/\"\n\t)\n\treturn cur == prev\n}\n\n// AssertIsSameNamespace panics if IsSameNamespace returns false.\nfunc AssertIsSameNamespace() {\n\tvar (\n\t\tcur = nsFromPath(std.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(std.PrevRealm().PkgPath()) + \"/\"\n\t)\n\tif cur != prev {\n\t\tpanic(\"call restricted to packages from the same namespace. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// nsFromPath extracts the namespace from a package path.\nfunc nsFromPath(pkgpath string) string {\n\tparts := strings.Split(pkgpath, \"/\")\n\n\t// Specifically for gno.land, potential paths are in the form of DOMAIN/r/NAMESPACE/...\n\t// XXX: Consider extra checks.\n\t// XXX: Support non gno.land domains, where p/ and r/ won't be enforced.\n\tif len(parts) \u003e= 3 {\n\t\treturn parts[2]\n\t}\n\treturn \"\"\n}\n\n// XXX: Consider adding IsCallerDirectlySubPath\n// XXX: Consider adding IsCallerDirectlyParentPath\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "authorizable", + "path": "gno.land/p/demo/ownable/exts/authorizable", + "files": [ + { + "name": "authorizable.gno", + "body": "// Package authorizable is an extension of p/demo/ownable;\n// It allows the user to instantiate an Authorizable struct, which extends\n// p/demo/ownable with a list of users that are authorized for something.\n// By using authorizable, you have a superuser (ownable), as well as another\n// authorization level, which can be used for adding moderators or similar to your realm.\npackage authorizable\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Authorizable struct {\n\t*ownable.Ownable // owner in ownable is superuser\n\tauthorized *avl.Tree // std.Addr \u003e struct{}{}\n}\n\nfunc NewAuthorizable() *Authorizable {\n\ta := \u0026Authorizable{\n\t\townable.New(),\n\t\tavl.NewTree(),\n\t}\n\n\t// Add owner to auth list\n\ta.authorized.Set(a.Owner().String(), struct{}{})\n\treturn a\n}\n\nfunc NewAuthorizableWithAddress(addr std.Address) *Authorizable {\n\ta := \u0026Authorizable{\n\t\townable.NewWithAddress(addr),\n\t\tavl.NewTree(),\n\t}\n\n\t// Add owner to auth list\n\ta.authorized.Set(a.Owner().String(), struct{}{})\n\treturn a\n}\n\nfunc (a *Authorizable) AddToAuthList(addr std.Address) error {\n\tif err := a.CallerIsOwner(); err != nil {\n\t\treturn ErrNotSuperuser\n\t}\n\n\tif _, exists := a.authorized.Get(addr.String()); exists {\n\t\treturn ErrAlreadyInList\n\t}\n\n\ta.authorized.Set(addr.String(), struct{}{})\n\n\treturn nil\n}\n\nfunc (a *Authorizable) DeleteFromAuthList(addr std.Address) error {\n\tif err := a.CallerIsOwner(); err != nil {\n\t\treturn ErrNotSuperuser\n\t}\n\n\tif !a.authorized.Has(addr.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\n\tif _, removed := a.authorized.Remove(addr.String()); !removed {\n\t\tstr := ufmt.Sprintf(\"authorizable: could not remove %s from auth list\", addr.String())\n\t\tpanic(str)\n\t}\n\n\treturn nil\n}\n\nfunc (a Authorizable) CallerOnAuthList() error {\n\tcaller := std.PrevRealm().Addr()\n\n\tif !a.authorized.Has(caller.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\n\treturn nil\n}\n\nfunc (a Authorizable) AssertOnAuthList() {\n\tcaller := std.PrevRealm().Addr()\n\n\tif !a.authorized.Has(caller.String()) {\n\t\tpanic(ErrNotInAuthList)\n\t}\n}\n" + }, + { + "name": "authorizable_test.gno", + "body": "package authorizable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestNewAuthorizable(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // TODO(bug, issue #2371): should not be needed\n\n\ta := NewAuthorizable()\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestNewAuthorizableWithAddress(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestCallerOnAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.CallerOnAuthList(); err == ErrNotInAuthList {\n\t\tt.Fatalf(\"expected alice to be on the list\")\n\t}\n}\n\nfunc TestNotCallerOnAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\tif err := a.CallerOnAuthList(); err == nil {\n\t\tt.Fatalf(\"expected bob to not be on the list\")\n\t}\n}\n\nfunc TestAddToAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.AddToAuthList(bob); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\tif err := a.AddToAuthList(bob); err == nil {\n\t\tt.Fatalf(\"Expected AddToAuth to error while bob called it, but it didn't\")\n\t}\n}\n\nfunc TestDeleteFromList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.AddToAuthList(bob); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tif err := a.AddToAuthList(charlie); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\t// Try an unauthorized deletion\n\tif err := a.DeleteFromAuthList(alice); err == nil {\n\t\tt.Fatalf(\"Expected DelFromAuth to error with %v\", err)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.DeleteFromAuthList(charlie); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n}\n\nfunc TestAssertOnList(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\ta := NewAuthorizableWithAddress(alice)\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\tuassert.PanicsWithMessage(t, ErrNotInAuthList.Error(), func() {\n\t\ta.AssertOnAuthList()\n\t})\n}\n" + }, + { + "name": "errors.gno", + "body": "package authorizable\n\nimport \"errors\"\n\nvar (\n\tErrNotInAuthList = errors.New(\"authorizable: caller is not in authorized list\")\n\tErrNotSuperuser = errors.New(\"authorizable: caller is not superuser\")\n\tErrAlreadyInList = errors.New(\"authorizable: address is already in authorized list\")\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "pausable", + "path": "gno.land/p/demo/pausable", + "files": [ + { + "name": "pausable.gno", + "body": "package pausable\n\nimport \"gno.land/p/demo/ownable\"\n\ntype Pausable struct {\n\t*ownable.Ownable\n\tpaused bool\n}\n\n// New returns a new Pausable struct with non-paused state as default\nfunc New() *Pausable {\n\treturn \u0026Pausable{\n\t\tOwnable: ownable.New(),\n\t\tpaused: false,\n\t}\n}\n\n// NewFromOwnable is the same as New, but with a pre-existing top-level ownable\nfunc NewFromOwnable(ownable *ownable.Ownable) *Pausable {\n\treturn \u0026Pausable{\n\t\tOwnable: ownable,\n\t\tpaused: false,\n\t}\n}\n\n// IsPaused checks if Pausable is paused\nfunc (p Pausable) IsPaused() bool {\n\treturn p.paused\n}\n\n// Pause sets the state of Pausable to true, meaning all pausable functions are paused\nfunc (p *Pausable) Pause() error {\n\tif err := p.CallerIsOwner(); err != nil {\n\t\treturn err\n\t}\n\n\tp.paused = true\n\treturn nil\n}\n\n// Unpause sets the state of Pausable to false, meaning all pausable functions are resumed\nfunc (p *Pausable) Unpause() error {\n\tif err := p.CallerIsOwner(); err != nil {\n\t\treturn err\n\t}\n\n\tp.paused = false\n\treturn nil\n}\n" + }, + { + "name": "pausable_test.gno", + "body": "package pausable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tfirstCaller = std.Address(\"g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de\")\n\tsecondCaller = std.Address(\"g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa\")\n)\n\nfunc TestNew(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\n\turequire.False(t, result.paused, \"Expected result to be unpaused\")\n\turequire.Equal(t, firstCaller.String(), result.Owner().String())\n}\n\nfunc TestNewFromOwnable(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\to := ownable.New()\n\n\tstd.TestSetOrigCaller(secondCaller)\n\tresult := NewFromOwnable(o)\n\n\turequire.Equal(t, firstCaller.String(), result.Owner().String())\n}\n\nfunc TestSetUnpaused(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\tresult.Unpause()\n\n\turequire.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n}\n\nfunc TestSetPaused(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\tresult.Pause()\n\n\turequire.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n\nfunc TestIsPaused(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\turequire.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n\n\tresult.Pause()\n\turequire.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "releases", + "path": "gno.land/p/demo/releases", + "files": [ + { + "name": "changelog.gno", + "body": "package releases\n\ntype changelog struct {\n\tname string\n\treleases []release\n}\n\nfunc NewChangelog(name string) *changelog {\n\treturn \u0026changelog{\n\t\tname: name,\n\t\treleases: make([]release, 0),\n\t}\n}\n\nfunc (c *changelog) NewRelease(version, url, notes string) {\n\tif latest := c.Latest(); latest != nil {\n\t\tlatest.isLatest = false\n\t}\n\n\trelease := release{\n\t\t// manual\n\t\tversion: version,\n\t\turl: url,\n\t\tnotes: notes,\n\n\t\t// internal\n\t\tchangelog: c,\n\t\tisLatest: true,\n\t}\n\n\tc.releases = append(c.releases, release)\n}\n\nfunc (c *changelog) Render(path string) string {\n\tif path == \"\" {\n\t\toutput := \"# \" + c.name + \"\\n\\n\"\n\t\tmax := len(c.releases) - 1\n\t\tmin := 0\n\t\tif max-min \u003e 10 {\n\t\t\tmin = max - 10\n\t\t}\n\t\tfor i := max; i \u003e= min; i-- {\n\t\t\trelease := c.releases[i]\n\t\t\toutput += release.Render()\n\t\t}\n\t\treturn output\n\t}\n\n\trelease := c.ByVersion(path)\n\tif release != nil {\n\t\treturn release.Render()\n\t}\n\n\treturn \"no such release\"\n}\n\nfunc (c *changelog) Latest() *release {\n\tif len(c.releases) \u003e 0 {\n\t\tpos := len(c.releases) - 1\n\t\treturn \u0026c.releases[pos]\n\t}\n\treturn nil\n}\n\nfunc (c *changelog) ByVersion(version string) *release {\n\tfor _, release := range c.releases {\n\t\tif release.version == version {\n\t\t\treturn \u0026release\n\t\t}\n\t}\n\treturn nil\n}\n" + }, + { + "name": "release.gno", + "body": "package releases\n\ntype release struct {\n\t// manual\n\tversion string\n\turl string\n\tnotes string\n\n\t// internal\n\tisLatest bool\n\tchangelog *changelog\n}\n\nfunc (r *release) URL() string { return r.url }\nfunc (r *release) Version() string { return r.version }\nfunc (r *release) Notes() string { return r.notes }\nfunc (r *release) IsLatest() bool { return r.isLatest }\n\nfunc (r *release) Title() string {\n\toutput := r.changelog.name + \" \" + r.version\n\tif r.isLatest {\n\t\toutput += \" (latest)\"\n\t}\n\treturn output\n}\n\nfunc (r *release) Link() string {\n\treturn \"[\" + r.Title() + \"](\" + r.url + \")\"\n}\n\nfunc (r *release) Render() string {\n\toutput := \"\"\n\toutput += \"## \" + r.Link() + \"\\n\\n\"\n\tif r.notes != \"\" {\n\t\toutput += r.notes + \"\\n\\n\"\n\t}\n\treturn output\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "simpledao", + "path": "gno.land/p/demo/simpledao", + "files": [ + { + "name": "dao.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrInvalidExecutor = errors.New(\"invalid executor provided\")\n\tErrInsufficientProposalFunds = errors.New(\"insufficient funds for proposal\")\n\tErrInsufficientExecuteFunds = errors.New(\"insufficient funds for executing proposal\")\n\tErrProposalExecuted = errors.New(\"proposal already executed\")\n\tErrProposalInactive = errors.New(\"proposal is inactive\")\n\tErrProposalNotAccepted = errors.New(\"proposal is not accepted\")\n)\n\nvar (\n\tminProposalFeeValue int64 = 100 * 1_000_000 // minimum gnot required for a govdao proposal (100 GNOT)\n\tminExecuteFeeValue int64 = 500 * 1_000_000 // minimum gnot required for a govdao proposal (500 GNOT)\n\n\tminProposalFee = std.NewCoin(\"ugnot\", minProposalFeeValue)\n\tminExecuteFee = std.NewCoin(\"ugnot\", minExecuteFeeValue)\n)\n\n// SimpleDAO is a simple DAO implementation\ntype SimpleDAO struct {\n\tproposals *avl.Tree // seqid.ID -\u003e proposal\n\tmembStore membstore.MemberStore\n}\n\n// New creates a new instance of the simpledao DAO\nfunc New(membStore membstore.MemberStore) *SimpleDAO {\n\treturn \u0026SimpleDAO{\n\t\tproposals: avl.NewTree(),\n\t\tmembStore: membStore,\n\t}\n}\n\nfunc (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {\n\t// Make sure the executor is set\n\tif request.Executor == nil {\n\t\treturn 0, ErrInvalidExecutor\n\t}\n\n\tvar (\n\t\tcaller = getDAOCaller()\n\t\tsentCoins = std.GetOrigSend() // Get the sent coins, if any\n\t\tcanCoverFee = sentCoins.AmountOf(\"ugnot\") \u003e= minProposalFee.Amount\n\t)\n\n\t// Check if the proposal is valid\n\tif !s.membStore.IsMember(caller) \u0026\u0026 !canCoverFee {\n\t\treturn 0, ErrInsufficientProposalFunds\n\t}\n\n\t// Create the wrapped proposal\n\tprop := \u0026proposal{\n\t\tauthor: caller,\n\t\tdescription: request.Description,\n\t\texecutor: request.Executor,\n\t\tstatus: dao.Active,\n\t\ttally: newTally(),\n\t\tgetTotalVotingPowerFn: s.membStore.TotalPower,\n\t}\n\n\t// Add the proposal\n\tid, err := s.addProposal(prop)\n\tif err != nil {\n\t\treturn 0, ufmt.Errorf(\"unable to add proposal, %s\", err.Error())\n\t}\n\n\t// Emit the proposal added event\n\tdao.EmitProposalAdded(id, caller)\n\n\treturn id, nil\n}\n\nfunc (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error {\n\t// Verify the GOVDAO member\n\tcaller := getDAOCaller()\n\n\tmember, err := s.membStore.Member(caller)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get govdao member, %s\", err.Error())\n\t}\n\n\t// Check if the proposal exists\n\tpropRaw, err := s.ProposalByID(id)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get proposal %d, %s\", id, err.Error())\n\t}\n\n\tprop := propRaw.(*proposal)\n\n\t// Check the proposal status\n\tif prop.Status() == dao.ExecutionSuccessful ||\n\t\tprop.Status() == dao.ExecutionFailed {\n\t\t// Proposal was already executed, nothing to vote on anymore.\n\t\t//\n\t\t// In fact, the proposal should stop accepting\n\t\t// votes as soon as a 2/3+ majority is reached\n\t\t// on either option, but leaving the ability to vote still,\n\t\t// even if a proposal is accepted, or not accepted,\n\t\t// leaves room for \"principle\" vote decisions to be recorded\n\t\treturn ErrProposalInactive\n\t}\n\n\t// Cast the vote\n\tif err = prop.tally.castVote(member, option); err != nil {\n\t\treturn ufmt.Errorf(\"unable to vote on proposal %d, %s\", id, err.Error())\n\t}\n\n\t// Emit the vote cast event\n\tdao.EmitVoteAdded(id, caller, option)\n\n\t// Check the votes to see if quorum is reached\n\tvar (\n\t\ttotalPower = s.membStore.TotalPower()\n\t\tmajorityPower = (2 * totalPower) / 3\n\t)\n\n\tacceptProposal := func() {\n\t\tprop.status = dao.Accepted\n\n\t\tdao.EmitProposalAccepted(id)\n\t}\n\n\tdeclineProposal := func() {\n\t\tprop.status = dao.NotAccepted\n\n\t\tdao.EmitProposalNotAccepted(id)\n\t}\n\n\tswitch {\n\tcase prop.tally.yays \u003e majorityPower:\n\t\t// 2/3+ voted YES\n\t\tacceptProposal()\n\tcase prop.tally.nays \u003e majorityPower:\n\t\t// 2/3+ voted NO\n\t\tdeclineProposal()\n\tcase prop.tally.abstains \u003e majorityPower:\n\t\t// 2/3+ voted ABSTAIN\n\t\tdeclineProposal()\n\tcase prop.tally.yays+prop.tally.nays+prop.tally.abstains \u003e= totalPower:\n\t\t// Everyone voted, but it's undecided,\n\t\t// hence the proposal can't go through\n\t\tdeclineProposal()\n\tdefault:\n\t\t// Quorum not reached\n\t}\n\n\treturn nil\n}\n\nfunc (s *SimpleDAO) ExecuteProposal(id uint64) error {\n\tvar (\n\t\tcaller = getDAOCaller()\n\t\tsentCoins = std.GetOrigSend() // Get the sent coins, if any\n\t\tcanCoverFee = sentCoins.AmountOf(\"ugnot\") \u003e= minExecuteFee.Amount\n\t)\n\n\t// Check if the non-DAO member can cover the execute fee\n\tif !s.membStore.IsMember(caller) \u0026\u0026 !canCoverFee {\n\t\treturn ErrInsufficientExecuteFunds\n\t}\n\n\t// Check if the proposal exists\n\tpropRaw, err := s.ProposalByID(id)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get proposal %d, %s\", id, err.Error())\n\t}\n\n\tprop := propRaw.(*proposal)\n\n\t// Check if the proposal is executed\n\tif prop.Status() == dao.ExecutionSuccessful ||\n\t\tprop.Status() == dao.ExecutionFailed {\n\t\t// Proposal is already executed\n\t\treturn ErrProposalExecuted\n\t}\n\n\t// Check the proposal status\n\tif prop.Status() != dao.Accepted {\n\t\t// Proposal is not accepted, cannot be executed\n\t\treturn ErrProposalNotAccepted\n\t}\n\n\t// Emit an event when the execution finishes\n\tdefer dao.EmitProposalExecuted(id, prop.status)\n\n\t// Attempt to execute the proposal\n\tif err = prop.executor.Execute(); err != nil {\n\t\tprop.status = dao.ExecutionFailed\n\n\t\treturn ufmt.Errorf(\"error during proposal %d execution, %s\", id, err.Error())\n\t}\n\n\t// Update the proposal status\n\tprop.status = dao.ExecutionSuccessful\n\n\treturn nil\n}\n\n// getDAOCaller returns the DAO caller.\n// XXX: This is not a great way to determine the caller, and it is very unsafe.\n// However, the current MsgRun context does not persist escaping the main() scope.\n// Until a better solution is developed, this enables proposals to be made through a package deployment + init()\nfunc getDAOCaller() std.Address {\n\treturn std.GetOrigCaller()\n}\n" + }, + { + "name": "dao_test.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateMembers generates dummy govdao members\nfunc generateMembers(t *testing.T, count int) []membstore.Member {\n\tt.Helper()\n\n\tmembers := make([]membstore.Member, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tmembers = append(members, membstore.Member{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"member %d\", i)),\n\t\t\tVotingPower: 10,\n\t\t})\n\t}\n\n\treturn members\n}\n\nfunc TestSimpleDAO_Propose(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"invalid executor\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\t_, err := s.Propose(dao.ProposalRequest{})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInvalidExecutor,\n\t\t)\n\t})\n\n\tt.Run(\"caller cannot cover fee\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue-1,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be lower\n\t\t// than the proposal fee\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\n\t\t_, err := s.Propose(dao.ProposalRequest{\n\t\t\tExecutor: ex,\n\t\t})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInsufficientProposalFunds,\n\t\t)\n\n\t\tuassert.False(t, called)\n\t})\n\n\tt.Run(\"proposal added\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\t\t\tdescription = \"Proposal description\"\n\n\t\t\tproposer = testutils.TestAddress(\"proposer\")\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue, // enough to cover\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(addr std.Address) bool {\n\t\t\t\t\treturn addr == proposer\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be enough\n\t\t// to cover the fee\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\t\tstd.TestSetOrigCaller(proposer)\n\n\t\t// Make sure the proposal was added\n\t\tid, err := s.Propose(dao.ProposalRequest{\n\t\t\tDescription: description,\n\t\t\tExecutor: ex,\n\t\t})\n\t\tuassert.NoError(t, err)\n\t\tuassert.False(t, called)\n\n\t\t// Make sure the proposal exists\n\t\tprop, err := s.ProposalByID(id)\n\t\tuassert.NoError(t, err)\n\n\t\tuassert.Equal(t, proposer.String(), prop.Author().String())\n\t\tuassert.Equal(t, description, prop.Description())\n\t\tuassert.Equal(t, dao.Active.String(), prop.Status().String())\n\n\t\tstats := prop.Stats()\n\n\t\tuassert.Equal(t, uint64(0), stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, uint64(0), stats.TotalVotingPower)\n\t})\n}\n\nfunc TestSimpleDAO_VoteOnProposal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"not govdao member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tfetchErr = errors.New(\"fetch error\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(_ std.Address) (membstore.Member, error) {\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, fetchErr\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(0, dao.YesVote),\n\t\t\tfetchErr.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(0, dao.YesVote),\n\t\t\tErrMissingProposal.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"proposal executed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.ExecutionSuccessful,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\tErrProposalInactive,\n\t\t)\n\t})\n\n\tt.Run(\"double vote on proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tmember = membstore.Member{\n\t\t\t\tAddress: voter,\n\t\t\t\tVotingPower: 10,\n\t\t\t}\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn member, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Cast the initial vote\n\t\turequire.NoError(t, prop.tally.castVote(member, dao.YesVote))\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\tErrAlreadyVoted.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"majority accepted\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was accepted\n\t\tuassert.Equal(t, dao.Accepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"majority rejected\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was not accepted\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"majority abstained\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.AbstainVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was not accepted\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"everyone voted, undecided\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// The first half votes yes\n\t\tfor _, m := range members[:len(members)/2] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// The other half votes no\n\t\tfor _, m := range members[len(members)/2:] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal is not active,\n\t\t// since everyone voted, and it was undecided\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"proposal undecided\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// The first quarter votes yes\n\t\tfor _, m := range members[:len(members)/4] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// The second quarter votes no\n\t\tfor _, m := range members[len(members)/4 : len(members)/2] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal is still active,\n\t\t// since there wasn't quorum reached on any decision\n\t\tuassert.Equal(t, dao.Active.String(), prop.status.String())\n\t})\n}\n\nfunc TestSimpleDAO_ExecuteProposal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller cannot cover fee\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminExecuteFeeValue-1,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be lower\n\t\t// than the execute fee\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(0),\n\t\t\tErrInsufficientExecuteFunds,\n\t\t)\n\t})\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminExecuteFeeValue,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be enough\n\t\t// so the execution can take place\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(0),\n\t\t\tErrMissingProposal.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"proposal not accepted\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.NotAccepted,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(id),\n\t\t\tErrProposalNotAccepted,\n\t\t)\n\t})\n\n\tt.Run(\"proposal already executed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestTable := []struct {\n\t\t\tname string\n\t\t\tstatus dao.ProposalStatus\n\t\t}{\n\t\t\t{\n\t\t\t\t\"execution was successful\",\n\t\t\t\tdao.ExecutionSuccessful,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"execution failed\",\n\t\t\t\tdao.ExecutionFailed,\n\t\t\t},\n\t\t}\n\n\t\tfor _, testCase := range testTable {\n\t\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tvar (\n\t\t\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\t\t\tms = \u0026mockMemberStore{\n\t\t\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ts = New(ms)\n\n\t\t\t\t\tprop = \u0026proposal{\n\t\t\t\t\t\tstatus: testCase.status,\n\t\t\t\t\t}\n\t\t\t\t)\n\n\t\t\t\tstd.TestSetOrigCaller(voter)\n\n\t\t\t\t// Add an initial proposal\n\t\t\t\tid, err := s.addProposal(prop)\n\t\t\t\turequire.NoError(t, err)\n\n\t\t\t\t// Attempt to vote on the proposal\n\t\t\t\tuassert.ErrorIs(\n\t\t\t\t\tt,\n\t\t\t\t\ts.ExecuteProposal(id),\n\t\t\t\t\tErrProposalExecuted,\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"execution error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\n\t\t\texecError = errors.New(\"exec error\")\n\n\t\t\tmockExecutor = \u0026mockExecutor{\n\t\t\t\texecuteFn: func() error {\n\t\t\t\t\treturn execError\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Accepted,\n\t\t\t\texecutor: mockExecutor,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(id),\n\t\t\texecError.Error(),\n\t\t)\n\n\t\tuassert.Equal(t, dao.ExecutionFailed.String(), prop.status.String())\n\t})\n\n\tt.Run(\"successful execution\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tcalled = false\n\t\t\tmockExecutor = \u0026mockExecutor{\n\t\t\t\texecuteFn: func() error {\n\t\t\t\t\tcalled = true\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Accepted,\n\t\t\t\texecutor: mockExecutor,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.NoError(t, s.ExecuteProposal(id))\n\t\tuassert.Equal(t, dao.ExecutionSuccessful.String(), prop.status.String())\n\t\tuassert.True(t, called)\n\t})\n}\n" + }, + { + "name": "mock_test.gno", + "body": "package simpledao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/membstore\"\n)\n\ntype executeDelegate func() error\n\ntype mockExecutor struct {\n\texecuteFn executeDelegate\n}\n\nfunc (m *mockExecutor) Execute() error {\n\tif m.executeFn != nil {\n\t\treturn m.executeFn()\n\t}\n\n\treturn nil\n}\n\ntype (\n\tmembersDelegate func(uint64, uint64) []membstore.Member\n\tsizeDelegate func() int\n\tisMemberDelegate func(std.Address) bool\n\ttotalPowerDelegate func() uint64\n\tmemberDelegate func(std.Address) (membstore.Member, error)\n\taddMemberDelegate func(membstore.Member) error\n\tupdateMemberDelegate func(std.Address, membstore.Member) error\n)\n\ntype mockMemberStore struct {\n\tmembersFn membersDelegate\n\tsizeFn sizeDelegate\n\tisMemberFn isMemberDelegate\n\ttotalPowerFn totalPowerDelegate\n\tmemberFn memberDelegate\n\taddMemberFn addMemberDelegate\n\tupdateMemberFn updateMemberDelegate\n}\n\nfunc (m *mockMemberStore) Members(offset, count uint64) []membstore.Member {\n\tif m.membersFn != nil {\n\t\treturn m.membersFn(offset, count)\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockMemberStore) Size() int {\n\tif m.sizeFn != nil {\n\t\treturn m.sizeFn()\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockMemberStore) IsMember(address std.Address) bool {\n\tif m.isMemberFn != nil {\n\t\treturn m.isMemberFn(address)\n\t}\n\n\treturn false\n}\n\nfunc (m *mockMemberStore) TotalPower() uint64 {\n\tif m.totalPowerFn != nil {\n\t\treturn m.totalPowerFn()\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockMemberStore) Member(address std.Address) (membstore.Member, error) {\n\tif m.memberFn != nil {\n\t\treturn m.memberFn(address)\n\t}\n\n\treturn membstore.Member{}, nil\n}\n\nfunc (m *mockMemberStore) AddMember(member membstore.Member) error {\n\tif m.addMemberFn != nil {\n\t\treturn m.addMemberFn(member)\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockMemberStore) UpdateMember(address std.Address, member membstore.Member) error {\n\tif m.updateMemberFn != nil {\n\t\treturn m.updateMemberFn(address, member)\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "propstore.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar ErrMissingProposal = errors.New(\"proposal is missing\")\n\n// maxRequestProposals is the maximum number of\n// paginated proposals that can be requested\nconst maxRequestProposals = 10\n\n// proposal is the internal simpledao proposal implementation\ntype proposal struct {\n\tauthor std.Address // initiator of the proposal\n\tdescription string // description of the proposal\n\n\texecutor dao.Executor // executor for the proposal\n\tstatus dao.ProposalStatus // status of the proposal\n\n\ttally *tally // voting tally\n\tgetTotalVotingPowerFn func() uint64 // callback for the total voting power\n}\n\nfunc (p *proposal) Author() std.Address {\n\treturn p.author\n}\n\nfunc (p *proposal) Description() string {\n\treturn p.description\n}\n\nfunc (p *proposal) Status() dao.ProposalStatus {\n\treturn p.status\n}\n\nfunc (p *proposal) Executor() dao.Executor {\n\treturn p.executor\n}\n\nfunc (p *proposal) Stats() dao.Stats {\n\t// Get the total voting power of the body\n\ttotalPower := p.getTotalVotingPowerFn()\n\n\treturn dao.Stats{\n\t\tYayVotes: p.tally.yays,\n\t\tNayVotes: p.tally.nays,\n\t\tAbstainVotes: p.tally.abstains,\n\t\tTotalVotingPower: totalPower,\n\t}\n}\n\nfunc (p *proposal) IsExpired() bool {\n\treturn false // this proposal never expires\n}\n\nfunc (p *proposal) Render() string {\n\t// Fetch the voting stats\n\tstats := p.Stats()\n\n\toutput := \"\"\n\toutput += ufmt.Sprintf(\"Author: %s\", p.Author().String())\n\toutput += \"\\n\\n\"\n\toutput += p.Description()\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Status: %s\", p.Status().String())\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\n\t\t\"Voting stats: YES %d (%d%%), NO %d (%d%%), ABSTAIN %d (%d%%), MISSING VOTE %d (%d%%)\",\n\t\tstats.YayVotes,\n\t\tstats.YayPercent(),\n\t\tstats.NayVotes,\n\t\tstats.NayPercent(),\n\t\tstats.AbstainVotes,\n\t\tstats.AbstainPercent(),\n\t\tstats.MissingVotes(),\n\t\tstats.MissingVotesPercent(),\n\t)\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Threshold met: %t\", stats.YayVotes \u003e (2*stats.TotalVotingPower)/3)\n\n\treturn output\n}\n\n// addProposal adds a new simpledao proposal to the store\nfunc (s *SimpleDAO) addProposal(proposal *proposal) (uint64, error) {\n\t// See what the next proposal number should be\n\tnextID := uint64(s.proposals.Size())\n\n\t// Save the proposal\n\ts.proposals.Set(getProposalID(nextID), proposal)\n\n\treturn nextID, nil\n}\n\nfunc (s *SimpleDAO) Proposals(offset, count uint64) []dao.Proposal {\n\t// Check the requested count\n\tif count \u003c 1 {\n\t\treturn []dao.Proposal{}\n\t}\n\n\t// Limit the maximum number of returned proposals\n\tif count \u003e maxRequestProposals {\n\t\tcount = maxRequestProposals\n\t}\n\n\tvar (\n\t\tstartIndex = offset\n\t\tendIndex = startIndex + count\n\n\t\tnumProposals = uint64(s.proposals.Size())\n\t)\n\n\t// Check if the current offset has any proposals\n\tif startIndex \u003e= numProposals {\n\t\treturn []dao.Proposal{}\n\t}\n\n\t// Check if the right bound is good\n\tif endIndex \u003e numProposals {\n\t\tendIndex = numProposals\n\t}\n\n\tprops := make([]dao.Proposal, 0)\n\ts.proposals.Iterate(\n\t\tgetProposalID(startIndex),\n\t\tgetProposalID(endIndex),\n\t\tfunc(_ string, val interface{}) bool {\n\t\t\tprop := val.(*proposal)\n\n\t\t\t// Save the proposal\n\t\t\tprops = append(props, prop)\n\n\t\t\treturn false\n\t\t},\n\t)\n\n\treturn props\n}\n\nfunc (s *SimpleDAO) ProposalByID(id uint64) (dao.Proposal, error) {\n\tprop, exists := s.proposals.Get(getProposalID(id))\n\tif !exists {\n\t\treturn nil, ErrMissingProposal\n\t}\n\n\treturn prop.(*proposal), nil\n}\n\nfunc (s *SimpleDAO) Size() int {\n\treturn s.proposals.Size()\n}\n\n// getProposalID generates a sequential proposal ID\n// from the given ID number\nfunc getProposalID(id uint64) string {\n\treturn seqid.ID(id).String()\n}\n" + }, + { + "name": "propstore_test.gno", + "body": "package simpledao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateProposals generates dummy proposals\nfunc generateProposals(t *testing.T, count int) []*proposal {\n\tt.Helper()\n\n\tvar (\n\t\tmembers = generateMembers(t, count)\n\t\tproposals = make([]*proposal, 0, count)\n\t)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tproposal := \u0026proposal{\n\t\t\tauthor: members[i].Address,\n\t\t\tdescription: ufmt.Sprintf(\"proposal %d\", i),\n\t\t\tstatus: dao.Active,\n\t\t\ttally: newTally(),\n\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t\texecutor: nil,\n\t\t}\n\n\t\tproposals = append(proposals, proposal)\n\t}\n\n\treturn proposals\n}\n\nfunc equalProposals(t *testing.T, p1, p2 dao.Proposal) {\n\tt.Helper()\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Author().String(),\n\t\tp2.Author().String(),\n\t)\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Description(),\n\t\tp2.Description(),\n\t)\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Status().String(),\n\t\tp2.Status().String(),\n\t)\n\n\tp1Stats := p1.Stats()\n\tp2Stats := p2.Stats()\n\n\tuassert.Equal(t, p1Stats.YayVotes, p2Stats.YayVotes)\n\tuassert.Equal(t, p1Stats.NayVotes, p2Stats.NayVotes)\n\tuassert.Equal(t, p1Stats.AbstainVotes, p2Stats.AbstainVotes)\n\tuassert.Equal(t, p1Stats.TotalVotingPower, p2Stats.TotalVotingPower)\n}\n\nfunc TestProposal_Data(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"author\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tauthor: testutils.TestAddress(\"address\"),\n\t\t}\n\n\t\tuassert.Equal(t, p.author, p.Author())\n\t})\n\n\tt.Run(\"description\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tdescription: \"example proposal description\",\n\t\t}\n\n\t\tuassert.Equal(t, p.description, p.Description())\n\t})\n\n\tt.Run(\"status\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tstatus: dao.ExecutionSuccessful,\n\t\t}\n\n\t\tuassert.Equal(t, p.status.String(), p.Status().String())\n\t})\n\n\tt.Run(\"executor\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumCalled = 0\n\t\t\tcb = func() error {\n\t\t\t\tnumCalled++\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\n\t\t\tp = \u0026proposal{\n\t\t\t\texecutor: ex,\n\t\t\t}\n\t\t)\n\n\t\turequire.NoError(t, p.executor.Execute())\n\t\turequire.NoError(t, p.Executor().Execute())\n\n\t\tuassert.Equal(t, 2, numCalled)\n\t})\n\n\tt.Run(\"no votes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\ttally: newTally(),\n\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t}\n\n\t\tstats := p.Stats()\n\n\t\tuassert.Equal(t, uint64(0), stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, uint64(0), stats.TotalVotingPower)\n\t})\n\n\tt.Run(\"existing votes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\t\t\ttotalPower = uint64(len(members)) * 10\n\n\t\t\tp = \u0026proposal{\n\t\t\t\ttally: newTally(),\n\t\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\t\treturn totalPower\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\tfor _, m := range members {\n\t\t\turequire.NoError(t, p.tally.castVote(m, dao.YesVote))\n\t\t}\n\n\t\tstats := p.Stats()\n\n\t\tuassert.Equal(t, totalPower, stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, totalPower, stats.TotalVotingPower)\n\t})\n}\n\nfunc TestSimpleDAO_GetProposals(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no proposals\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\tuassert.Equal(t, 0, s.Size())\n\t\tproposals := s.Proposals(0, 0)\n\n\t\tuassert.Equal(t, 0, len(proposals))\n\t})\n\n\tt.Run(\"proper pagination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumProposals = 20\n\t\t\thalfRange = numProposals / 2\n\n\t\t\ts = New(nil)\n\t\t\tproposals = generateProposals(t, numProposals)\n\t\t)\n\n\t\t// Add initial proposals\n\t\tfor _, proposal := range proposals {\n\t\t\t_, err := s.addProposal(proposal)\n\n\t\t\turequire.NoError(t, err)\n\t\t}\n\n\t\tuassert.Equal(t, numProposals, s.Size())\n\n\t\tfetchedProposals := s.Proposals(0, uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedProposals))\n\n\t\tfor index, fetchedProposal := range fetchedProposals {\n\t\t\tequalProposals(t, proposals[index], fetchedProposal)\n\t\t}\n\n\t\t// Fetch the other half\n\t\tfetchedProposals = s.Proposals(uint64(halfRange), uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedProposals))\n\n\t\tfor index, fetchedProposal := range fetchedProposals {\n\t\t\tequalProposals(t, proposals[index+halfRange], fetchedProposal)\n\t\t}\n\t})\n}\n\nfunc TestSimpleDAO_GetProposalByID(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\t_, err := s.ProposalByID(0)\n\t\tuassert.ErrorIs(t, err, ErrMissingProposal)\n\t})\n\n\tt.Run(\"proposal found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\ts = New(nil)\n\t\t\tproposal = generateProposals(t, 1)[0]\n\t\t)\n\n\t\t// Add the initial proposal\n\t\t_, err := s.addProposal(proposal)\n\t\turequire.NoError(t, err)\n\n\t\t// Fetch the proposal\n\t\tfetchedProposal, err := s.ProposalByID(0)\n\t\turequire.NoError(t, err)\n\n\t\tequalProposals(t, proposal, fetchedProposal)\n\t})\n}\n" + }, + { + "name": "votestore.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\nvar ErrAlreadyVoted = errors.New(\"vote already cast\")\n\n// tally is a simple vote tally system\ntype tally struct {\n\t// tally cache to keep track of active\n\t// yes / no / abstain votes\n\tyays uint64\n\tnays uint64\n\tabstains uint64\n\n\tvoters *avl.Tree // std.Address -\u003e dao.VoteOption\n}\n\n// newTally creates a new tally system instance\nfunc newTally() *tally {\n\treturn \u0026tally{\n\t\tvoters: avl.NewTree(),\n\t}\n}\n\n// castVote casts a single vote in the name of the given member\nfunc (t *tally) castVote(member membstore.Member, option dao.VoteOption) error {\n\t// Check if the member voted already\n\taddress := member.Address.String()\n\n\t_, voted := t.voters.Get(address)\n\tif voted {\n\t\treturn ErrAlreadyVoted\n\t}\n\n\t// convert option to upper-case, like the constants are.\n\toption = dao.VoteOption(strings.ToUpper(string(option)))\n\n\t// Update the tally\n\tswitch option {\n\tcase dao.YesVote:\n\t\tt.yays += member.VotingPower\n\tcase dao.AbstainVote:\n\t\tt.abstains += member.VotingPower\n\tcase dao.NoVote:\n\t\tt.nays += member.VotingPower\n\tdefault:\n\t\tpanic(\"invalid voting option: \" + option)\n\t}\n\n\t// Save the voting status\n\tt.voters.Set(address, option)\n\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "stack", + "path": "gno.land/p/demo/stack", + "files": [ + { + "name": "stack.gno", + "body": "package stack\n\ntype Stack struct {\n\ttop *node\n\tlength int\n}\n\ntype node struct {\n\tvalue interface{}\n\tprev *node\n}\n\nfunc New() *Stack {\n\treturn \u0026Stack{nil, 0}\n}\n\nfunc (s *Stack) Len() int {\n\treturn s.length\n}\n\nfunc (s *Stack) Top() interface{} {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\treturn s.top.value\n}\n\nfunc (s *Stack) Pop() interface{} {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\n\tnode := s.top\n\ts.top = node.prev\n\ts.length -= 1\n\treturn node.value\n}\n\nfunc (s *Stack) Push(value interface{}) {\n\tnode := \u0026node{value, s.top}\n\ts.top = node\n\ts.length += 1\n}\n" + }, + { + "name": "stack_test.gno", + "body": "package stack\n\nimport \"testing\"\n\nfunc TestStack(t *testing.T) {\n\ts := New() // Empty stack\n\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n\n\ts.Push(1)\n\n\tif s.Len() != 1 {\n\t\tt.Errorf(\"s.Len(): expected 1; got %d\", s.Len())\n\t}\n\n\tif top := s.Top(); top.(int) != 1 {\n\t\tt.Errorf(\"s.Top(): expected 1; got %v\", top.(int))\n\t}\n\n\tif elem := s.Pop(); elem.(int) != 1 {\n\t\tt.Errorf(\"s.Pop(): expected 1; got %v\", elem.(int))\n\t}\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "subscription", + "path": "gno.land/p/demo/subscription", + "files": [ + { + "name": "doc.gno", + "body": "// Package subscription provides a flexible system for managing both recurring and\n// lifetime subscriptions in Gno applications. It enables developers to handle\n// payment-based access control for services or products. The library supports\n// both subscriptions requiring periodic payments (recurring) and one-time payments\n// (lifetime). Subscriptions are tracked using an AVL tree for efficient management\n// of subscription statuses.\n//\n// Usage:\n//\n// Import the required sub-packages (`recurring` and/or `lifetime`) to manage specific\n// subscription types. The methods provided allow users to subscribe, check subscription\n// status, and manage payments.\n//\n// Recurring Subscription:\n//\n// Recurring subscriptions require periodic payments to maintain access.\n// Users pay to extend their access for a specific duration.\n//\n// Example:\n//\n//\t// Create a recurring subscription requiring 100 ugnot every 30 days\n//\trecSub := recurring.NewRecurringSubscription(time.Hour * 24 * 30, 100)\n//\n//\t// Process payment for the recurring subscription\n//\trecSub.Subscribe()\n//\n//\t// Gift a recurring subscription to another user\n//\trecSub.GiftSubscription(recipientAddress)\n//\n//\t// Check if a user has a valid subscription\n//\trecSub.HasValidSubscription(addr)\n//\n//\t// Get the expiration date of the subscription\n//\trecSub.GetExpiration(caller)\n//\n//\t// Update the subscription amount to 200 ugnot\n//\trecSub.UpdateAmount(200)\n//\n//\t// Get the current subscription amount\n//\trecSub.GetAmount()\n//\n// Lifetime Subscription:\n//\n// Lifetime subscriptions require a one-time payment for permanent access.\n// Once paid, users have indefinite access without further payments.\n//\n// Example:\n//\n//\t// Create a lifetime subscription costing 500 ugnot\n//\tlifeSub := lifetime.NewLifetimeSubscription(500)\n//\n//\t// Process payment for lifetime access\n//\tlifeSub.Subscribe()\n//\n//\t// Gift a lifetime subscription to another user\n//\tlifeSub.GiftSubscription(recipientAddress)\n//\n//\t// Check if a user has a valid subscription\n//\tlifeSub.HasValidSubscription(addr)\n//\n//\t// Update the lifetime subscription amount to 1000 ugnot\n//\tlifeSub.UpdateAmount(1000)\n//\n//\t// Get the current lifetime subscription amount\n//\tlifeSub.GetAmount()\npackage subscription\n" + }, + { + "name": "subscription.gno", + "body": "package subscription\n\nimport (\n\t\"std\"\n)\n\n// Subscription interface defines standard methods that all subscription types must implement.\ntype Subscription interface {\n\tHasValidSubscription(std.Address) error\n\tSubscribe() error\n\tUpdateAmount(newAmount int64) error\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "lifetime", + "path": "gno.land/p/demo/subscription/lifetime", + "files": [ + { + "name": "errors.gno", + "body": "package lifetime\n\nimport \"errors\"\n\nvar (\n\tErrNoSub = errors.New(\"lifetime subscription: no active subscription found\")\n\tErrAmt = errors.New(\"lifetime subscription: payment amount does not match the required subscription amount\")\n\tErrAlreadySub = errors.New(\"lifetime subscription: this address already has an active lifetime subscription\")\n\tErrNotAuthorized = errors.New(\"lifetime subscription: action not authorized\")\n)\n" + }, + { + "name": "lifetime.gno", + "body": "package lifetime\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\n// LifetimeSubscription represents a subscription that requires only a one-time payment.\n// It grants permanent access to a service or product.\ntype LifetimeSubscription struct {\n\townable.Ownable\n\tamount int64\n\tsubs *avl.Tree // std.Address -\u003e bool\n}\n\n// NewLifetimeSubscription creates and returns a new lifetime subscription.\nfunc NewLifetimeSubscription(amount int64) *LifetimeSubscription {\n\treturn \u0026LifetimeSubscription{\n\t\tOwnable: *ownable.New(),\n\t\tamount: amount,\n\t\tsubs: avl.NewTree(),\n\t}\n}\n\n// processSubscription handles the subscription process for a given receiver.\nfunc (ls *LifetimeSubscription) processSubscription(receiver std.Address) error {\n\tamount := std.GetOrigSend()\n\n\tif amount.AmountOf(\"ugnot\") != ls.amount {\n\t\treturn ErrAmt\n\t}\n\n\t_, exists := ls.subs.Get(receiver.String())\n\n\tif exists {\n\t\treturn ErrAlreadySub\n\t}\n\n\tls.subs.Set(receiver.String(), true)\n\n\treturn nil\n}\n\n// Subscribe processes the payment for a lifetime subscription.\nfunc (ls *LifetimeSubscription) Subscribe() error {\n\tcaller := std.PrevRealm().Addr()\n\treturn ls.processSubscription(caller)\n}\n\n// GiftSubscription allows the caller to pay for a lifetime subscription for another user.\nfunc (ls *LifetimeSubscription) GiftSubscription(receiver std.Address) error {\n\treturn ls.processSubscription(receiver)\n}\n\n// HasValidSubscription checks if the given address has an active lifetime subscription.\nfunc (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error {\n\t_, exists := ls.subs.Get(addr.String())\n\n\tif !exists {\n\t\treturn ErrNoSub\n\t}\n\n\treturn nil\n}\n\n// UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price.\nfunc (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error {\n\tif err := ls.CallerIsOwner(); err != nil {\n\t\treturn ErrNotAuthorized\n\t}\n\n\tls.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current subscription price.\nfunc (ls *LifetimeSubscription) GetAmount() int64 {\n\treturn ls.amount\n}\n" + }, + { + "name": "lifetime_test.gno", + "body": "package lifetime\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestLifetimeSubscription(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed\")\n\n\terr = ls.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n}\n\nfunc TestLifetimeSubscriptionGift(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := ls.GiftSubscription(bob)\n\tuassert.NoError(t, err, \"Expected ProcessPaymentGift to succeed for Bob\")\n\n\terr = ls.HasValidSubscription(bob)\n\tuassert.NoError(t, err, \"Expected Bob to have access\")\n\n\terr = ls.HasValidSubscription(charlie)\n\tuassert.Error(t, err, \"Expected Charlie to fail access check\")\n}\n\nfunc TestUpdateAmountAuthorization(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\terr := ls.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tstd.TestSetOrigCaller(bob)\n\n\terr = ls.UpdateAmount(3000)\n\tuassert.Error(t, err, \"Expected Bob to fail when updating amount\")\n}\n\nfunc TestIncorrectPaymentAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}}, nil)\n\terr := ls.Subscribe()\n\tuassert.Error(t, err, \"Expected payment to fail with incorrect amount\")\n}\n\nfunc TestMultipleSubscriptionAttempts(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected first subscription to succeed\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = ls.Subscribe()\n\tuassert.Error(t, err, \"Expected second subscription to fail as Alice is already subscribed\")\n}\n\nfunc TestGiftSubscriptionWithIncorrectAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}}, nil)\n\terr := ls.GiftSubscription(bob)\n\tuassert.Error(t, err, \"Expected gift subscription to fail with incorrect amount\")\n\n\terr = ls.HasValidSubscription(bob)\n\tuassert.Error(t, err, \"Expected Bob to not have access after incorrect gift subscription\")\n}\n\nfunc TestUpdateAmountEffectiveness(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\terr := ls.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = ls.Subscribe()\n\tuassert.Error(t, err, \"Expected subscription to fail with old amount after update\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 2000}}, nil)\n\terr = ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected subscription to succeed with new amount\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "recurring", + "path": "gno.land/p/demo/subscription/recurring", + "files": [ + { + "name": "errors.gno", + "body": "package recurring\n\nimport \"errors\"\n\nvar (\n\tErrNoSub = errors.New(\"recurring subscription: no active subscription found\")\n\tErrSubExpired = errors.New(\"recurring subscription: your subscription has expired\")\n\tErrAmt = errors.New(\"recurring subscription: payment amount does not match the required subscription amount\")\n\tErrAlreadySub = errors.New(\"recurring subscription: this address already has an active subscription\")\n\tErrNotAuthorized = errors.New(\"recurring subscription: action not authorized\")\n)\n" + }, + { + "name": "recurring.gno", + "body": "package recurring\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\n// RecurringSubscription represents a subscription that requires periodic payments.\n// It includes the duration of the subscription and the amount required per period.\ntype RecurringSubscription struct {\n\townable.Ownable\n\tduration time.Duration\n\tamount int64\n\tsubs *avl.Tree // std.Address -\u003e time.Time\n}\n\n// NewRecurringSubscription creates and returns a new recurring subscription.\nfunc NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription {\n\treturn \u0026RecurringSubscription{\n\t\tOwnable: *ownable.New(),\n\t\tduration: duration,\n\t\tamount: amount,\n\t\tsubs: avl.NewTree(),\n\t}\n}\n\n// HasValidSubscription verifies if the caller has an active recurring subscription.\nfunc (rs *RecurringSubscription) HasValidSubscription(addr std.Address) error {\n\texpTime, exists := rs.subs.Get(addr.String())\n\tif !exists {\n\t\treturn ErrNoSub\n\t}\n\n\tif time.Now().After(expTime.(time.Time)) {\n\t\treturn ErrSubExpired\n\t}\n\n\treturn nil\n}\n\n// processSubscription processes the payment for a given receiver and renews or adds their subscription.\nfunc (rs *RecurringSubscription) processSubscription(receiver std.Address) error {\n\tamount := std.GetOrigSend()\n\n\tif amount.AmountOf(\"ugnot\") != rs.amount {\n\t\treturn ErrAmt\n\t}\n\n\texpTime, exists := rs.subs.Get(receiver.String())\n\n\t// If the user is already a subscriber but his subscription has expired, authorize renewal\n\tif exists {\n\t\texpiration := expTime.(time.Time)\n\t\tif time.Now().Before(expiration) {\n\t\t\treturn ErrAlreadySub\n\t\t}\n\t}\n\n\t// Renew or add subscription\n\tnewExpiration := time.Now().Add(rs.duration)\n\trs.subs.Set(receiver.String(), newExpiration)\n\n\treturn nil\n}\n\n// Subscribe handles the payment for the caller's subscription.\nfunc (rs *RecurringSubscription) Subscribe() error {\n\tcaller := std.PrevRealm().Addr()\n\n\treturn rs.processSubscription(caller)\n}\n\n// GiftSubscription allows the user to pay for a subscription for another user (receiver).\nfunc (rs *RecurringSubscription) GiftSubscription(receiver std.Address) error {\n\treturn rs.processSubscription(receiver)\n}\n\n// GetExpiration returns the expiration date of the recurring subscription for a given caller.\nfunc (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, error) {\n\texpTime, exists := rs.subs.Get(addr.String())\n\tif !exists {\n\t\treturn time.Time{}, ErrNoSub\n\t}\n\n\treturn expTime.(time.Time), nil\n}\n\n// UpdateAmount allows the owner of the subscription contract to change the required subscription amount.\nfunc (rs *RecurringSubscription) UpdateAmount(newAmount int64) error {\n\tif err := rs.CallerIsOwner(); err != nil {\n\t\treturn ErrNotAuthorized\n\t}\n\n\trs.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current amount required for each subscription period.\nfunc (rs *RecurringSubscription) GetAmount() int64 {\n\treturn rs.amount\n}\n" + }, + { + "name": "recurring_test.gno", + "body": "package recurring\n\nimport (\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestRecurringSubscription(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n\n\texpiration, err := rs.GetExpiration(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected to get expiration for Alice\")\n}\n\nfunc TestRecurringSubscriptionGift(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.GiftSubscription(bob)\n\tuassert.NoError(t, err, \"Expected ProcessPaymentGift to succeed for Bob\")\n\n\terr = rs.HasValidSubscription(bob)\n\tuassert.NoError(t, err, \"Expected Bob to have access\")\n\n\terr = rs.HasValidSubscription(charlie)\n\tuassert.Error(t, err, \"Expected Charlie to fail access check\")\n}\n\nfunc TestRecurringSubscriptionExpiration(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n\n\texpiration := time.Now().Add(-time.Hour * 2)\n\trs.subs.Set(std.PrevRealm().Addr().String(), expiration)\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.Error(t, err, \"Expected Alice's subscription to be expired\")\n}\n\nfunc TestUpdateAmountAuthorization(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\terr := rs.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tstd.TestSetOrigCaller(bob)\n\terr = rs.UpdateAmount(3000)\n\tuassert.Error(t, err, \"Expected Bob to fail when updating amount\")\n}\n\nfunc TestGetAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tamount := rs.GetAmount()\n\tuassert.Equal(t, amount, int64(1000), \"Expected the initial amount to be 1000 ugnot\")\n\n\terr := rs.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tamount = rs.GetAmount()\n\tuassert.Equal(t, amount, int64(2000), \"Expected the updated amount to be 2000 ugnot\")\n}\n\nfunc TestIncorrectPaymentAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}}, nil)\n\terr := rs.Subscribe()\n\tuassert.Error(t, err, \"Expected payment with incorrect amount to fail\")\n}\n\nfunc TestMultiplePaymentsForSameUser(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected first ProcessPayment to succeed for Alice\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = rs.Subscribe()\n\tuassert.Error(t, err, \"Expected second ProcessPayment to fail for Alice due to existing subscription\")\n}\n\nfunc TestRecurringSubscriptionWithMultiplePayments(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected first ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access after first payment\")\n\n\texpiration := time.Now().Add(-time.Hour * 2)\n\trs.subs.Set(std.PrevRealm().Addr().String(), expiration)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected second ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access after second payment\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "svg", + "path": "gno.land/p/demo/svg", + "files": [ + { + "name": "doc.gno", + "body": "/*\nPackage svg is a minimalist SVG generation library for Gno.\n\nThe svg package provides a simple and lightweight solution for programmatically generating SVG (Scalable Vector Graphics) markup in Gno. It allows you to create basic shapes like rectangles and circles, and output the generated SVG to a\n\nExample:\n\n\timport \"gno.land/p/demo/svg\"\"\n\n\tfunc Foo() string {\n\t canvas := svg.Canvas{Width: 200, Height: 200}\n\t canvas.DrawRectangle(50, 50, 100, 100, \"red\")\n\t canvas.DrawCircle(100, 100, 50, \"blue\")\n\t return canvas.String()\n\t}\n*/\npackage svg // import \"gno.land/p/demo/svg\"\n" + }, + { + "name": "svg.gno", + "body": "package svg\n\nimport \"gno.land/p/demo/ufmt\"\n\ntype Canvas struct {\n\tWidth int\n\tHeight int\n\tElems []Elem\n}\n\ntype Elem interface{ String() string }\n\nfunc (c Canvas) String() string {\n\toutput := \"\"\n\toutput += ufmt.Sprintf(`\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\"\u003e`, c.Width, c.Height)\n\tfor _, elem := range c.Elems {\n\t\toutput += elem.String()\n\t}\n\toutput += \"\u003c/svg\u003e\"\n\treturn output\n}\n\nfunc (c *Canvas) Append(elem Elem) {\n\tc.Elems = append(c.Elems, elem)\n}\n\ntype Circle struct {\n\tCX int // center X\n\tCY int // center Y\n\tR int // radius\n\tFill string\n}\n\nfunc (c Circle) String() string {\n\treturn ufmt.Sprintf(`\u003ccircle cx=\"%d\" cy=\"%d\" r=\"%d\" fill=\"%s\" /\u003e`, c.CX, c.CY, c.R, c.Fill)\n}\n\nfunc (c *Canvas) DrawCircle(cx, cy, r int, fill string) {\n\tc.Append(Circle{\n\t\tCX: cx,\n\t\tCY: cy,\n\t\tR: r,\n\t\tFill: fill,\n\t})\n}\n\ntype Rectangle struct {\n\tX, Y, Width, Height int\n\tFill string\n}\n\nfunc (c Rectangle) String() string {\n\treturn ufmt.Sprintf(`\u003crect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"%s\" /\u003e`, c.X, c.Y, c.Width, c.Height, c.Fill)\n}\n\nfunc (c *Canvas) DrawRectangle(x, y, width, height int, fill string) {\n\tc.Append(Rectangle{\n\t\tX: x,\n\t\tY: y,\n\t\tWidth: width,\n\t\tHeight: height,\n\t\tFill: fill,\n\t})\n}\n\ntype Text struct {\n\tX, Y int\n\tText, Fill string\n}\n\nfunc (c Text) String() string {\n\treturn ufmt.Sprintf(`\u003ctext x=\"%d\" y=\"%d\" fill=\"%s\"\u003e%s\u003c/text\u003e`, c.X, c.Y, c.Fill, c.Text)\n}\n\nfunc (c *Canvas) DrawText(x, y int, text, fill string) {\n\tc.Append(Text{\n\t\tX: x,\n\t\tY: y,\n\t\tText: text,\n\t\tFill: fill,\n\t})\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "// PKGPATH: gno.land/p/demo/svg_test\npackage svg_test\n\nimport \"gno.land/p/demo/svg\"\n\nfunc main() {\n\tcanvas := svg.Canvas{Width: 500, Height: 500}\n\tcanvas.DrawRectangle(50, 50, 100, 100, \"red\")\n\tcanvas.DrawCircle(100, 100, 50, \"blue\")\n\tcanvas.DrawText(100, 100, \"hello world!\", \"magenta\")\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"100\" cy=\"100\" r=\"50\" fill=\"blue\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n" + }, + { + "name": "z1_filetest.gno", + "body": "// PKGPATH: gno.land/p/demo/svg_test\npackage svg_test\n\nimport \"gno.land/p/demo/svg\"\n\nfunc main() {\n\tcanvas := svg.Canvas{\n\t\tWidth: 500, Height: 500,\n\t\tElems: []svg.Elem{\n\t\t\tsvg.Rectangle{50, 50, 100, 100, \"red\"},\n\t\t\tsvg.Circle{50, 50, 100, \"red\"},\n\t\t\tsvg.Text{100, 100, \"hello world!\", \"magenta\"},\n\t\t},\n\t}\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"50\" cy=\"50\" r=\"100\" fill=\"red\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "tamagotchi", + "path": "gno.land/p/demo/tamagotchi", + "files": [ + { + "name": "tamagotchi.gno", + "body": "package tamagotchi\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Tamagotchi structure\ntype Tamagotchi struct {\n\tname string\n\thunger int\n\thappiness int\n\thealth int\n\tage int\n\tmaxAge int\n\tsleepy int\n\tcreated time.Time\n\tlastUpdated time.Time\n}\n\nfunc New(name string) *Tamagotchi {\n\tnow := time.Now()\n\treturn \u0026Tamagotchi{\n\t\tname: name,\n\t\thunger: 50,\n\t\thappiness: 50,\n\t\thealth: 50,\n\t\tmaxAge: 100,\n\t\tlastUpdated: now,\n\t\tcreated: now,\n\t}\n}\n\nfunc (t *Tamagotchi) Name() string {\n\tt.update()\n\treturn t.name\n}\n\nfunc (t *Tamagotchi) Hunger() int {\n\tt.update()\n\treturn t.hunger\n}\n\nfunc (t *Tamagotchi) Happiness() int {\n\tt.update()\n\treturn t.happiness\n}\n\nfunc (t *Tamagotchi) Health() int {\n\tt.update()\n\treturn t.health\n}\n\nfunc (t *Tamagotchi) Age() int {\n\tt.update()\n\treturn t.age\n}\n\nfunc (t *Tamagotchi) Sleepy() int {\n\tt.update()\n\treturn t.sleepy\n}\n\n// Feed method for Tamagotchi\nfunc (t *Tamagotchi) Feed() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.hunger = bound(t.hunger-10, 0, 100)\n}\n\n// Play method for Tamagotchi\nfunc (t *Tamagotchi) Play() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.happiness = bound(t.happiness+10, 0, 100)\n}\n\n// Heal method for Tamagotchi\nfunc (t *Tamagotchi) Heal() {\n\tt.update()\n\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.health = bound(t.health+10, 0, 100)\n}\n\nfunc (t Tamagotchi) dead() bool { return t.health == 0 }\n\n// Update applies changes based on the duration since the last update\nfunc (t *Tamagotchi) update() {\n\tif t.dead() {\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\tif t.lastUpdated == now {\n\t\treturn\n\t}\n\n\tduration := now.Sub(t.lastUpdated)\n\telapsedMins := int(duration.Minutes())\n\n\tt.hunger = bound(t.hunger+elapsedMins, 0, 100)\n\tt.happiness = bound(t.happiness-elapsedMins, 0, 100)\n\tt.health = bound(t.health-elapsedMins, 0, 100)\n\tt.sleepy = bound(t.sleepy+elapsedMins, 0, 100)\n\n\t// age is hours since created\n\tt.age = int(now.Sub(t.created).Hours())\n\tif t.age \u003e t.maxAge {\n\t\tt.age = t.maxAge\n\t\tt.health = 0\n\t}\n\tif t.health == 0 {\n\t\tt.sleepy = 0\n\t\tt.happiness = 0\n\t\tt.hunger = 0\n\t}\n\n\tt.lastUpdated = now\n}\n\n// Face returns an ASCII art representation of the Tamagotchi's current state\nfunc (t *Tamagotchi) Face() string {\n\tt.update()\n\treturn t.face()\n}\n\nfunc (t *Tamagotchi) face() string {\n\tswitch {\n\tcase t.health == 0:\n\t\treturn \"😵\" // dead face\n\tcase t.health \u003c 30:\n\t\treturn \"😷\" // sick face\n\tcase t.happiness \u003c 30:\n\t\treturn \"😢\" // sad face\n\tcase t.hunger \u003e 70:\n\t\treturn \"😫\" // hungry face\n\tcase t.sleepy \u003e 70:\n\t\treturn \"😴\" // sleepy face\n\tdefault:\n\t\treturn \"😃\" // happy face\n\t}\n}\n\n// Markdown method for Tamagotchi\nfunc (t *Tamagotchi) Markdown() string {\n\tt.update()\n\treturn ufmt.Sprintf(`# %s %s\n\n* age: %d\n* hunger: %d\n* happiness: %d\n* health: %d\n* sleepy: %d`,\n\t\tt.name, t.Face(),\n\t\tt.age, t.hunger, t.happiness, t.health, t.sleepy,\n\t)\n}\n\nfunc bound(n, min, max int) int {\n\tif n \u003c min {\n\t\treturn min\n\t}\n\tif n \u003e max {\n\t\treturn max\n\t}\n\treturn n\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"time\"\n\n\t\"internal/os_test\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n)\n\nfunc main() {\n\tt := tamagotchi.New(\"Gnome\")\n\n\tprintln(\"\\n-- INITIAL\\n\")\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos_test.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- FEEDx3, PLAYx2, HEALx4\\n\")\n\tt.Feed()\n\tt.Feed()\n\tt.Feed()\n\tt.Play()\n\tt.Play()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos_test.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos_test.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos_test.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n}\n\n// Output:\n// -- INITIAL\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 70\n// * happiness: 30\n// * health: 30\n// * sleepy: 20\n//\n// -- FEEDx3, PLAYx2, HEALx4\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 40\n// * happiness: 50\n// * health: 70\n// * sleepy: 20\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 60\n// * happiness: 30\n// * health: 50\n// * sleepy: 40\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "subtests", + "path": "gno.land/p/demo/tests/subtests", + "files": [ + { + "name": "subtests.gno", + "body": "package subtests\n\nimport (\n\t\"std\"\n)\n\nfunc GetCurrentRealm() std.Realm {\n\treturn std.CurrentRealm()\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "subtests", + "path": "gno.land/r/demo/tests/subtests", + "files": [ + { + "name": "subtests.gno", + "body": "package subtests\n\nimport (\n\t\"std\"\n)\n\nfunc GetCurrentRealm() std.Realm {\n\treturn std.CurrentRealm()\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc CallAssertOriginCall() {\n\tstd.AssertOriginCall()\n}\n\nfunc CallIsOriginCall() bool {\n\treturn std.IsOriginCall()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "tests", + "path": "gno.land/r/demo/tests", + "files": [ + { + "name": "README.md", + "body": "Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n" + }, + { + "name": "interfaces.gno", + "body": "package tests\n\nimport (\n\t\"strconv\"\n)\n\ntype Stringer interface {\n\tString() string\n}\n\nvar stringers []Stringer\n\nfunc AddStringer(str Stringer) {\n\t// NOTE: this is ridiculous, a slice that will become too long\n\t// eventually. Don't do this in production programs; use\n\t// gno.land/p/demo/avl or similar structures.\n\tstringers = append(stringers, str)\n}\n\nfunc Render(path string) string {\n\tres := \"\"\n\t// NOTE: like the function above, this function too will eventually\n\t// become too expensive to call.\n\tfor i, stringer := range stringers {\n\t\tres += strconv.Itoa(i) + \": \" + stringer.String() + \"\\n\"\n\t}\n\treturn res\n}\n" + }, + { + "name": "nestedpkg_test.gno", + "body": "package tests\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\nfunc TestNestedPkg(t *testing.T) {\n\t// direct child\n\tcur := \"gno.land/r/demo/tests/foo\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif !IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// grand-grand-child\n\tcur = \"gno.land/r/demo/tests/foo/bar/baz\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif !IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// direct parent\n\tcur = \"gno.land/r/demo\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif !IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// fake parent (prefix)\n\tcur = \"gno.land/r/dem\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n\n\t// different namespace\n\tcur = \"gno.land/r/foo\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n}\n" + }, + { + "name": "realm_compositelit.gno", + "body": "package tests\n\ntype (\n\tWord uint\n\tnat []Word\n)\n\nvar zero = \u0026Int{\n\tneg: true,\n\tabs: []Word{0},\n}\n\n// structLit\ntype Int struct {\n\tneg bool\n\tabs nat\n}\n\nfunc GetZeroType() nat {\n\ta := zero.abs\n\treturn a\n}\n" + }, + { + "name": "realm_method38d.gno", + "body": "package tests\n\nvar abs nat\n\nfunc (n nat) Add() nat {\n\treturn []Word{0}\n}\n\nfunc GetAbs() nat {\n\tabs = []Word{0}\n\n\treturn abs\n}\n\nfunc AbsAdd() nat {\n\trt := GetAbs().Add()\n\n\treturn rt\n}\n" + }, + { + "name": "tests.gno", + "body": "package tests\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/nestedpkg\"\n\trsubtests \"gno.land/r/demo/tests/subtests\"\n)\n\nvar counter int\n\nfunc IncCounter() {\n\tcounter++\n}\n\nfunc Counter() int {\n\treturn counter\n}\n\nfunc CurrentRealmPath() string {\n\treturn std.CurrentRealm().PkgPath()\n}\n\nvar initOrigCaller = std.GetOrigCaller()\n\nfunc InitOrigCaller() std.Address {\n\treturn initOrigCaller\n}\n\nfunc CallAssertOriginCall() {\n\tstd.AssertOriginCall()\n}\n\nfunc CallIsOriginCall() bool {\n\treturn std.IsOriginCall()\n}\n\nfunc CallSubtestsAssertOriginCall() {\n\trsubtests.CallAssertOriginCall()\n}\n\nfunc CallSubtestsIsOriginCall() bool {\n\treturn rsubtests.CallIsOriginCall()\n}\n\n//----------------------------------------\n// Test structure to ensure cross-realm modification is prevented.\n\ntype TestRealmObject struct {\n\tField string\n}\n\nfunc ModifyTestRealmObject(t *TestRealmObject) {\n\tt.Field += \"_modified\"\n}\n\nfunc (t *TestRealmObject) Modify() {\n\tt.Field += \"_modified\"\n}\n\n//----------------------------------------\n// Test helpers to test a particular realm bug.\n\ntype TestNode struct {\n\tName string\n\tChild *TestNode\n}\n\nvar (\n\tgTestNode1 *TestNode\n\tgTestNode2 *TestNode\n\tgTestNode3 *TestNode\n)\n\nfunc InitTestNodes() {\n\tgTestNode1 = \u0026TestNode{Name: \"first\"}\n\tgTestNode2 = \u0026TestNode{Name: \"second\", Child: \u0026TestNode{Name: \"second's child\"}}\n}\n\nfunc ModTestNodes() {\n\ttmp := \u0026TestNode{}\n\ttmp.Child = gTestNode2.Child\n\tgTestNode3 = tmp // set to new-real\n\t// gTestNode1 = tmp.Child // set back to original is-real\n\tgTestNode3 = nil // delete.\n}\n\nfunc PrintTestNodes() {\n\tprintln(gTestNode2.Child.Name)\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc GetRSubtestsPrevRealm() std.Realm {\n\treturn rsubtests.GetPrevRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc IsCallerSubPath() bool {\n\treturn nestedpkg.IsCallerSubPath()\n}\n\nfunc IsCallerParentPath() bool {\n\treturn nestedpkg.IsCallerParentPath()\n}\n\nfunc HasCallerSameNamespace() bool {\n\treturn nestedpkg.IsSameNamespace()\n}\n" + }, + { + "name": "tests_test.gno", + "body": "package tests\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\nfunc TestAssertOriginCall(t *testing.T) {\n\t// CallAssertOriginCall(): no panic\n\tCallAssertOriginCall()\n\tif !CallIsOriginCall() {\n\t\tt.Errorf(\"expected IsOriginCall=true but got false\")\n\t}\n\n\t// CallAssertOriginCall() from a block: panic\n\texpectedReason := \"invalid non-origin call\"\n\tfunc() {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tif r == nil || r.(string) != expectedReason {\n\t\t\t\tt.Errorf(\"expected panic with '%v', got '%v'\", expectedReason, r)\n\t\t\t}\n\t\t}()\n\t\t// if called inside a function literal, this is no longer an origin call\n\t\t// because there's one additional frame (the function literal block).\n\t\tif CallIsOriginCall() {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\tCallAssertOriginCall()\n\t}()\n\n\t// CallSubtestsAssertOriginCall(): panic\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil || r.(string) != expectedReason {\n\t\t\tt.Errorf(\"expected panic with '%v', got '%v'\", expectedReason, r)\n\t\t}\n\t}()\n\tif CallSubtestsIsOriginCall() {\n\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t}\n\tCallSubtestsAssertOriginCall()\n}\n\nfunc TestPrevRealm(t *testing.T) {\n\tvar (\n\t\tuser1Addr = std.DerivePkgAddr(\"user1.gno\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\t// When a single realm in the frames, PrevRealm returns the user\n\tif addr := GetPrevRealm().Addr(); addr != user1Addr {\n\t\tt.Errorf(\"want GetPrevRealm().Addr==%s, got %s\", user1Addr, addr)\n\t}\n\t// When 2 or more realms in the frames, PrevRealm returns the second to last\n\tif addr := GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr {\n\t\tt.Errorf(\"want GetRSubtestsPrevRealm().Addr==%s, got %s\", rTestsAddr, addr)\n\t}\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall())\n\ttests.CallAssertOriginCall()\n\tprintln(\"tests.CallAssertOriginCall doesn't panic when called directly\")\n\n\t{\n\t\t// if called inside a block, this is no longer an origin call because\n\t\t// there's one additional frame (the block).\n\t\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall())\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tprintln(\"tests.AssertOriginCall panics if when called inside a function literal:\", r)\n\t\t}()\n\t\ttests.CallAssertOriginCall()\n\t}\n}\n\n// Output:\n// tests.CallIsOriginCall: true\n// tests.CallAssertOriginCall doesn't panic when called directly\n// tests.CallIsOriginCall: true\n// tests.AssertOriginCall panics if when called inside a function literal: undefined\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(tests.Counter())\n\ttests.IncCounter()\n\tprintln(tests.Counter())\n}\n\n// Output:\n// 0\n// 1\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\n)\n\n// When a single realm in the frames, PrevRealm returns the user\n// When 2 or more realms in the frames, PrevRealm returns the second to last\nfunc main() {\n\tvar (\n\t\teoa = testutils.TestAddress(\"someone\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\tstd.TestSetOrigCaller(eoa)\n\tprintln(\"tests.GetPrevRealm().Addr(): \", tests.GetPrevRealm().Addr())\n\tprintln(\"tests.GetRSubtestsPrevRealm().Addr(): \", tests.GetRSubtestsPrevRealm().Addr())\n}\n\n// Output:\n// tests.GetPrevRealm().Addr(): g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk\n// tests.GetRSubtestsPrevRealm().Addr(): g1gz4ycmx0s6ln2wdrsh4e00l9fsel2wskqa3snq\n" + }, + { + "name": "z3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/test_test\npackage test_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tvar (\n\t\teoa = testutils.TestAddress(\"someone\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\tstd.TestSetOrigCaller(eoa)\n\t// Contrarily to z2_filetest.gno we EXPECT GetPrevRealms != eoa (#1704)\n\tif addr := tests.GetPrevRealm().Addr(); addr != eoa {\n\t\tprintln(\"want tests.GetPrevRealm().Addr ==\", eoa, \"got\", addr)\n\t}\n\t// When 2 or more realms in the frames, it is also different\n\tif addr := tests.GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr {\n\t\tprintln(\"want GetRSubtestsPrevRealm().Addr ==\", rTestsAddr, \"got\", addr)\n\t}\n}\n\n// Output:\n// want tests.GetPrevRealm().Addr == g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk got g1xufrdvnfk6zc9r0nqa23ld3tt2r5gkyvw76q63\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "tests", + "path": "gno.land/p/demo/tests", + "files": [ + { + "name": "README.md", + "body": "Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n" + }, + { + "name": "tests.gno", + "body": "package tests\n\nimport (\n\t\"std\"\n\n\tpsubtests \"gno.land/p/demo/tests/subtests\"\n\t\"gno.land/r/demo/tests\"\n\trtests \"gno.land/r/demo/tests\"\n)\n\nconst World = \"world\"\n\n// IncCounter demonstrates that it's possible to call a realm function from\n// a package. So a package can potentially write into the store, by calling\n// an other realm.\nfunc IncCounter() {\n\ttests.IncCounter()\n}\n\nfunc CurrentRealmPath() string {\n\treturn std.CurrentRealm().PkgPath()\n}\n\n//----------------------------------------\n// cross realm test vars\n\ntype TestRealmObject2 struct {\n\tField string\n}\n\nfunc (o2 *TestRealmObject2) Modify() {\n\to2.Field = \"modified\"\n}\n\nvar (\n\tsomevalue1 TestRealmObject2\n\tSomeValue2 TestRealmObject2\n\tSomeValue3 *TestRealmObject2\n)\n\nfunc init() {\n\tsomevalue1 = TestRealmObject2{Field: \"init\"}\n\tSomeValue2 = TestRealmObject2{Field: \"init\"}\n\tSomeValue3 = \u0026TestRealmObject2{Field: \"init\"}\n}\n\nfunc ModifyTestRealmObject2a() {\n\tsomevalue1.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2b() {\n\tSomeValue2.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2c() {\n\tSomeValue3.Field = \"modified\"\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc GetPSubtestsPrevRealm() std.Realm {\n\treturn psubtests.GetPrevRealm()\n}\n\nfunc GetRTestsGetPrevRealm() std.Realm {\n\treturn rtests.GetPrevRealm()\n}\n\n// Warning: unsafe pattern.\nfunc Exec(fn func()) {\n\tfn()\n}\n" + }, + { + "name": "tests_test.gno", + "body": "package tests_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/tests\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar World = \"WORLD\"\n\nfunc TestGetHelloWorld(t *testing.T) {\n\t// tests.World is 'world'\n\ts := \"hello \" + tests.World + World\n\tconst want = \"hello worldWORLD\"\n\n\tuassert.Equal(t, want, s)\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\tptests \"gno.land/p/demo/tests\"\n\trtests \"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(rtests.Counter())\n\tptests.IncCounter()\n\tprintln(rtests.Counter())\n}\n\n// Output:\n// 0\n// 1\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "p_crossrealm", + "path": "gno.land/p/demo/tests/p_crossrealm", + "files": [ + { + "name": "p_crossrealm.gno", + "body": "package p_crossrealm\n\ntype Stringer interface {\n\tString() string\n}\n\ntype Container struct {\n\tA int\n\tB Stringer\n}\n\nfunc (c *Container) Touch() *Container {\n\tc.A += 1\n\treturn c\n}\n\nfunc (c *Container) Print() {\n\tprintln(\"A:\", c.A)\n\tif c.B == nil {\n\t\tprintln(\"B: undefined\")\n\t} else {\n\t\tprintln(\"B:\", c.B.String())\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "todolist", + "path": "gno.land/p/demo/todolist", + "files": [ + { + "name": "todolist.gno", + "body": "package todolist\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype TodoList struct {\n\tTitle string\n\tTasks *avl.Tree\n\tOwner std.Address\n}\n\ntype Task struct {\n\tTitle string\n\tDone bool\n}\n\nfunc NewTodoList(title string) *TodoList {\n\treturn \u0026TodoList{\n\t\tTitle: title,\n\t\tTasks: avl.NewTree(),\n\t\tOwner: std.GetOrigCaller(),\n\t}\n}\n\nfunc NewTask(title string) *Task {\n\treturn \u0026Task{\n\t\tTitle: title,\n\t\tDone: false,\n\t}\n}\n\nfunc (tl *TodoList) AddTask(id int, task *Task) {\n\ttl.Tasks.Set(strconv.Itoa(id), task)\n}\n\nfunc ToggleTaskStatus(task *Task) {\n\ttask.Done = !task.Done\n}\n\nfunc (tl *TodoList) RemoveTask(taskId string) {\n\ttl.Tasks.Remove(taskId)\n}\n\nfunc (tl *TodoList) GetTasks() []*Task {\n\ttasks := make([]*Task, 0, tl.Tasks.Size())\n\ttl.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttasks = append(tasks, value.(*Task))\n\t\treturn false\n\t})\n\treturn tasks\n}\n\nfunc (tl *TodoList) GetTodolistOwner() std.Address {\n\treturn tl.Owner\n}\n\nfunc (tl *TodoList) GetTodolistTitle() string {\n\treturn tl.Title\n}\n" + }, + { + "name": "todolist_test.gno", + "body": "package todolist\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttodoList := NewTodoList(title)\n\n\tuassert.Equal(t, title, todoList.GetTodolistTitle())\n\tuassert.Equal(t, 0, len(todoList.GetTasks()))\n\tuassert.Equal(t, std.GetOrigCaller().String(), todoList.GetTodolistOwner().String())\n}\n\nfunc TestNewTask(t *testing.T) {\n\ttitle := \"My Task\"\n\ttask := NewTask(title)\n\n\tuassert.Equal(t, title, task.Title)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is done\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\n\ttodoList.AddTask(1, task)\n\n\ttasks := todoList.GetTasks()\n\n\tuassert.Equal(t, 1, len(tasks))\n\tuassert.True(t, tasks[0] == task, \"Task does not match\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\ttask := NewTask(\"My Task\")\n\n\tToggleTaskStatus(task)\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not done\")\n\n\tToggleTaskStatus(task)\n\tuassert.False(t, task.Done, \"Expected task to be done, but it is not done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\ttodoList.AddTask(1, task)\n\n\ttodoList.RemoveTask(\"1\")\n\n\ttasks := todoList.GetTasks()\n\tuassert.Equal(t, 0, len(tasks))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "ui", + "path": "gno.land/p/demo/ui", + "files": [ + { + "name": "ui.gno", + "body": "package ui\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype DOM struct {\n\t// metadata\n\tPrefix string\n\tTitle string\n\tWithComments bool\n\tClasses []string\n\n\t// elements\n\tHeader Element\n\tBody Element\n\tFooter Element\n}\n\nfunc (dom DOM) String() string {\n\tclasses := strings.Join(dom.Classes, \" \")\n\n\toutput := \"\"\n\n\tif classes != \"\" {\n\t\toutput += \"\u003cmain class='\" + classes + \"'\u003e\" + \"\\n\\n\"\n\t}\n\n\tif dom.Title != \"\" {\n\t\toutput += H1(dom.Title).String(dom) + \"\\n\"\n\t}\n\n\tif header := dom.Header.String(dom); header != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- header --\u003e\"\n\t\t}\n\t\toutput += header + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /header --\u003e\"\n\t\t}\n\t}\n\n\tif body := dom.Body.String(dom); body != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- body --\u003e\"\n\t\t}\n\t\toutput += body + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /body --\u003e\"\n\t\t}\n\t}\n\n\tif footer := dom.Footer.String(dom); footer != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- footer --\u003e\"\n\t\t}\n\t\toutput += footer + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /footer --\u003e\"\n\t\t}\n\t}\n\n\tif classes != \"\" {\n\t\toutput += \"\u003c/main\u003e\"\n\t}\n\n\t// TODO: cleanup double new-lines.\n\n\treturn output\n}\n\ntype Jumbotron []DomStringer\n\nfunc (j Jumbotron) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tfor _, elem := range j {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\toutput += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\treturn output\n}\n\n// XXX: rename Element to Div?\ntype Element []DomStringer\n\nfunc (e *Element) Append(elems ...DomStringer) {\n\t*e = append(*e, elems...)\n}\n\nfunc (e *Element) String(dom DOM) string {\n\toutput := \"\"\n\tfor _, elem := range *e {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\treturn output\n}\n\ntype Breadcrumb []DomStringer\n\nfunc (b *Breadcrumb) Append(elems ...DomStringer) {\n\t*b = append(*b, elems...)\n}\n\nfunc (b Breadcrumb) String(dom DOM) string {\n\toutput := \"\"\n\tfor idx, entry := range b {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" / \"\n\t\t}\n\t\toutput += entry.String(dom)\n\t}\n\treturn output\n}\n\ntype Columns struct {\n\tMaxWidth int\n\tColumns []Element\n}\n\nfunc (c *Columns) Append(elems ...Element) {\n\tc.Columns = append(c.Columns, elems...)\n}\n\nfunc (c Columns) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"columns-` + strconv.Itoa(c.MaxWidth) + `\"\u003e` + \"\\n\"\n\tfor _, entry := range c.Columns {\n\t\toutput += `\u003cdiv class=\"column\"\u003e` + \"\\n\\n\"\n\t\toutput += entry.String(dom)\n\t\toutput += \"\u003c/div\u003e\u003c!-- /column--\u003e\\n\"\n\t}\n\toutput += \"\u003c/div\u003e\u003c!-- /columns-\" + strconv.Itoa(c.MaxWidth) + \" --\u003e\\n\"\n\treturn output\n}\n\ntype Link struct {\n\tText string\n\tPath string\n\tURL string\n}\n\n// TODO: image\n\n// TODO: pager\n\nfunc (l Link) String(dom DOM) string {\n\turl := \"\"\n\tswitch {\n\tcase l.Path != \"\" \u0026\u0026 l.URL != \"\":\n\t\tpanic(\"a link should have a path or a URL, not both.\")\n\tcase l.Path != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.Path\n\t\t}\n\t\turl = dom.Prefix + l.Path\n\tcase l.URL != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.URL\n\t\t}\n\t\turl = l.URL\n\t}\n\n\treturn \"[\" + l.Text + \"](\" + url + \")\"\n}\n\ntype BulletList []DomStringer\n\nfunc (bl BulletList) String(dom DOM) string {\n\toutput := \"\"\n\n\tfor _, entry := range bl {\n\t\toutput += \"- \" + entry.String(dom) + \"\\n\"\n\t}\n\n\treturn output\n}\n\nfunc Text(s string) DomStringer {\n\treturn Raw{Content: s}\n}\n\ntype DomStringer interface {\n\tString(dom DOM) string\n}\n\ntype Raw struct {\n\tContent string\n}\n\nfunc (r Raw) String(_ DOM) string {\n\treturn r.Content\n}\n\ntype (\n\tH1 string\n\tH2 string\n\tH3 string\n\tH4 string\n\tH5 string\n\tH6 string\n\tBold string\n\tItalic string\n\tCode string\n\tParagraph string\n\tQuote string\n\tHR struct{}\n)\n\nfunc (text H1) String(_ DOM) string { return \"# \" + string(text) + \"\\n\" }\nfunc (text H2) String(_ DOM) string { return \"## \" + string(text) + \"\\n\" }\nfunc (text H3) String(_ DOM) string { return \"### \" + string(text) + \"\\n\" }\nfunc (text H4) String(_ DOM) string { return \"#### \" + string(text) + \"\\n\" }\nfunc (text H5) String(_ DOM) string { return \"##### \" + string(text) + \"\\n\" }\nfunc (text H6) String(_ DOM) string { return \"###### \" + string(text) + \"\\n\" }\nfunc (text Quote) String(_ DOM) string { return \"\u003e \" + string(text) + \"\\n\" }\nfunc (text Bold) String(_ DOM) string { return \"**\" + string(text) + \"**\" }\nfunc (text Italic) String(_ DOM) string { return \"_\" + string(text) + \"_\" }\nfunc (text Paragraph) String(_ DOM) string { return \"\\n\" + string(text) + \"\\n\" }\nfunc (_ HR) String(_ DOM) string { return \"\\n---\\n\" }\n\nfunc (text Code) String(_ DOM) string {\n\t// multiline\n\tif strings.Contains(string(text), \"\\n\") {\n\t\treturn \"\\n```\\n\" + string(text) + \"\\n```\\n\"\n\t}\n\n\t// single line\n\treturn \"`\" + string(text) + \"`\"\n}\n" + }, + { + "name": "ui_test.gno", + "body": "package ui\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "watchdog", + "path": "gno.land/p/demo/watchdog", + "files": [ + { + "name": "watchdog.gno", + "body": "package watchdog\n\nimport \"time\"\n\ntype Watchdog struct {\n\tDuration time.Duration\n\tlastUpdate time.Time\n\tlastDown time.Time\n}\n\nfunc (w *Watchdog) Alive() {\n\tnow := time.Now()\n\tif !w.IsAlive() {\n\t\tw.lastDown = now\n\t}\n\tw.lastUpdate = now\n}\n\nfunc (w Watchdog) Status() string {\n\tif w.IsAlive() {\n\t\treturn \"OK\"\n\t}\n\treturn \"KO\"\n}\n\nfunc (w Watchdog) IsAlive() bool {\n\treturn time.Since(w.lastUpdate) \u003c w.Duration\n}\n\nfunc (w Watchdog) UpSince() time.Time {\n\treturn w.lastDown\n}\n\nfunc (w Watchdog) DownSince() time.Time {\n\tif !w.IsAlive() {\n\t\treturn w.lastUpdate\n\t}\n\treturn time.Time{}\n}\n" + }, + { + "name": "watchdog_test.gno", + "body": "package watchdog\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tw := Watchdog{Duration: 5 * time.Minute}\n\tuassert.False(t, w.IsAlive())\n\tw.Alive()\n\tuassert.True(t, w.IsAlive())\n\t// XXX: add more tests when we'll be able to \"skip time\".\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "executor", + "path": "gno.land/p/gov/executor", + "files": [ + { + "name": "callback.gno", + "body": "package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar errInvalidCaller = errors.New(\"invalid executor caller\")\n\n// NewCallbackExecutor creates a new callback executor with the provided callback function\nfunc NewCallbackExecutor(callback func() error, path string) *CallbackExecutor {\n\treturn \u0026CallbackExecutor{\n\t\tcallback: callback,\n\t\tdaoPkgPath: path,\n\t}\n}\n\n// CallbackExecutor is an implementation of the dao.Executor interface,\n// based on a specific callback.\n// The given callback should verify the validity of the govdao call\ntype CallbackExecutor struct {\n\tcallback func() error // the callback to be executed\n\tdaoPkgPath string // the active pkg path of the govdao\n}\n\n// Execute runs the executor's callback function.\nfunc (exec *CallbackExecutor) Execute() error {\n\t// Verify the caller is an adequate Realm\n\tcaller := std.CurrentRealm().PkgPath()\n\tif caller != exec.daoPkgPath {\n\t\treturn errInvalidCaller\n\t}\n\n\tif exec.callback != nil {\n\t\treturn exec.callback()\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "context.gno", + "body": "package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/context\"\n)\n\ntype propContextKey string\n\nfunc (k propContextKey) String() string { return string(k) }\n\nconst (\n\tstatusContextKey = propContextKey(\"govdao-prop-status\")\n\tapprovedStatus = \"approved\"\n)\n\nvar errNotApproved = errors.New(\"not approved by govdao\")\n\n// CtxExecutor is an implementation of the dao.Executor interface,\n// based on the given context.\n// It utilizes the given context to assert the validity of the govdao call\ntype CtxExecutor struct {\n\tcallbackCtx func(ctx context.Context) error // the callback ctx fn, if any\n\tdaoPkgPath string // the active pkg path of the govdao\n}\n\n// NewCtxExecutor creates a new executor with the provided callback function.\nfunc NewCtxExecutor(callback func(ctx context.Context) error, path string) *CtxExecutor {\n\treturn \u0026CtxExecutor{\n\t\tcallbackCtx: callback,\n\t\tdaoPkgPath: path,\n\t}\n}\n\n// Execute runs the executor's callback function\nfunc (exec *CtxExecutor) Execute() error {\n\t// Verify the caller is an adequate Realm\n\tcaller := std.CurrentRealm().PkgPath()\n\tif caller != exec.daoPkgPath {\n\t\treturn errInvalidCaller\n\t}\n\n\t// Create the context\n\tctx := context.WithValue(\n\t\tcontext.Empty(),\n\t\tstatusContextKey,\n\t\tapprovedStatus,\n\t)\n\n\treturn exec.callbackCtx(ctx)\n}\n\n// IsApprovedByGovdaoContext asserts that the govdao approved the context\nfunc IsApprovedByGovdaoContext(ctx context.Context) bool {\n\tv := ctx.Value(statusContextKey)\n\tif v == nil {\n\t\treturn false\n\t}\n\n\tvs, ok := v.(string)\n\n\treturn ok \u0026\u0026 vs == approvedStatus\n}\n\n// AssertContextApprovedByGovDAO asserts the given context\n// was approved by GOVDAO\nfunc AssertContextApprovedByGovDAO(ctx context.Context) {\n\tif IsApprovedByGovdaoContext(ctx) {\n\t\treturn\n\t}\n\n\tpanic(errNotApproved)\n}\n" + }, + { + "name": "proposal_test.gno", + "body": "package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/context\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestExecutor_Callback(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"govdao not caller\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\te := NewCallbackExecutor(cb, \"gno.land/r/gov/dao\")\n\n\t\t// Execute as not the /r/gov/dao caller\n\t\tuassert.ErrorIs(t, e.Execute(), errInvalidCaller)\n\t\tuassert.False(t, called, \"expected proposal to not execute\")\n\t})\n\n\tt.Run(\"execution successful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCallbackExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\tuassert.NoError(t, e.Execute())\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n\n\tt.Run(\"execution unsuccessful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\texpectedErr = errors.New(\"unexpected\")\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn expectedErr\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCallbackExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\tuassert.ErrorIs(t, e.Execute(), expectedErr)\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n}\n\nfunc TestExecutor_Context(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"govdao not caller\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\te := NewCtxExecutor(cb, \"gno.land/r/gov/dao\")\n\n\t\t// Execute as not the /r/gov/dao caller\n\t\tuassert.ErrorIs(t, e.Execute(), errInvalidCaller)\n\t\tuassert.False(t, called, \"expected proposal to not execute\")\n\t})\n\n\tt.Run(\"execution successful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCtxExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\turequire.NoError(t, e.Execute())\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n\n\tt.Run(\"execution unsuccessful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\texpectedErr = errors.New(\"unexpected\")\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn expectedErr\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCtxExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\terr := e.Execute()\n\n\t\t\tuassert.ErrorIs(t, err, expectedErr)\n\t\t})\n\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "helplink", + "path": "gno.land/p/moul/helplink", + "files": [ + { + "name": "helplink.gno", + "body": "// Package helplink provides utilities for creating help page links compatible\n// with Gnoweb, Gnobro, and other clients that support the Gno contracts'\n// flavored Markdown format.\n//\n// This package simplifies the generation of dynamic, context-sensitive help\n// links, enabling users to navigate relevant documentation seamlessly within\n// the Gno ecosystem.\n//\n// For a more lightweight alternative, consider using p/moul/txlink.\n//\n// The primary functions — Func, FuncURL, and Home — are intended for use with\n// the \"relative realm\". When specifying a custom Realm, you can create links\n// that utilize either the current realm path or a fully qualified path to\n// another realm.\npackage helplink\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/moul/txlink\"\n)\n\nconst chainDomain = \"gno.land\" // XXX: std.ChainDomain (#2911)\n\n// Func returns a markdown link for the specific function with optional\n// key-value arguments, for the current realm.\nfunc Func(title string, fn string, args ...string) string {\n\treturn Realm(\"\").Func(title, fn, args...)\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc FuncURL(fn string, args ...string) string {\n\treturn Realm(\"\").FuncURL(fn, args...)\n}\n\n// Home returns the URL for the help homepage of the current realm.\nfunc Home() string {\n\treturn Realm(\"\").Home()\n}\n\n// Realm represents a specific realm for generating help links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// local realm -\u003e /realm\n\trealm := string(r)\n\tif strings.Contains(realm, chainDomain) {\n\t\treturn strings.TrimPrefix(realm, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + string(r)\n}\n\n// Func returns a markdown link for the specified function with optional\n// key-value arguments.\nfunc (r Realm) Func(title string, fn string, args ...string) string {\n\t// XXX: escape title\n\treturn \"[\" + title + \"](\" + r.FuncURL(fn, args...) + \")\"\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments.\nfunc (r Realm) FuncURL(fn string, args ...string) string {\n\ttlr := txlink.Realm(r)\n\treturn tlr.URL(fn, args...)\n}\n\n// Home returns the base help URL for the specified realm.\nfunc (r Realm) Home() string {\n\treturn r.prefix() + \"$help\"\n}\n" + }, + { + "name": "helplink_test.gno", + "body": "package helplink\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestFunc(t *testing.T) {\n\ttests := []struct {\n\t\ttitle string\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Example]($help\u0026func=foo\u0026bar=1\u0026baz=2)\", \"\"},\n\t\t{\"Realm Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Realm Example](/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2)\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"Single Arg\", \"testFunc\", []string{\"key\", \"value\"}, \"[Single Arg]($help\u0026func=testFunc\u0026key=value)\", \"\"},\n\t\t{\"No Args\", \"noArgsFunc\", []string{}, \"[No Args]($help\u0026func=noArgsFunc)\", \"\"},\n\t\t{\"Odd Args\", \"oddArgsFunc\", []string{\"key\"}, \"[Odd Args]($help\u0026func=oddArgsFunc)\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.title, func(t *testing.T) {\n\t\t\tgot := tt.realm.Func(tt.title, tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestFuncURL(t *testing.T) {\n\ttests := []struct {\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"$help\u0026func=oddArgsFunc\", \"\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"https://gno.world/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tgot := tt.realm.FuncURL(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestHome(t *testing.T) {\n\ttests := []struct {\n\t\trealm Realm\n\t\twant string\n\t}{\n\t\t{\"\", \"$help\"},\n\t\t{\"gno.land/r/lorem/ipsum\", \"/r/lorem/ipsum$help\"},\n\t\t{\"gno.world/r/lorem/ipsum\", \"https://gno.world/r/lorem/ipsum$help\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.realm), func(t *testing.T) {\n\t\t\tgot := tt.realm.Home()\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "printfdebugging", + "path": "gno.land/p/demo/printfdebugging", + "files": [ + { + "name": "color.gno", + "body": "package printfdebugging\n\n// consts copied from https://github.com/fatih/color/blob/main/color.go\n\n// Attribute defines a single SGR Code\ntype Attribute int\n\nconst Escape = \"\\x1b\"\n\n// Base attributes\nconst (\n\tReset Attribute = iota\n\tBold\n\tFaint\n\tItalic\n\tUnderline\n\tBlinkSlow\n\tBlinkRapid\n\tReverseVideo\n\tConcealed\n\tCrossedOut\n)\n\nconst (\n\tResetBold Attribute = iota + 22\n\tResetItalic\n\tResetUnderline\n\tResetBlinking\n\t_\n\tResetReversed\n\tResetConcealed\n\tResetCrossedOut\n)\n\n// Foreground text colors\nconst (\n\tFgBlack Attribute = iota + 30\n\tFgRed\n\tFgGreen\n\tFgYellow\n\tFgBlue\n\tFgMagenta\n\tFgCyan\n\tFgWhite\n)\n\n// Foreground Hi-Intensity text colors\nconst (\n\tFgHiBlack Attribute = iota + 90\n\tFgHiRed\n\tFgHiGreen\n\tFgHiYellow\n\tFgHiBlue\n\tFgHiMagenta\n\tFgHiCyan\n\tFgHiWhite\n)\n\n// Background text colors\nconst (\n\tBgBlack Attribute = iota + 40\n\tBgRed\n\tBgGreen\n\tBgYellow\n\tBgBlue\n\tBgMagenta\n\tBgCyan\n\tBgWhite\n)\n\n// Background Hi-Intensity text colors\nconst (\n\tBgHiBlack Attribute = iota + 100\n\tBgHiRed\n\tBgHiGreen\n\tBgHiYellow\n\tBgHiBlue\n\tBgHiMagenta\n\tBgHiCyan\n\tBgHiWhite\n)\n" + }, + { + "name": "printfdebugging.gno", + "body": "// this package is a joke... or not.\npackage printfdebugging\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc BigRedLine(args ...string) {\n\tprintln(ufmt.Sprintf(\"%s[%dm####################################%s[%dm %s\",\n\t\tEscape, int(BgRed), Escape, int(Reset),\n\t\tstrings.Join(args, \" \"),\n\t))\n}\n\nfunc Success() {\n\tprintln(\" \\033[31mS\\033[33mU\\033[32mC\\033[36mC\\033[34mE\\033[35mS\\033[31mS\\033[0m \")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "validators", + "path": "gno.land/p/sys/validators", + "files": [ + { + "name": "types.gno", + "body": "package validators\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\n// ValsetProtocol defines the validator set protocol (PoA / PoS / PoC / ?)\ntype ValsetProtocol interface {\n\t// AddValidator adds a new validator to the validator set.\n\t// If the validator is already present, the method should error out\n\t//\n\t// TODO: This API is not ideal -- the address should be derived from\n\t// the public key, and not be passed in as such, but currently Gno\n\t// does not support crypto address derivation\n\tAddValidator(address std.Address, pubKey string, power uint64) (Validator, error)\n\n\t// RemoveValidator removes the given validator from the set.\n\t// If the validator is not present in the set, the method should error out\n\tRemoveValidator(address std.Address) (Validator, error)\n\n\t// IsValidator returns a flag indicating if the given\n\t// bech32 address is part of the validator set\n\tIsValidator(address std.Address) bool\n\n\t// GetValidator returns the validator using the given address\n\tGetValidator(address std.Address) (Validator, error)\n\n\t// GetValidators returns the currently active validator set\n\tGetValidators() []Validator\n}\n\n// Validator represents a single chain validator\ntype Validator struct {\n\tAddress std.Address // bech32 address\n\tPubKey string // bech32 representation of the public key\n\tVotingPower uint64\n}\n\nconst (\n\tValidatorAddedEvent = \"ValidatorAdded\" // emitted when a validator was added to the set\n\tValidatorRemovedEvent = \"ValidatorRemoved\" // emitted when a validator was removed from the set\n)\n\nvar (\n\t// ErrValidatorExists is returned when the validator is already in the set\n\tErrValidatorExists = errors.New(\"validator already exists\")\n\n\t// ErrValidatorMissing is returned when the validator is not in the set\n\tErrValidatorMissing = errors.New(\"validator doesn't exist\")\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "poa", + "path": "gno.land/p/nt/poa", + "files": [ + { + "name": "option.gno", + "body": "package poa\n\nimport \"gno.land/p/sys/validators\"\n\ntype Option func(*PoA)\n\n// WithInitialSet sets the initial PoA validator set\nfunc WithInitialSet(validators []validators.Validator) Option {\n\treturn func(p *PoA) {\n\t\tfor _, validator := range validators {\n\t\t\tp.validators.Set(validator.Address.String(), validator)\n\t\t}\n\t}\n}\n" + }, + { + "name": "poa.gno", + "body": "package poa\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar ErrInvalidVotingPower = errors.New(\"invalid voting power\")\n\n// PoA specifies the Proof of Authority validator set, with simple add / remove constraints.\n//\n// To add:\n// - proposed validator must not be part of the set already\n// - proposed validator voting power must be \u003e 0\n//\n// To remove:\n// - proposed validator must be part of the set already\ntype PoA struct {\n\tvalidators *avl.Tree // std.Address -\u003e validators.Validator\n}\n\n// NewPoA creates a new empty Proof of Authority validator set\nfunc NewPoA(opts ...Option) *PoA {\n\t// Create the empty set\n\tp := \u0026PoA{\n\t\tvalidators: avl.NewTree(),\n\t}\n\n\t// Apply the options\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\n\treturn p\n}\n\nfunc (p *PoA) AddValidator(address std.Address, pubKey string, power uint64) (validators.Validator, error) {\n\t// Validate that the operation is a valid call.\n\t// Check if the validator is already in the set\n\tif p.IsValidator(address) {\n\t\treturn validators.Validator{}, validators.ErrValidatorExists\n\t}\n\n\t// Make sure the voting power \u003e 0\n\tif power == 0 {\n\t\treturn validators.Validator{}, ErrInvalidVotingPower\n\t}\n\n\tv := validators.Validator{\n\t\tAddress: address,\n\t\tPubKey: pubKey, // TODO: in the future, verify the public key\n\t\tVotingPower: power,\n\t}\n\n\t// Add the validator to the set\n\tp.validators.Set(address.String(), v)\n\n\treturn v, nil\n}\n\nfunc (p *PoA) RemoveValidator(address std.Address) (validators.Validator, error) {\n\t// Validate that the operation is a valid call\n\t// Fetch the validator\n\tvalidator, err := p.GetValidator(address)\n\tif err != nil {\n\t\treturn validators.Validator{}, err\n\t}\n\n\t// Remove the validator from the set\n\tp.validators.Remove(address.String())\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) IsValidator(address std.Address) bool {\n\t_, exists := p.validators.Get(address.String())\n\n\treturn exists\n}\n\nfunc (p *PoA) GetValidator(address std.Address) (validators.Validator, error) {\n\tvalidatorRaw, exists := p.validators.Get(address.String())\n\tif !exists {\n\t\treturn validators.Validator{}, validators.ErrValidatorMissing\n\t}\n\n\tvalidator := validatorRaw.(validators.Validator)\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) GetValidators() []validators.Validator {\n\tvals := make([]validators.Validator, 0, p.validators.Size())\n\n\tp.validators.Iterate(\"\", \"\", func(_ string, value interface{}) bool {\n\t\tvalidator := value.(validators.Validator)\n\t\tvals = append(vals, validator)\n\n\t\treturn false\n\t})\n\n\treturn vals\n}\n" + }, + { + "name": "poa_test.gno", + "body": "package poa\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/sys/validators\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// generateTestValidators generates a dummy validator set\nfunc generateTestValidators(count int) []validators.Validator {\n\tvals := make([]validators.Validator, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tval := validators.Validator{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"%d\", i)),\n\t\t\tPubKey: \"public-key\",\n\t\t\tVotingPower: 1,\n\t\t}\n\n\t\tvals = append(vals, val)\n\t}\n\n\treturn vals\n}\n\nfunc TestPoA_AddValidator_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validator already in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tproposalKey = \"public-key\"\n\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = proposalAddress\n\t\tinitialSet[0].PubKey = proposalKey\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Attempt to add the validator\n\t\t_, err := p.AddValidator(proposalAddress, proposalKey, 1)\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorExists)\n\t})\n\n\tt.Run(\"invalid voting power\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tproposalKey = \"public-key\"\n\t\t)\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to add the validator\n\t\t_, err := p.AddValidator(proposalAddress, proposalKey, 0)\n\t\tuassert.ErrorIs(t, err, ErrInvalidVotingPower)\n\t})\n}\n\nfunc TestPoA_AddValidator(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\tproposalKey = \"public-key\"\n\t)\n\n\t// Create the protocol with no initial set\n\tp := NewPoA()\n\n\t// Attempt to add the validator\n\t_, err := p.AddValidator(proposalAddress, proposalKey, 1)\n\tuassert.NoError(t, err)\n\n\t// Make sure the validator is added\n\tif !p.IsValidator(proposalAddress) || p.validators.Size() != 1 {\n\t\tt.Fatal(\"address is not validator\")\n\t}\n}\n\nfunc TestPoA_RemoveValidator_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"proposed removal not in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = proposalAddress\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Attempt to remove the validator\n\t\t_, err := p.RemoveValidator(testutils.TestAddress(\"totally random\"))\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorMissing)\n\t})\n}\n\nfunc TestPoA_RemoveValidator(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\tinitialSet = generateTestValidators(1)\n\t)\n\n\tinitialSet[0].Address = proposalAddress\n\n\t// Create the protocol with an initial set\n\tp := NewPoA(WithInitialSet(initialSet))\n\n\t// Attempt to remove the validator\n\t_, err := p.RemoveValidator(proposalAddress)\n\turequire.NoError(t, err)\n\n\t// Make sure the validator is removed\n\tif p.IsValidator(proposalAddress) || p.validators.Size() != 0 {\n\t\tt.Fatal(\"address is validator\")\n\t}\n}\n\nfunc TestPoA_GetValidator(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validator not in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to get the voting power\n\t\t_, err := p.GetValidator(testutils.TestAddress(\"caller\"))\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorMissing)\n\t})\n\n\tt.Run(\"validator fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\taddress = testutils.TestAddress(\"caller\")\n\t\t\tpubKey = \"public-key\"\n\t\t\tvotingPower = uint64(10)\n\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = address\n\t\tinitialSet[0].PubKey = pubKey\n\t\tinitialSet[0].VotingPower = votingPower\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Get the validator\n\t\tval, err := p.GetValidator(address)\n\t\turequire.NoError(t, err)\n\n\t\t// Validate the address\n\t\tif val.Address != address {\n\t\t\tt.Fatal(\"invalid address\")\n\t\t}\n\n\t\t// Validate the voting power\n\t\tif val.VotingPower != votingPower {\n\t\t\tt.Fatal(\"invalid voting power\")\n\t\t}\n\n\t\t// Validate the public key\n\t\tif val.PubKey != pubKey {\n\t\t\tt.Fatal(\"invalid public key\")\n\t\t}\n\t})\n}\n\nfunc TestPoA_GetValidators(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to get the voting power\n\t\tvals := p.GetValidators()\n\n\t\tif len(vals) != 0 {\n\t\t\tt.Fatal(\"validator set is not empty\")\n\t\t}\n\t})\n\n\tt.Run(\"validator set fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tinitialSet := generateTestValidators(10)\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Get the validator set\n\t\tvals := p.GetValidators()\n\n\t\tif len(vals) != len(initialSet) {\n\t\t\tt.Fatal(\"returned validator set mismatch\")\n\t\t}\n\n\t\tfor _, val := range vals {\n\t\t\tfor _, initialVal := range initialSet {\n\t\t\t\tif val.Address != initialVal.Address {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Validate the voting power\n\t\t\t\tuassert.Equal(t, val.VotingPower, initialVal.VotingPower)\n\n\t\t\t\t// Validate the public key\n\t\t\t\tuassert.Equal(t, val.PubKey, initialVal.PubKey)\n\t\t\t}\n\t\t}\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "gnoface", + "path": "gno.land/r/demo/art/gnoface", + "files": [ + { + "name": "gnoface.gno", + "body": "package gnoface\n\nimport (\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Render(path string) string {\n\tseed := uint64(entropy.New().Value())\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\ts, err := strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tseed = uint64(s)\n\t}\n\n\toutput := ufmt.Sprintf(\"Gnoface #%d\\n\", seed)\n\toutput += \"```\\n\" + Draw(seed) + \"```\\n\"\n\treturn output\n}\n\nfunc Draw(seed uint64) string {\n\tvar (\n\t\thairs = []string{\n\t\t\t\" s\",\n\t\t\t\" .......\",\n\t\t\t\" s s s\",\n\t\t\t\" /\\\\ /\\\\\",\n\t\t\t\" |||||||\",\n\t\t}\n\t\theadtop = []string{\n\t\t\t\" /-------\\\\\",\n\t\t\t\" /~~~~~~~\\\\\",\n\t\t\t\" /|||||||\\\\\",\n\t\t\t\" ////////\\\\\",\n\t\t\t\" |||||||||\",\n\t\t\t\" /\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\n\t\t}\n\t\theadspace = []string{\n\t\t\t\" | |\",\n\t\t}\n\t\teyebrow = []string{\n\t\t\t\"~\",\n\t\t\t\"*\",\n\t\t\t\"_\",\n\t\t\t\".\",\n\t\t}\n\t\tear = []string{\n\t\t\t\"o\",\n\t\t\t\" \",\n\t\t\t\"D\",\n\t\t\t\"O\",\n\t\t\t\"\u003c\",\n\t\t\t\"\u003e\",\n\t\t\t\".\",\n\t\t\t\"|\",\n\t\t\t\")\",\n\t\t\t\"(\",\n\t\t}\n\t\teyesmiddle = []string{\n\t\t\t\"| o o |\",\n\t\t\t\"| o _ |\",\n\t\t\t\"| _ o |\",\n\t\t\t\"| . . |\",\n\t\t\t\"| O O |\",\n\t\t\t\"| v v |\",\n\t\t\t\"| X X |\",\n\t\t\t\"| x X |\",\n\t\t\t\"| X D |\",\n\t\t\t\"| ~ ~ |\",\n\t\t}\n\t\tnose = []string{\n\t\t\t\" | o |\",\n\t\t\t\" | O |\",\n\t\t\t\" | V |\",\n\t\t\t\" | L |\",\n\t\t\t\" | C |\",\n\t\t\t\" | ~ |\",\n\t\t\t\" | . . |\",\n\t\t\t\" | . |\",\n\t\t}\n\t\tmouth = []string{\n\t\t\t\" | __/ |\",\n\t\t\t\" | \\\\_/ |\",\n\t\t\t\" | . |\",\n\t\t\t\" | ___ |\",\n\t\t\t\" | ~~~ |\",\n\t\t\t\" | === |\",\n\t\t\t\" | \u003c=\u003e |\",\n\t\t}\n\t\theadbottom = []string{\n\t\t\t\" \\\\-------/\",\n\t\t\t\" \\\\~~~~~~~/\",\n\t\t\t\" \\\\_______/\",\n\t\t}\n\t)\n\n\tr := rand.New(rand.NewPCG(seed, 0xdeadbeef))\n\n\treturn pick(r, hairs) + \"\\n\" +\n\t\tpick(r, headtop) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\t\" | \" + pick(r, eyebrow) + \" \" + pick(r, eyebrow) + \" |\\n\" +\n\t\tpick(r, ear) + pick(r, eyesmiddle) + pick(r, ear) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, nose) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, mouth) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, headbottom) + \"\\n\"\n}\n\nfunc pick(r *rand.Rand, slice []string) string {\n\treturn slice[r.IntN(len(slice))]\n}\n\n// based on https://github.com/moul/pipotron/blob/master/dict/ascii-face.yml\n" + }, + { + "name": "gnoface_test.gno", + "body": "package gnoface\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestDraw(t *testing.T) {\n\tcases := []struct {\n\t\tseed uint64\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tseed: 42,\n\t\t\texpected: `\n |||||||\n |||||||||\n | |\n | . ~ |\n)| v v |O\n | |\n | L |\n | |\n | ___ |\n | |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 1337,\n\t\t\texpected: `\n .......\n |||||||||\n | |\n | . _ |\nD| x X |O\n | |\n | ~ |\n | |\n | ~~~ |\n | |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 123456789,\n\t\t\texpected: `\n .......\n ////////\\\n | |\n | ~ * |\n|| x X |o\n | |\n | V |\n | |\n | . |\n | |\n \\-------/\n`[1:],\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tname := ufmt.Sprintf(\"%d\", tc.seed)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Draw(tc.seed)\n\t\t\tuassert.Equal(t, string(tc.expected), got)\n\t\t})\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"42\",\n\t\t\texpected: \"Gnoface #42\\n```\" + `\n |||||||\n |||||||||\n | |\n | . ~ |\n)| v v |O\n | |\n | L |\n | |\n | ___ |\n | |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"1337\",\n\t\t\texpected: \"Gnoface #1337\\n```\" + `\n .......\n |||||||||\n | |\n | . _ |\nD| x X |O\n | |\n | ~ |\n | |\n | ~~~ |\n | |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"123456789\",\n\t\t\texpected: \"Gnoface #123456789\\n```\" + `\n .......\n ////////\\\n | |\n | ~ * |\n|| x X |o\n | |\n | V |\n | |\n | . |\n | |\n \\-------/\n` + \"```\\n\",\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "millipede", + "path": "gno.land/r/demo/art/millipede", + "files": [ + { + "name": "millipede.gno", + "body": "package millipede\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tminSize = 1\n\tdefaultSize = 20\n\tmaxSize = 100\n)\n\nfunc Draw(size int) string {\n\tif size \u003c minSize || size \u003e maxSize {\n\t\tpanic(\"invalid millipede size\")\n\t}\n\tpaddings := []string{\" \", \" \", \"\", \" \", \" \", \" \", \" \", \" \", \" \"}\n\tvar b strings.Builder\n\tb.WriteString(\" ╚⊙ ⊙╝\\n\")\n\tfor i := 0; i \u003c size; i++ {\n\t\tb.WriteString(paddings[i%9] + \"╚═(███)═╝\\n\")\n\t}\n\treturn b.String()\n}\n\nfunc Render(path string) string {\n\tsize := defaultSize\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\tvar err error\n\t\tsize, err = strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\toutput := \"```\\n\" + Draw(size) + \"```\\n\"\n\tif size \u003e minSize {\n\t\toutput += ufmt.Sprintf(\"[%d](/r/demo/art/millipede:%d)\u003c \", size-1, size-1)\n\t}\n\tif size \u003c maxSize {\n\t\toutput += ufmt.Sprintf(\" \u003e[%d](/r/demo/art/millipede:%d)\", size+1, size+1)\n\t}\n\treturn output\n}\n\n// based on https://github.com/getmillipede/millipede-go/blob/977f046c39c35a650eac0fd30245e96b22c7803c/main.go\n" + }, + { + "name": "millipede_test.gno", + "body": "package millipede\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"\",\n\t\t\texpected: \"```\" + `\n ╚⊙ ⊙╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[19](/r/demo/art/millipede:19)\u003c \u003e[21](/r/demo/art/millipede:21)\",\n\t\t},\n\t\t{\n\t\t\tpath: \"4\",\n\t\t\texpected: \"```\" + `\n ╚⊙ ⊙╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[3](/r/demo/art/millipede:3)\u003c \u003e[5](/r/demo/art/millipede:5)\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "banktest", + "path": "gno.land/r/demo/banktest", + "files": [ + { + "name": "README.md", + "body": "This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.go](/r/demo/banktest/banktest.go) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n \"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e Self explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n caller std.Address\n sent std.Coins\n returned std.Coins\n time time.Time\n}\n\nfunc (act *activity) String() string {\n return act.caller.String() + \" \" +\n act.sent.String() + \" sent, \" +\n act.returned.String() + \" returned, at \" +\n act.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract. Notice that the \"latest\" variable is defined \"globally\" within the context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package are encapsulated within this \"data realm\", where the data is mutated based on transactions that can potentially cross many realm and non-realm package boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n std.AssertOriginCall()\n caller := std.GetOrigCaller()\n send := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named \"Deposit\". `std.AssertOriginCall() asserts that this function was called by a gno transactional Message. The caller is the user who signed off on this transactional message. Send is the amount of deposit sent along with this message.\n\n```go\n // record activity\n act := \u0026activity{\n caller: caller,\n sent: std.GetOrigSend(),\n returned: send,\n time: time.Now(),\n }\n for i := len(latest) - 2; i \u003e= 0; i-- {\n latest[i+1] = latest[i] // shift by +1.\n }\n latest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n // return if any.\n if returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n banker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n pkgaddr := std.GetOrigPkgAddr()\n // TODO: use std.Coins constructors, this isn't generally safe.\n banker.SendCoins(pkgaddr, caller, send)\n return \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n // get realm coins.\n banker := std.GetBanker(std.BankerTypeReadonly)\n coins := banker.GetCoins(std.GetOrigPkgAddr())\n\n // render\n res := \"\"\n res += \"## recent activity\\n\"\n res += \"\\n\"\n for _, act := range latest {\n if act == nil {\n break\n }\n res += \" * \" + act.String() + \"\\n\"\n }\n res += \"\\n\"\n res += \"## total deposits\\n\"\n res += coins.String()\n return res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:gnolang/4).\n" + }, + { + "name": "banktest.gno", + "body": "package banktest\n\nimport (\n\t\"std\"\n\t\"time\"\n)\n\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime time.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tact.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: time.Now(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n\t// return if any.\n\tif returnAmount \u003e 0 {\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n\t} else {\n\t\treturn \"thank you!\"\n\t}\n}\n\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\n// SEND: 100000000ugnot\n\npackage bank1\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\t// set up main address and banktest addr.\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\tstd.TestSetOrigCaller(mainaddr)\n\tstd.TestSetOrigPkgAddr(banktestAddr)\n\n\t// get and print balance of mainaddr.\n\t// with the SEND, + 200 gnot given by the TestContext, main should have 300gnot.\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\t// simulate a Deposit call. use Send + OrigSend to simulate -send.\n\tbanker.SendCoins(mainaddr, banktestAddr, std.Coins{{\"ugnot\", 100_000_000}})\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 100_000_000}}, nil)\n\tres := banktest.Deposit(\"ugnot\", 50_000_000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n\n\t// simulate a Render(). banker should have given back all coins.\n\tres = banktest.Render(\"\")\n\tprintln(res)\n}\n\n// Output:\n// main before: 300000000ugnot\n// Deposit(): returned!\n// main after: 250000000ugnot\n// ## recent activity\n//\n// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 50000000ugnot\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\t// simulate a Deposit call.\n\tstd.TestSetOrigPkgAddr(banktestAddr)\n\tstd.TestIssueCoins(banktestAddr, std.Coins{{\"ugnot\", 100000000}})\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 100000000}}, nil)\n\tres := banktest.Deposit(\"ugnot\", 101000000)\n\tprintln(res)\n}\n\n// Error:\n// cannot send \"101000000ugnot\", limit \"100000000ugnot\" exceeded with \"\" already spent\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\t// print main balance before.\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal) // plus OrigSend equals 300.\n\n\t// simulate a Deposit call.\n\tstd.TestSetOrigPkgAddr(banktestAddr)\n\tstd.TestIssueCoins(banktestAddr, std.Coins{{\"ugnot\", 100000000}})\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 100000000}}, nil)\n\tres := banktest.Deposit(\"ugnot\", 55000000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal) // now 255.\n\n\t// simulate a Render().\n\tres = banktest.Render(\"\")\n\tprintln(res)\n}\n\n// Output:\n// main before: 200000000ugnot\n// Deposit(): returned!\n// main after: 255000000ugnot\n// ## recent activity\n//\n// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 45000000ugnot\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tsend := std.Coins{{\"ugnot\", 123}}\n\tbanker.SendCoins(banktestAddr, mainaddr, send)\n\n}\n\n// Error:\n// can only send coins from realm that created banker \"g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk\", not \"g1dv3435088tlrgggf745kaud0ptrkc9v42k8llz\"\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "bar20", + "path": "gno.land/r/demo/bar20", + "files": [ + { + "name": "bar20.gno", + "body": "// Package bar20 is similar to gno.land/r/demo/foo20 but exposes a safe-object\n// that can be used by `maketx run`, another contract importing foo20, and in\n// the future when we'll support `maketx call Token.XXX`.\npackage bar20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tbanker *grc20.Banker // private banker.\n\tToken grc20.Token // public safe-object.\n)\n\nfunc init() {\n\tbanker = grc20.NewBanker(\"Bar\", \"BAR\", 4)\n\tToken = banker.Token()\n}\n\nfunc Faucet() string {\n\tcaller := std.PrevRealm().Addr()\n\tif err := banker.Mint(caller, 1_000_000); err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\treturn \"OK\"\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn banker.RenderHome() // XXX: should be Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := std.Address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n" + }, + { + "name": "bar20_test.gno", + "body": "package bar20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestPackage(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // XXX: should not need this\n\n\turequire.Equal(t, Token.BalanceOf(alice), uint64(0))\n\turequire.Equal(t, Faucet(), \"OK\")\n\turequire.Equal(t, Token.BalanceOf(alice), uint64(1_000_000))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "counter", + "path": "gno.land/r/demo/counter", + "files": [ + { + "name": "counter.gno", + "body": "package counter\n\nimport \"strconv\"\n\nvar counter int\n\nfunc Increment() int {\n\tcounter++\n\treturn counter\n}\n\nfunc Render(_ string) string {\n\treturn strconv.Itoa(counter)\n}\n" + }, + { + "name": "counter_test.gno", + "body": "package counter\n\nimport \"testing\"\n\nfunc TestIncrement(t *testing.T) {\n\tcounter = 0\n\tval := Increment()\n\tif val != 1 {\n\t\tt.Fatalf(\"result from Increment(): %d != 1\", val)\n\t}\n\tif counter != val {\n\t\tt.Fatalf(\"counter (%d) != val (%d)\", counter, val)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcounter = 1337\n\tres := Render(\"\")\n\tif res != \"1337\" {\n\t\tt.Fatalf(\"render result %q != %q\", res, \"1337\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "govdao", + "path": "gno.land/r/gov/dao/v2", + "files": [ + { + "name": "dao.gno", + "body": "package govdao\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/simpledao\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\td *simpledao.SimpleDAO // the current active DAO implementation\n\tmembers membstore.MemberStore // the member store\n)\n\nfunc init() {\n\tvar (\n\t\tset = []membstore.Member{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"), // Jae\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"), // Manfred\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1e6gxg5tvc55mwsn7t7dymmlasratv7mkv0rap2\"), // Milos\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1jazghxvvgz3egnr2fc8uf72z4g0l03596y9ls7\"), // Nemanja\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1qhskthp2uycmg4zsdc9squ2jds7yv3t0qyrlnp\"), // Petar\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g18amm3fc00t43dcxsys6udug0czyvqt9e7p23rd\"), // Marc\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1dfr24yhk5ztwtqn2a36m8f6ud8cx5hww4dkjfl\"), // Antonio\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g19p3yzr3cuhzqa02j0ce6kzvyjqfzwemw3vam0x\"), // Guilhem\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1mx4pum9976th863jgry4sdjzfwu03qan5w2v9j\"), // Ray\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g127l4gkhk0emwsx5tmxe96sp86c05h8vg5tufzq\"), // Maxwell\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1cpx59z5r8vzeww2fm4ezpz7yvjs7kptywkm864\"), // Morgan\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1ker4vvggvsyatexxn3hkthp2hu80pkhrwmuczr\"), // Sergio\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g18x425qmujg99cfz3q97y4uep5pxjq3z8lmpt25\"), // Antoine\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t// GNO DEVX\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g16tfrrul20g4jzt3z303raqw8vs8s2pqqh5clwu\"), // Ilker\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun\"), // Jerónimo\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g15ruzptpql4dpuyzej0wkt5rq6r26kw4nxu9fwd\"), // Denis\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1dnllrdzwfhxv3evyk09y48mgn5phfjvtyrlzm7\"), // Danny\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1778y2yphxs2wpuaflsy5y9qwcd4gttn4g5yjx5\"), // Michelle\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1mq7g0jszdmn4qdpc9tq94w0gyex37su892n80m\"), // Alan\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g197q5e9v00vuz256ly7fq7v3ekaun5cr7wmjgfh\"), // Salvo\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1mpkp5lm8lwpm0pym4388836d009zfe4maxlqsq\"), // Alexis\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"), // Leon\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1whzkakk4hzjkvy60d5pwfk484xu67ar2cl62h2\"), // Kirk\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t// AiB\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr\"), // Albert\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t// ONBLOC\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g12vx7dn3dqq89mz550zwunvg4qw6epq73d9csay\"), // Dongwon\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1r04aw56fgvzy859fachr8hzzhqkulkaemltr76\"), // Blake\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g17n4y745s08awwq4e0a38lagsgtntna0749tnxe\"), // Jinwoo\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1ckae7tc5sez8ul3ssne75sk4muwgttp6ks2ky9\"), // ByeongJun\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t// TERITORI\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a\"), // Norman\n\t\t\t\tVotingPower: 5,\n\t\t\t},\n\t\t\t// BERTY\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1qynsu9dwj9lq0m5fkje7jh6qy3md80ztqnshhm\"), // Rémi\n\t\t\t\tVotingPower: 5,\n\t\t\t},\n\t\t\t// FLIPPANDO / ZENTASKTIC\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3\"), // Dragos\n\t\t\t\tVotingPower: 5,\n\t\t\t},\n\t\t}\n\t)\n\n\t// Set the member store\n\tmembers = membstore.NewMembStore(membstore.WithInitialMembers(set))\n\n\t// Set the DAO implementation\n\td = simpledao.New(members)\n}\n\n// Propose is designed to be called by another contract or with\n// `maketx run`, not by a `maketx call`.\nfunc Propose(request dao.ProposalRequest) uint64 {\n\tidx, err := d.Propose(request)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn idx\n}\n\n// VoteOnProposal casts a vote for the given proposal\nfunc VoteOnProposal(id uint64, option dao.VoteOption) {\n\tif err := d.VoteOnProposal(id, option); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// ExecuteProposal executes the proposal\nfunc ExecuteProposal(id uint64) {\n\tif err := d.ExecuteProposal(id); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetPropStore returns the active proposal store\nfunc GetPropStore() dao.PropStore {\n\treturn d\n}\n\n// GetMembStore returns the active member store\nfunc GetMembStore() membstore.MemberStore {\n\treturn members\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tnumProposals := d.Size()\n\n\t\tif numProposals == 0 {\n\t\t\treturn \"No proposals found :(\" // corner case\n\t\t}\n\n\t\toutput := \"\"\n\n\t\toffset := uint64(0)\n\t\tif numProposals \u003e= 10 {\n\t\t\toffset = uint64(numProposals) - 10\n\t\t}\n\n\t\t// Fetch the last 10 proposals\n\t\tfor idx, prop := range d.Proposals(offset, uint64(10)) {\n\t\t\toutput += ufmt.Sprintf(\n\t\t\t\t\"- [Proposal #%d](%s:%d) - (**%s**)(by %s)\\n\",\n\t\t\t\tidx,\n\t\t\t\t\"/r/gov/dao/v2\",\n\t\t\t\tidx,\n\t\t\t\tprop.Status().String(),\n\t\t\t\tprop.Author().String(),\n\t\t\t)\n\t\t}\n\n\t\treturn output\n\t}\n\n\t// Display the detailed proposal\n\tidx, err := strconv.Atoi(path)\n\tif err != nil {\n\t\treturn \"404: Invalid proposal ID\"\n\t}\n\n\t// Fetch the proposal\n\tprop, err := d.ProposalByID(uint64(idx))\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"unable to fetch proposal, %s\", err.Error())\n\t}\n\n\t// Render the proposal\n\toutput := \"\"\n\toutput += ufmt.Sprintf(\"# Prop #%d\", idx)\n\toutput += \"\\n\\n\"\n\toutput += prop.Render()\n\toutput += \"\\n\\n\"\n\n\treturn output\n}\n" + }, + { + "name": "poc.gno", + "body": "package govdao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/combinederr\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/gov/executor\"\n)\n\nvar errNoChangesProposed = errors.New(\"no set changes proposed\")\n\n// NewGovDAOExecutor creates the govdao wrapped callback executor\nfunc NewGovDAOExecutor(cb func() error) dao.Executor {\n\tif cb == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\treturn executor.NewCallbackExecutor(\n\t\tcb,\n\t\tstd.CurrentRealm().PkgPath(),\n\t)\n}\n\n// NewMemberPropExecutor returns the GOVDAO member change executor\nfunc NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor {\n\tif changesFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\terrs := \u0026combinederr.CombinedError{}\n\t\tcbMembers := changesFn()\n\n\t\tfor _, member := range cbMembers {\n\t\t\tswitch {\n\t\t\tcase !members.IsMember(member.Address):\n\t\t\t\t// Addition request\n\t\t\t\terr := members.AddMember(member)\n\n\t\t\t\terrs.Add(err)\n\t\t\tcase member.VotingPower == 0:\n\t\t\t\t// Remove request\n\t\t\t\terr := members.UpdateMember(member.Address, membstore.Member{\n\t\t\t\t\tAddress: member.Address,\n\t\t\t\t\tVotingPower: 0, // 0 indicated removal\n\t\t\t\t})\n\n\t\t\t\terrs.Add(err)\n\t\t\tdefault:\n\t\t\t\t// Update request\n\t\t\t\terr := members.UpdateMember(member.Address, member)\n\n\t\t\t\terrs.Add(err)\n\t\t\t}\n\t\t}\n\n\t\t// Check if there were any execution errors\n\t\tif errs.Size() == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errs\n\t}\n\n\treturn NewGovDAOExecutor(callback)\n}\n\nfunc NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor {\n\tif changeFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\tsetMembStoreImpl(changeFn())\n\n\t\treturn nil\n\t}\n\n\treturn NewGovDAOExecutor(callback)\n}\n\n// setMembStoreImpl sets a new dao.MembStore implementation\nfunc setMembStoreImpl(impl membstore.MemberStore) {\n\tif impl == nil {\n\t\tpanic(\"invalid member store\")\n\t}\n\n\tmembers = impl\n}\n" + }, + { + "name": "prop1_filetest.gno", + "body": "// Please note that this package is intended for demonstration purposes only.\n// You could execute this code (the init part) by running a `maketx run` command\n// or by uploading a similar package to a personal namespace.\n//\n// For the specific case of validators, a `r/gnoland/valopers` will be used to\n// organize the lifecycle of validators (register, etc), and this more complex\n// contract will be responsible to generate proposals.\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\tpVals \"gno.land/p/sys/validators\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n\tvalidators \"gno.land/r/sys/validators/v2\"\n)\n\nfunc init() {\n\tchangesFn := func() []pVals.Validator {\n\t\treturn []pVals.Validator{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g12345678\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 10, // add a new validator\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g000000000\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 10, // add a new validator\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g000000000\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 0, // remove an existing validator\n\t\t\t},\n\t\t}\n\t}\n\n\t// Wraps changesFn to emit a certified event only if executed from a\n\t// complete governance proposal process.\n\texecutor := validators.NewPropExecutor(changesFn)\n\n\t// Create a proposal\n\tdescription := \"manual valset changes proposal example\"\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\tgovdao.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.VoteOnProposal(0, dao.YesVote)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(validators.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(validators.Render(\"\"))\n}\n\n// Output:\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// manual valset changes proposal example\n//\n// Status: active\n//\n// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)\n//\n// Threshold met: false\n//\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// manual valset changes proposal example\n//\n// Status: accepted\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// No valset changes to apply.\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// manual valset changes proposal example\n//\n// Status: execution successful\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// Valset changes:\n// - #123: g12345678 (10)\n// - #123: g000000000 (10)\n// - #123: g000000000 (0)\n" + }, + { + "name": "prop2_filetest.gno", + "body": "package main\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/dao\"\n\tgnoblog \"gno.land/r/gnoland/blog\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tex := gnoblog.NewPostExecutor(\n\t\t\"hello-from-govdao\", // slug\n\t\t\"Hello from GovDAO!\", // title\n\t\t\"This post was published by a GovDAO proposal.\", // body\n\t\ttime.Now().Format(time.RFC3339), // publication date\n\t\t\"moul\", // authors\n\t\t\"govdao,example\", // tags\n\t)\n\n\t// Create a proposal\n\tdescription := \"post a new blogpost about govdao\"\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: ex,\n\t}\n\n\tgovdao.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(gnoblog.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(gnoblog.Render(\"\"))\n}\n\n// Output:\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// post a new blogpost about govdao\n//\n// Status: active\n//\n// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)\n//\n// Threshold met: false\n//\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// post a new blogpost about govdao\n//\n// Status: accepted\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// # Gnoland's Blog\n//\n// No posts.\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// post a new blogpost about govdao\n//\n// Status: execution successful\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// # Gnoland's Blog\n//\n// \u003cdiv class='columns-3'\u003e\u003cdiv\u003e\n//\n// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao)\n// 13 Feb 2009\n// \u003c/div\u003e\u003c/div\u003e\n" + }, + { + "name": "prop3_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tmemberFn := func() []membstore.Member {\n\t\treturn []membstore.Member{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g123\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g456\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g789\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Create a proposal\n\tdescription := \"add new members to the govdao\"\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: govdao.NewMemberPropExecutor(memberFn),\n\t}\n\n\tgovdao.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.GetMembStore().Size())\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.GetMembStore().Size())\n}\n\n// Output:\n// --\n// 1\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// add new members to the govdao\n//\n// Status: active\n//\n// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)\n//\n// Threshold met: false\n//\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// add new members to the govdao\n//\n// Status: accepted\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// add new members to the govdao\n//\n// Status: execution successful\n//\n// Voting stats: YES 10 (25%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 30 (75%)\n//\n// Threshold met: false\n//\n//\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// 4\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "bridge", + "path": "gno.land/r/gov/dao/bridge", + "files": [ + { + "name": "bridge.gno", + "body": "package bridge\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\nconst initialOwner = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\") // @moul\n\nvar b *Bridge\n\n// Bridge is the active GovDAO\n// implementation bridge\ntype Bridge struct {\n\t*ownable.Ownable\n\n\tdao DAO\n}\n\n// init constructs the initial GovDAO implementation\nfunc init() {\n\tb = \u0026Bridge{\n\t\tOwnable: ownable.NewWithAddress(initialOwner),\n\t\tdao: \u0026govdaoV2{},\n\t}\n}\n\n// SetDAO sets the currently active GovDAO implementation\nfunc SetDAO(dao DAO) {\n\tb.AssertCallerIsOwner()\n\n\tb.dao = dao\n}\n\n// GovDAO returns the current GovDAO implementation\nfunc GovDAO() DAO {\n\treturn b.dao\n}\n" + }, + { + "name": "bridge_test.gno", + "body": "package bridge\n\nimport (\n\t\"testing\"\n\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestBridge_DAO(t *testing.T) {\n\tvar (\n\t\tproposalID = uint64(10)\n\t\tmockDAO = \u0026mockDAO{\n\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\treturn proposalID\n\t\t\t},\n\t\t}\n\t)\n\n\tb.dao = mockDAO\n\n\tuassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{}))\n}\n\nfunc TestBridge_SetDAO(t *testing.T) {\n\tt.Run(\"invalid owner\", func(t *testing.T) {\n\t\t// Attempt to set a new DAO implementation\n\t\tuassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() {\n\t\t\tSetDAO(\u0026mockDAO{})\n\t\t})\n\t})\n\n\tt.Run(\"valid owner\", func(t *testing.T) {\n\t\tvar (\n\t\t\taddr = testutils.TestAddress(\"owner\")\n\n\t\t\tproposalID = uint64(10)\n\t\t\tmockDAO = \u0026mockDAO{\n\t\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\t\treturn proposalID\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(addr)\n\n\t\tb.Ownable = ownable.NewWithAddress(addr)\n\n\t\turequire.NotPanics(t, func() {\n\t\t\tSetDAO(mockDAO)\n\t\t})\n\n\t\tuassert.Equal(\n\t\t\tt,\n\t\t\tmockDAO.Propose(dao.ProposalRequest{}),\n\t\t\tGovDAO().Propose(dao.ProposalRequest{}),\n\t\t)\n\t})\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package bridge represents a GovDAO implementation wrapper, used by other Realms and Packages to\n// always fetch the most active GovDAO implementation, instead of directly referencing it, and having to\n// update it each time the GovDAO implementation changes\npackage bridge\n" + }, + { + "name": "mock_test.gno", + "body": "package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\ntype (\n\tproposeDelegate func(dao.ProposalRequest) uint64\n\tvoteOnProposalDelegate func(uint64, dao.VoteOption)\n\texecuteProposalDelegate func(uint64)\n\tgetPropStoreDelegate func() dao.PropStore\n\tgetMembStoreDelegate func() membstore.MemberStore\n\tnewGovDAOExecutorDelegate func(func() error) dao.Executor\n)\n\ntype mockDAO struct {\n\tproposeFn proposeDelegate\n\tvoteOnProposalFn voteOnProposalDelegate\n\texecuteProposalFn executeProposalDelegate\n\tgetPropStoreFn getPropStoreDelegate\n\tgetMembStoreFn getMembStoreDelegate\n\tnewGovDAOExecutorFn newGovDAOExecutorDelegate\n}\n\nfunc (m *mockDAO) Propose(request dao.ProposalRequest) uint64 {\n\tif m.proposeFn != nil {\n\t\treturn m.proposeFn(request)\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockDAO) VoteOnProposal(id uint64, option dao.VoteOption) {\n\tif m.voteOnProposalFn != nil {\n\t\tm.voteOnProposalFn(id, option)\n\t}\n}\n\nfunc (m *mockDAO) ExecuteProposal(id uint64) {\n\tif m.executeProposalFn != nil {\n\t\tm.executeProposalFn(id)\n\t}\n}\n\nfunc (m *mockDAO) GetPropStore() dao.PropStore {\n\tif m.getPropStoreFn != nil {\n\t\treturn m.getPropStoreFn()\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockDAO) GetMembStore() membstore.MemberStore {\n\tif m.getMembStoreFn != nil {\n\t\treturn m.getMembStoreFn()\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockDAO) NewGovDAOExecutor(cb func() error) dao.Executor {\n\tif m.newGovDAOExecutorFn != nil {\n\t\treturn m.newGovDAOExecutorFn(cb)\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "types.gno", + "body": "package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\n// DAO abstracts the commonly used DAO interface\ntype DAO interface {\n\tPropose(dao.ProposalRequest) uint64\n\tVoteOnProposal(uint64, dao.VoteOption)\n\tExecuteProposal(uint64)\n\tGetPropStore() dao.PropStore\n\tGetMembStore() membstore.MemberStore\n\n\tNewGovDAOExecutor(func() error) dao.Executor\n}\n" + }, + { + "name": "v2.gno", + "body": "package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\n// govdaoV2 is a wrapper for interacting with the /r/gov/dao/v2 Realm\ntype govdaoV2 struct{}\n\nfunc (g *govdaoV2) Propose(request dao.ProposalRequest) uint64 {\n\treturn govdao.Propose(request)\n}\n\nfunc (g *govdaoV2) VoteOnProposal(id uint64, option dao.VoteOption) {\n\tgovdao.VoteOnProposal(id, option)\n}\n\nfunc (g *govdaoV2) ExecuteProposal(id uint64) {\n\tgovdao.ExecuteProposal(id)\n}\n\nfunc (g *govdaoV2) GetPropStore() dao.PropStore {\n\treturn govdao.GetPropStore()\n}\n\nfunc (g *govdaoV2) GetMembStore() membstore.MemberStore {\n\treturn govdao.GetMembStore()\n}\n\nfunc (g *govdaoV2) NewGovDAOExecutor(cb func() error) dao.Executor {\n\treturn govdao.NewGovDAOExecutor(cb)\n}\n\nfunc (g *govdaoV2) NewMemberPropExecutor(cb func() []membstore.Member) dao.Executor {\n\treturn govdao.NewMemberPropExecutor(cb)\n}\n\nfunc (g *govdaoV2) NewMembStoreImplExecutor(cb func() membstore.MemberStore) dao.Executor {\n\treturn govdao.NewMembStoreImplExecutor(cb)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "daoweb", + "path": "gno.land/r/demo/daoweb", + "files": [ + { + "name": "daoweb.gno", + "body": "package daoweb\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/json\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\n// Proposals returns the paginated GovDAO proposals\nfunc Proposals(offset, count uint64) string {\n\tvar (\n\t\tpropStore = bridge.GovDAO().GetPropStore()\n\t\tsize = propStore.Size()\n\t)\n\n\t// Get the props\n\tprops := propStore.Proposals(offset, count)\n\n\tresp := ProposalsResponse{\n\t\tProposals: make([]Proposal, 0, count),\n\t\tTotal: uint64(size),\n\t}\n\n\tfor _, p := range props {\n\t\tprop := Proposal{\n\t\t\tAuthor: p.Author(),\n\t\t\tDescription: p.Description(),\n\t\t\tStatus: p.Status(),\n\t\t\tStats: p.Stats(),\n\t\t\tIsExpired: p.IsExpired(),\n\t\t}\n\n\t\tresp.Proposals = append(resp.Proposals, prop)\n\t}\n\n\t// Encode the response into JSON\n\tencodedProps, err := json.Marshal(encodeProposalsResponse(resp))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encodedProps)\n}\n\n// ProposalByID fetches the proposal using the given ID\nfunc ProposalByID(id uint64) string {\n\tpropStore := bridge.GovDAO().GetPropStore()\n\n\tp, err := propStore.ProposalByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Encode the response into JSON\n\tprop := Proposal{\n\t\tAuthor: p.Author(),\n\t\tDescription: p.Description(),\n\t\tStatus: p.Status(),\n\t\tStats: p.Stats(),\n\t\tIsExpired: p.IsExpired(),\n\t}\n\n\tencodedProp, err := json.Marshal(encodeProposal(prop))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encodedProp)\n}\n\n// encodeProposal encodes a proposal into a json node\nfunc encodeProposal(p Proposal) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"author\": json.StringNode(\"author\", p.Author.String()),\n\t\t\"description\": json.StringNode(\"description\", p.Description),\n\t\t\"status\": json.StringNode(\"status\", p.Status.String()),\n\t\t\"stats\": json.ObjectNode(\"stats\", map[string]*json.Node{\n\t\t\t\"yay_votes\": json.NumberNode(\"yay_votes\", float64(p.Stats.YayVotes)),\n\t\t\t\"nay_votes\": json.NumberNode(\"nay_votes\", float64(p.Stats.NayVotes)),\n\t\t\t\"abstain_votes\": json.NumberNode(\"abstain_votes\", float64(p.Stats.AbstainVotes)),\n\t\t\t\"total_voting_power\": json.NumberNode(\"total_voting_power\", float64(p.Stats.TotalVotingPower)),\n\t\t}),\n\t\t\"is_expired\": json.BoolNode(\"is_expired\", p.IsExpired),\n\t})\n}\n\n// encodeProposalsResponse encodes a proposal response into a JSON node\nfunc encodeProposalsResponse(props ProposalsResponse) *json.Node {\n\tproposals := make([]*json.Node, 0, len(props.Proposals))\n\n\tfor _, p := range props.Proposals {\n\t\tproposals = append(proposals, encodeProposal(p))\n\t}\n\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"proposals\": json.ArrayNode(\"proposals\", proposals),\n\t\t\"total\": json.NumberNode(\"total\", float64(props.Total)),\n\t})\n}\n\n// ProposalsResponse is a paginated proposal response\ntype ProposalsResponse struct {\n\tProposals []Proposal `json:\"proposals\"`\n\tTotal uint64 `json:\"total\"`\n}\n\n// Proposal is a single GovDAO proposal\ntype Proposal struct {\n\tAuthor std.Address `json:\"author\"`\n\tDescription string `json:\"description\"`\n\tStatus dao.ProposalStatus `json:\"status\"`\n\tStats dao.Stats `json:\"stats\"`\n\tIsExpired bool `json:\"is_expired\"`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "deep", + "path": "gno.land/r/demo/deep/very/deep", + "files": [ + { + "name": "render.gno", + "body": "package deep\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn \"it works!\"\n\t} else {\n\t\treturn \"hi \" + path\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "foo20", + "path": "gno.land/r/demo/grc20factory", + "files": [ + { + "name": "grc20factory.gno", + "body": "package foo20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar instances avl.Tree // symbol -\u003e instance\n\nfunc New(name, symbol string, decimals uint, initialMint, faucet uint64) {\n\tcaller := std.PrevRealm().Addr()\n\tNewWithAdmin(name, symbol, decimals, initialMint, faucet, caller)\n}\n\nfunc NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64, admin std.Address) {\n\texists := instances.Has(symbol)\n\tif exists {\n\t\tpanic(\"token already exists\")\n\t}\n\n\tbanker := grc20.NewBanker(name, symbol, decimals)\n\tif initialMint \u003e 0 {\n\t\tbanker.Mint(admin, initialMint)\n\t}\n\n\tinst := instance{\n\t\tbanker: banker,\n\t\tadmin: ownable.NewWithAddress(admin),\n\t\tfaucet: faucet,\n\t}\n\n\tinstances.Set(symbol, \u0026inst)\n}\n\ntype instance struct {\n\tbanker *grc20.Banker\n\tadmin *ownable.Ownable\n\tfaucet uint64 // per-request amount. disabled if 0.\n}\n\nfunc (inst instance) Token() grc20.Token { return inst.banker.Token() }\n\nfunc TotalSupply(symbol string) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.Token().TotalSupply()\n}\n\nfunc BalanceOf(symbol string, owner std.Address) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.Token().BalanceOf(owner)\n}\n\nfunc Allowance(symbol string, owner, spender std.Address) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.Token().Allowance(owner, spender)\n}\n\nfunc Transfer(symbol string, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.Token().Transfer(to, amount))\n}\n\nfunc Approve(symbol string, spender std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.Token().Approve(spender, amount))\n}\n\nfunc TransferFrom(symbol string, from, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.Token().TransferFrom(from, to, amount))\n}\n\n// faucet.\nfunc Faucet(symbol string) {\n\tinst := mustGetInstance(symbol)\n\tif inst.faucet == 0 {\n\t\tpanic(\"faucet disabled for this token\")\n\t}\n\t// FIXME: add limits?\n\t// FIXME: add payment in gnot?\n\tcaller := std.PrevRealm().Addr()\n\tcheckErr(inst.banker.Mint(caller, inst.faucet))\n}\n\nfunc Mint(symbol string, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertCallerIsOwner()\n\tcheckErr(inst.banker.Mint(to, amount))\n}\n\nfunc Burn(symbol string, from std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertCallerIsOwner()\n\tcheckErr(inst.banker.Burn(from, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn \"TODO: list existing tokens and admins\"\n\tcase c == 1:\n\t\tsymbol := parts[0]\n\t\tinst := mustGetInstance(symbol)\n\t\treturn inst.banker.RenderHome()\n\tcase c == 3 \u0026\u0026 parts[1] == \"balance\":\n\t\tsymbol := parts[0]\n\t\tinst := mustGetInstance(symbol)\n\t\towner := std.Address(parts[2])\n\t\tbalance := inst.Token().BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\nfunc mustGetInstance(symbol string) *instance {\n\tt, exists := instances.Get(symbol)\n\tif !exists {\n\t\tpanic(\"token instance does not exist\")\n\t}\n\treturn t.(*instance)\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + }, + { + "name": "grc20factory_test.gno", + "body": "package foo20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestReadOnlyPublicMethods(t *testing.T) {\n\tadmin := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\tmanfred := std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\tunknown := std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // valid but never used.\n\tNewWithAdmin(\"Foo\", \"FOO\", 4, 10_000*1_000_000, 0, admin)\n\tNewWithAdmin(\"Bar\", \"BAR\", 4, 10_000*1_000, 0, admin)\n\tmustGetInstance(\"FOO\").banker.Mint(manfred, 100_000_000)\n\n\ttype test struct {\n\t\tname string\n\t\tbalance uint64\n\t\tfn func() uint64\n\t}\n\n\t// check balances #1.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_100_000_000, func() uint64 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(manfred)\", 100_000_000, func() uint64 { return BalanceOf(\"FOO\", manfred) }},\n\t\t\t{\"Allowance(admin, manfred)\", 0, func() uint64 { return Allowance(\"FOO\", admin, manfred) }},\n\t\t\t{\"BalanceOf(unknown)\", 0, func() uint64 { return BalanceOf(\"FOO\", unknown) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tuassert.Equal(t, tc.balance, tc.fn(), \"balance does not match\")\n\t\t}\n\t}\n\treturn\n\n\t// unknown uses the faucet.\n\tstd.TestSetOrigCaller(unknown)\n\tFaucet(\"FOO\")\n\n\t// check balances #2.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_110_000_000, func() uint64 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(manfred)\", 100_000_000, func() uint64 { return BalanceOf(\"FOO\", manfred) }},\n\t\t\t{\"Allowance(admin, manfred)\", 0, func() uint64 { return Allowance(\"FOO\", admin, manfred) }},\n\t\t\t{\"BalanceOf(unknown)\", 10_000_000, func() uint64 { return BalanceOf(\"FOO\", unknown) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tuassert.Equal(t, tc.balance, tc.fn(), \"balance does not match\")\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "disperse", + "path": "gno.land/r/demo/disperse", + "files": [ + { + "name": "disperse.gno", + "body": "package disperse\n\nimport (\n\t\"std\"\n\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\n// Get address of Disperse realm\nvar realmAddr = std.CurrentRealm().Addr()\n\n// DisperseUgnot parses receivers and amounts and sends out ugnot\n// The function will send out the coins to the addresses and return the leftover coins to the caller\n// if there are any to return\nfunc DisperseUgnot(addresses []std.Address, coins std.Coins) {\n\tcoinSent := std.GetOrigSend()\n\tcaller := std.PrevRealm().Addr()\n\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n\n\tif len(addresses) != len(coins) {\n\t\tpanic(ErrNumAddrValMismatch)\n\t}\n\n\tfor _, coin := range coins {\n\t\tif coin.Amount \u003c= 0 {\n\t\t\tpanic(ErrNegativeCoinAmount)\n\t\t}\n\n\t\tif banker.GetCoins(realmAddr).AmountOf(coin.Denom) \u003c coin.Amount {\n\t\t\tpanic(ErrMismatchBetweenSentAndParams)\n\t\t}\n\t}\n\n\t// Send coins\n\tfor i, _ := range addresses {\n\t\tbanker.SendCoins(realmAddr, addresses[i], std.NewCoins(coins[i]))\n\t}\n\n\t// Return possible leftover coins\n\tfor _, coin := range coinSent {\n\t\tleftoverAmt := banker.GetCoins(realmAddr).AmountOf(coin.Denom)\n\t\tif leftoverAmt \u003e 0 {\n\t\t\tsend := std.Coins{std.NewCoin(coin.Denom, leftoverAmt)}\n\t\t\tbanker.SendCoins(realmAddr, caller, send)\n\t\t}\n\t}\n}\n\n// DisperseGRC20 disperses tokens to multiple addresses\n// Note that it is necessary to approve the realm to spend the tokens before calling this function\n// see the corresponding filetests for examples\nfunc DisperseGRC20(addresses []std.Address, amounts []uint64, symbols []string) {\n\tcaller := std.PrevRealm().Addr()\n\n\tif (len(addresses) != len(amounts)) || (len(amounts) != len(symbols)) {\n\t\tpanic(ErrArgLenAndSentLenMismatch)\n\t}\n\n\tfor i := 0; i \u003c len(addresses); i++ {\n\t\ttokens.TransferFrom(symbols[i], caller, addresses[i], amounts[i])\n\t}\n}\n\n// DisperseGRC20String receives a string of addresses and a string of tokens\n// and parses them to be used in DisperseGRC20\nfunc DisperseGRC20String(addresses string, tokens string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, parsedSymbols, err := parseTokens(tokens)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tDisperseGRC20(parsedAddresses, parsedAmounts, parsedSymbols)\n}\n\n// DisperseUgnotString receives a string of addresses and a string of amounts\n// and parses them to be used in DisperseUgnot\nfunc DisperseUgnotString(addresses string, amounts string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, err := parseAmounts(amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcoins := make(std.Coins, len(parsedAmounts))\n\tfor i, amount := range parsedAmounts {\n\t\tcoins[i] = std.NewCoin(\"ugnot\", amount)\n\t}\n\n\tDisperseUgnot(parsedAddresses, coins)\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package disperse provides methods to disperse coins or GRC20 tokens among multiple addresses.\n//\n// The disperse package is an implementation of an existing service that allows users to send coins or GRC20 tokens to multiple addresses\n// on the Ethereum blockchain.\n//\n// Usage:\n// To use disperse, you can either use `DisperseUgnot` to send coins or `DisperseGRC20` to send GRC20 tokens to multiple addresses.\n//\n// Example:\n// Dispersing 200 coins to two addresses:\n// - DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n// Dispersing 200 worth of a GRC20 token \"TEST\" to two addresses:\n// - DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n//\n// Reference:\n// - [the original dispere app](https://disperse.app/)\n// - [the original disperse app on etherscan](https://etherscan.io/address/0xd152f549545093347a162dce210e7293f1452150#code)\n// - [the gno disperse web app](https://gno-disperse.netlify.app/)\npackage disperse // import \"gno.land/r/demo/disperse\"\n" + }, + { + "name": "errors.gno", + "body": "package disperse\n\nimport \"errors\"\n\nvar (\n\tErrNotEnoughCoin = errors.New(\"disperse: not enough coin sent in\")\n\tErrNumAddrValMismatch = errors.New(\"disperse: number of addresses and values to send doesn't match\")\n\tErrInvalidAddress = errors.New(\"disperse: invalid address\")\n\tErrNegativeCoinAmount = errors.New(\"disperse: coin amount cannot be negative\")\n\tErrMismatchBetweenSentAndParams = errors.New(\"disperse: mismatch between coins sent and params called\")\n\tErrArgLenAndSentLenMismatch = errors.New(\"disperse: mismatch between coins sent and args called\")\n)\n" + }, + { + "name": "util.gno", + "body": "package disperse\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nfunc parseAddresses(addresses string) ([]std.Address, error) {\n\tvar ret []std.Address\n\n\tfor _, str := range strings.Split(addresses, \",\") {\n\t\taddr := std.Address(str)\n\t\tif !addr.IsValid() {\n\t\t\treturn nil, ErrInvalidAddress\n\t\t}\n\n\t\tret = append(ret, addr)\n\t}\n\n\treturn ret, nil\n}\n\nfunc splitString(input string) (string, string) {\n\tvar pos int\n\tfor i, char := range input {\n\t\tif !unicode.IsDigit(char) {\n\t\t\tpos = i\n\t\t\tbreak\n\t\t}\n\t}\n\treturn input[:pos], input[pos:]\n}\n\nfunc parseTokens(tokens string) ([]uint64, []string, error) {\n\tvar amounts []uint64\n\tvar symbols []string\n\n\tfor _, token := range strings.Split(tokens, \",\") {\n\t\tamountStr, symbol := splitString(token)\n\t\tamount, _ := strconv.Atoi(amountStr)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tamounts = append(amounts, uint64(amount))\n\t\tsymbols = append(symbols, symbol)\n\t}\n\n\treturn amounts, symbols, nil\n}\n\nfunc parseAmounts(amounts string) ([]int64, error) {\n\tvar ret []int64\n\n\tfor _, amt := range strings.Split(amounts, \",\") {\n\t\tamount, _ := strconv.Atoi(amt)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tret = append(ret, int64(amount))\n\t}\n\n\treturn ret, nil\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 200ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 200}})\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n}\n\n// Output:\n// main before: 200000200ugnot\n// main after: 200000000ugnot\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 300}})\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n}\n\n// Output:\n// main before: 200000300ugnot\n// main after: 200000100ugnot\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 100}})\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n}\n\n// Error:\n// disperse: mismatch between coins sent and params called\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\tbeneficiary1 := std.Address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := std.Address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\ttokens.New(\"test\", \"TEST\", 4, 0, 0)\n\ttokens.Mint(\"TEST\", mainaddr, 200)\n\n\tmainbal := tokens.BalanceOf(\"TEST\", mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\ttokens.Approve(\"TEST\", disperseAddr, 200)\n\n\tdisperse.DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n\n\tmainbal = tokens.BalanceOf(\"TEST\", mainaddr)\n\tprintln(\"main after:\", mainbal)\n\tben1bal := tokens.BalanceOf(\"TEST\", beneficiary1)\n\tprintln(\"beneficiary1:\", ben1bal)\n\tben2bal := tokens.BalanceOf(\"TEST\", beneficiary2)\n\tprintln(\"beneficiary2:\", ben2bal)\n}\n\n// Output:\n// main before: 200\n// main after: 0\n// beneficiary1: 150\n// beneficiary2: 50\n" + }, + { + "name": "z_4_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\tbeneficiary1 := std.Address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := std.Address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\ttokens.New(\"test1\", \"TEST1\", 4, 0, 0)\n\ttokens.Mint(\"TEST1\", mainaddr, 200)\n\ttokens.New(\"test2\", \"TEST2\", 4, 0, 0)\n\ttokens.Mint(\"TEST2\", mainaddr, 200)\n\n\tmainbal := tokens.BalanceOf(\"TEST1\", mainaddr) + tokens.BalanceOf(\"TEST2\", mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\ttokens.Approve(\"TEST1\", disperseAddr, 200)\n\ttokens.Approve(\"TEST2\", disperseAddr, 200)\n\n\tdisperse.DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"200TEST1,200TEST2\")\n\n\tmainbal = tokens.BalanceOf(\"TEST1\", mainaddr) + tokens.BalanceOf(\"TEST2\", mainaddr)\n\tprintln(\"main after:\", mainbal)\n\tben1bal := tokens.BalanceOf(\"TEST1\", beneficiary1) + tokens.BalanceOf(\"TEST2\", beneficiary1)\n\tprintln(\"beneficiary1:\", ben1bal)\n\tben2bal := tokens.BalanceOf(\"TEST1\", beneficiary2) + tokens.BalanceOf(\"TEST2\", beneficiary2)\n\tprintln(\"beneficiary2:\", ben2bal)\n}\n\n// Output:\n// main before: 400\n// main after: 0\n// beneficiary1: 200\n// beneficiary2: 200\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "echo", + "path": "gno.land/r/demo/echo", + "files": [ + { + "name": "echo.gno", + "body": "package echo\n\n/*\n * This realm echoes the `path` argument it received.\n * Can be used by developers as a simple endpoint to test\n * forbidden characters, for pentesting or simply to\n * test it works.\n *\n * See also r/demo/print (to print various thing like user address)\n */\nfunc Render(path string) string {\n\treturn path\n}\n" + }, + { + "name": "echo_test.gno", + "body": "package echo\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc Test(t *testing.T) {\n\turequire.Equal(t, \"aa\", Render(\"aa\"))\n\turequire.Equal(t, \"\", Render(\"\"))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "event", + "path": "gno.land/r/demo/event", + "files": [ + { + "name": "event.gno", + "body": "package event\n\nimport (\n\t\"std\"\n)\n\nfunc Emit(value string) {\n\tstd.Emit(\"TAG\", \"key\", value)\n}\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport \"gno.land/r/demo/event\"\n\nfunc main() {\n\tevent.Emit(\"foo\")\n\tevent.Emit(\"bar\")\n}\n\n// Events:\n// [\n// {\n// \"type\": \"TAG\",\n// \"attrs\": [\n// {\n// \"key\": \"key\",\n// \"value\": \"foo\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/demo/event\",\n// \"func\": \"Emit\"\n// },\n// {\n// \"type\": \"TAG\",\n// \"attrs\": [\n// {\n// \"key\": \"key\",\n// \"value\": \"bar\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/demo/event\",\n// \"func\": \"Emit\"\n// }\n// ]\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "foo1155", + "path": "gno.land/r/demo/foo1155", + "files": [ + { + "name": "foo1155.gno", + "body": "package foo1155\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nvar (\n\tdummyURI = \"ipfs://xyz\"\n\tadmin std.Address = \"g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530\"\n\tfoo = grc1155.NewBasicGRC1155Token(dummyURI)\n)\n\nfunc init() {\n\tmintGRC1155Token(admin) // @administrator (10)\n}\n\nfunc mintGRC1155Token(owner std.Address) {\n\tfor i := 1; i \u003c= 10; i++ {\n\t\ttid := grc1155.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.SafeMint(owner, tid, 100)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user pusers.AddressOrName, tid grc1155.TokenID) uint64 {\n\tbalance, err := foo.BalanceOf(users.Resolve(user), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc BalanceOfBatch(ul []pusers.AddressOrName, batch []grc1155.TokenID) []uint64 {\n\tvar usersResolved []std.Address\n\n\tfor i := 0; i \u003c len(ul); i++ {\n\t\tusersResolved[i] = users.Resolve(ul[i])\n\t}\n\tbalanceBatch, err := foo.BalanceOfBatch(usersResolved, batch)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balanceBatch\n}\n\nfunc IsApprovedForAll(owner, user pusers.AddressOrName) bool {\n\treturn foo.IsApprovedForAll(users.Resolve(owner), users.Resolve(user))\n}\n\n// Setters\n\nfunc SetApprovalForAll(user pusers.AddressOrName, approved bool) {\n\terr := foo.SetApprovalForAll(users.Resolve(user), approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, tid grc1155.TokenID, amount uint64) {\n\terr := foo.SafeTransferFrom(users.Resolve(from), users.Resolve(to), tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BatchTransferFrom(from, to pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) {\n\terr := foo.SafeBatchTransferFrom(users.Resolve(from), users.Resolve(to), batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(to pusers.AddressOrName, tid grc1155.TokenID, amount uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeMint(users.Resolve(to), tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc MintBatch(to pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeBatchMint(users.Resolve(to), batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(from pusers.AddressOrName, tid grc1155.TokenID, amount uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(users.Resolve(from), tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BurnBatch(from pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.BatchBurn(users.Resolve(from), batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address std.Address) {\n\tif address != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n" + }, + { + "name": "foo1155_test.gno", + "body": "package foo1155\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/users\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := users.AddressOrName(\"g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530\")\n\tbob := users.AddressOrName(\"g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw\")\n\ttid1 := grc1155.TokenID(\"1\")\n\ttid2 := grc1155.TokenID(\"2\")\n\n\tfor i, tc := range []struct {\n\t\tname string\n\t\texpected interface{}\n\t\tfn func() interface{}\n\t}{\n\t\t{\"BalanceOf(admin, tid1)\", uint64(100), func() interface{} { return BalanceOf(admin, tid1) }},\n\t\t{\"BalanceOf(bob, tid1)\", uint64(0), func() interface{} { return BalanceOf(bob, tid1) }},\n\t\t{\"IsApprovedForAll(admin, bob)\", false, func() interface{} { return IsApprovedForAll(admin, bob) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "foo20", + "path": "gno.land/r/demo/foo20", + "files": [ + { + "name": "foo20.gno", + "body": "// foo20 is a GRC20 token contract where all the GRC20 methods are proxified\n// with top-level functions. see also gno.land/r/demo/bar20.\npackage foo20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\tpusers \"gno.land/p/demo/users\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbanker *grc20.Banker\n\tadmin *ownable.Ownable\n\ttoken grc20.Token\n)\n\nfunc init() {\n\tadmin = ownable.NewWithAddress(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\") // @manfred\n\tbanker = grc20.NewBanker(\"Foo\", \"FOO\", 4)\n\tbanker.Mint(admin.Owner(), 1000000*10000) // @administrator (1M)\n\ttoken = banker.Token()\n}\n\nfunc TotalSupply() uint64 { return token.TotalSupply() }\n\nfunc BalanceOf(owner pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\treturn token.BalanceOf(ownerAddr)\n}\n\nfunc Allowance(owner, spender pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\tspenderAddr := users.Resolve(spender)\n\treturn token.Allowance(ownerAddr, spenderAddr)\n}\n\nfunc Transfer(to pusers.AddressOrName, amount uint64) {\n\ttoAddr := users.Resolve(to)\n\tcheckErr(token.Transfer(toAddr, amount))\n}\n\nfunc Approve(spender pusers.AddressOrName, amount uint64) {\n\tspenderAddr := users.Resolve(spender)\n\tcheckErr(token.Approve(spenderAddr, amount))\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, amount uint64) {\n\tfromAddr := users.Resolve(from)\n\ttoAddr := users.Resolve(to)\n\tcheckErr(token.TransferFrom(fromAddr, toAddr, amount))\n}\n\n// Faucet is distributing foo20 tokens without restriction (unsafe).\n// For a real token faucet, you should take care of setting limits are asking payment.\nfunc Faucet() {\n\tcaller := std.PrevRealm().Addr()\n\tamount := uint64(1_000 * 10_000) // 1k\n\tcheckErr(banker.Mint(caller, amount))\n}\n\nfunc Mint(to pusers.AddressOrName, amount uint64) {\n\tadmin.AssertCallerIsOwner()\n\ttoAddr := users.Resolve(to)\n\tcheckErr(banker.Mint(toAddr, amount))\n}\n\nfunc Burn(from pusers.AddressOrName, amount uint64) {\n\tadmin.AssertCallerIsOwner()\n\tfromAddr := users.Resolve(from)\n\tcheckErr(banker.Burn(fromAddr, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn banker.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := pusers.AddressOrName(parts[1])\n\t\townerAddr := users.Resolve(owner)\n\t\tbalance := banker.BalanceOf(ownerAddr)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + }, + { + "name": "foo20_test.gno", + "body": "package foo20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\tpusers \"gno.land/p/demo/users\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc TestReadOnlyPublicMethods(t *testing.T) {\n\tvar (\n\t\tadmin = pusers.AddressOrName(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\t\talice = pusers.AddressOrName(testutils.TestAddress(\"alice\"))\n\t\tbob = pusers.AddressOrName(testutils.TestAddress(\"bob\"))\n\t)\n\n\ttype test struct {\n\t\tname string\n\t\tbalance uint64\n\t\tfn func() uint64\n\t}\n\n\t// check balances #1.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_000_000_000, func() uint64 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint64 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint64 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 0, func() uint64 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tuassert.Equal(t, got, tc.balance)\n\t\t}\n\t}\n\n\t// bob uses the faucet.\n\tstd.TestSetOrigCaller(users.Resolve(bob))\n\tFaucet()\n\n\t// check balances #2.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_010_000_000, func() uint64 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint64 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint64 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 10_000_000, func() uint64 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tuassert.Equal(t, got, tc.balance)\n\t\t}\n\t}\n}\n\nfunc TestErrConditions(t *testing.T) {\n\tvar (\n\t\tadmin = pusers.AddressOrName(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\t\talice = pusers.AddressOrName(testutils.TestAddress(\"alice\"))\n\t\tempty = pusers.AddressOrName(\"\")\n\t)\n\n\ttype test struct {\n\t\tname string\n\t\tmsg string\n\t\tfn func()\n\t}\n\n\tstd.TestSetOrigCaller(users.Resolve(admin))\n\t{\n\t\ttests := []test{\n\t\t\t{\"Transfer(admin, 1)\", \"cannot send transfer to self\", func() { Transfer(admin, 1) }},\n\t\t\t{\"Approve(empty, 1))\", \"invalid address\", func() { Approve(empty, 1) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tuassert.PanicsWithMessage(t, tc.msg, tc.fn)\n\t\t\t})\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "foo721", + "path": "gno.land/r/demo/foo721", + "files": [ + { + "name": "foo721.gno", + "body": "package foo721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nvar (\n\tadmin std.Address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"\n\tfoo = grc721.NewBasicNFT(\"FooNFT\", \"FNFT\")\n)\n\nfunc init() {\n\tmintNNFT(admin, 10) // @administrator (10)\n\tmintNNFT(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", 5) // @hariom (5)\n}\n\nfunc mintNNFT(owner std.Address, n uint64) {\n\tcount := foo.TokenCount()\n\tfor i := count; i \u003c count+n; i++ {\n\t\ttid := grc721.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.Mint(owner, tid)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user pusers.AddressOrName) uint64 {\n\tbalance, err := foo.BalanceOf(users.Resolve(user))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc OwnerOf(tid grc721.TokenID) std.Address {\n\towner, err := foo.OwnerOf(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn owner\n}\n\nfunc IsApprovedForAll(owner, user pusers.AddressOrName) bool {\n\treturn foo.IsApprovedForAll(users.Resolve(owner), users.Resolve(user))\n}\n\nfunc GetApproved(tid grc721.TokenID) std.Address {\n\taddr, err := foo.GetApproved(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn addr\n}\n\n// Setters\n\nfunc Approve(user pusers.AddressOrName, tid grc721.TokenID) {\n\terr := foo.Approve(users.Resolve(user), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc SetApprovalForAll(user pusers.AddressOrName, approved bool) {\n\terr := foo.SetApprovalForAll(users.Resolve(user), approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) {\n\terr := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(to pusers.AddressOrName, tid grc721.TokenID) {\n\tcaller := std.PrevRealm().Addr()\n\tassertIsAdmin(caller)\n\terr := foo.Mint(users.Resolve(to), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(tid grc721.TokenID) {\n\tcaller := std.PrevRealm().Addr()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address std.Address) {\n\tif address != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n" + }, + { + "name": "foo721_test.gno", + "body": "package foo721\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/r/demo/users\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := pusers.AddressOrName(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\thariom := pusers.AddressOrName(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tfor i, tc := range []struct {\n\t\tname string\n\t\texpected interface{}\n\t\tfn func() interface{}\n\t}{\n\t\t{\"BalanceOf(admin)\", uint64(10), func() interface{} { return BalanceOf(admin) }},\n\t\t{\"BalanceOf(hariom)\", uint64(5), func() interface{} { return BalanceOf(hariom) }},\n\t\t{\"OwnerOf(0)\", users.Resolve(admin), func() interface{} { return OwnerOf(grc721.TokenID(\"0\")) }},\n\t\t{\"IsApprovedForAll(admin, hariom)\", false, func() interface{} { return IsApprovedForAll(admin, hariom) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "dice_roller", + "path": "gno.land/r/demo/games/dice_roller", + "files": [ + { + "name": "dice_roller.gno", + "body": "package dice_roller\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n)\n\ntype (\n\t// game represents a Dice Roller game between two players\n\tgame struct {\n\t\tplayer1, player2 std.Address\n\t\troll1, roll2 int\n\t}\n\n\t// player holds the information about each player including their stats\n\tplayer struct {\n\t\taddr std.Address\n\t\twins, losses, draws, points int\n\t}\n\n\t// leaderBoard is a slice of players, used to sort players by rank\n\tleaderBoard []player\n)\n\nconst (\n\t// Constants to represent game result outcomes\n\tongoing = iota\n\twin\n\tdraw\n\tloss\n)\n\nvar (\n\tgames avl.Tree // AVL tree for storing game states\n\tgameId seqid.ID // Sequence ID for games\n\n\tplayers avl.Tree // AVL tree for storing player data\n\n\tseed = uint64(entropy.New().Seed())\n\tr = rand.New(rand.NewPCG(seed, 0xdeadbeef))\n)\n\n// rollDice generates a random dice roll between 1 and 6\nfunc rollDice() int {\n\treturn r.IntN(6) + 1\n}\n\n// NewGame initializes a new game with the provided opponent's address\nfunc NewGame(addr std.Address) int {\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid opponent's address\")\n\t}\n\n\tgames.Set(gameId.Next().String(), \u0026game{\n\t\tplayer1: std.PrevRealm().Addr(),\n\t\tplayer2: addr,\n\t})\n\n\treturn int(gameId)\n}\n\n// Play allows a player to roll the dice and updates the game state accordingly\nfunc Play(idx int) int {\n\tg, err := getGame(idx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\troll := rollDice() // Random the player's dice roll\n\n\t// Play the game and update the player's roll\n\tif err := g.play(std.PrevRealm().Addr(), roll); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// If both players have rolled, update the results and leaderboard\n\tif g.isFinished() {\n\t\t// If the player is playing against themselves, no points are awarded\n\t\tif g.player1 == g.player2 {\n\t\t\treturn roll\n\t\t}\n\n\t\tplayer1 := getPlayer(g.player1)\n\t\tplayer2 := getPlayer(g.player2)\n\n\t\tif g.roll1 \u003e g.roll2 {\n\t\t\tplayer1.updateStats(win)\n\t\t\tplayer2.updateStats(loss)\n\t\t} else if g.roll2 \u003e g.roll1 {\n\t\t\tplayer2.updateStats(win)\n\t\t\tplayer1.updateStats(loss)\n\t\t} else {\n\t\t\tplayer1.updateStats(draw)\n\t\t\tplayer2.updateStats(draw)\n\t\t}\n\t}\n\n\treturn roll\n}\n\n// play processes a player's roll and updates their score\nfunc (g *game) play(player std.Address, roll int) error {\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\n\tif g.isFinished() {\n\t\treturn errors.New(\"game over\")\n\t}\n\n\tif player == g.player1 \u0026\u0026 g.roll1 == 0 {\n\t\tg.roll1 = roll\n\t\treturn nil\n\t}\n\n\tif player == g.player2 \u0026\u0026 g.roll2 == 0 {\n\t\tg.roll2 = roll\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"already played\")\n}\n\n// isFinished checks if the game has ended\nfunc (g *game) isFinished() bool {\n\treturn g.roll1 != 0 \u0026\u0026 g.roll2 != 0\n}\n\n// checkResult returns the game status as a formatted string\nfunc (g *game) status() string {\n\tif !g.isFinished() {\n\t\treturn resultIcon(ongoing) + \" Game still in progress\"\n\t}\n\n\tif g.roll1 \u003e g.roll2 {\n\t\treturn resultIcon(win) + \" Player1 Wins !\"\n\t} else if g.roll2 \u003e g.roll1 {\n\t\treturn resultIcon(win) + \" Player2 Wins !\"\n\t} else {\n\t\treturn resultIcon(draw) + \" It's a Draw !\"\n\t}\n}\n\n// Render provides a summary of the current state of games and leader board\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(`# 🎲 **Dice Roller Game**\n\nWelcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score !\n\n---\n\n## **How to Play**:\n1. **Create a game**: Challenge an opponent using [NewGame](./dice_roller$help\u0026func=NewGame)\n2. **Roll the dice**: Play your turn by rolling a dice using [Play](./dice_roller$help\u0026func=Play)\n\n---\n\n## **Scoring Rules**:\n- **Win** 🏆: +3 points\n- **Draw** 🤝: +1 point each\n- **Lose** ❌: No points\n- **Playing against yourself**: No points or stats changes for you\n\n---\n\n## **Recent Games**:\nBelow are the results from the most recent games. Up to 10 recent games are displayed\n\n| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | 🏆 Winner |\n|------|----------|-----------|----------|-----------|-----------|\n`)\n\n\tmaxGames := 10\n\tfor n := int(gameId); n \u003e 0 \u0026\u0026 int(gameId)-n \u003c maxGames; n-- {\n\t\tg, err := getGame(n)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tsb.WriteString(strconv.Itoa(n) + \" | \" +\n\t\t\t\"\u003cspan title=\\\"\" + string(g.player1) + \"\\\"\u003e\" + shortName(g.player1) + \"\u003c/span\u003e\" + \" | \" + diceIcon(g.roll1) + \" | \" +\n\t\t\t\"\u003cspan title=\\\"\" + string(g.player2) + \"\\\"\u003e\" + shortName(g.player2) + \"\u003c/span\u003e\" + \" | \" + diceIcon(g.roll2) + \" | \" +\n\t\t\tg.status() + \"\\n\")\n\t}\n\n\tsb.WriteString(`\n---\n\n## **Leaderboard**:\nThe top players are ranked by performance. Games played against oneself are not counted in the leaderboard\n\n| Rank | Player | Wins | Losses | Draws | Points |\n|------|-----------------------|------|--------|-------|--------|\n`)\n\n\tfor i, player := range getLeaderBoard() {\n\t\tsb.WriteString(ufmt.Sprintf(\"| %s | \u003cspan title=\\\"\"+string(player.addr)+\"\\\"\u003e**%s**\u003c/span\u003e | %d | %d | %d | %d |\\n\",\n\t\t\trankIcon(i+1),\n\t\t\tshortName(player.addr),\n\t\t\tplayer.wins,\n\t\t\tplayer.losses,\n\t\t\tplayer.draws,\n\t\t\tplayer.points,\n\t\t))\n\t}\n\n\tsb.WriteString(\"\\n---\\n**Good luck and have fun !** 🎉\")\n\treturn sb.String()\n}\n\n// shortName returns a shortened name for the given address\nfunc shortName(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user != nil {\n\t\treturn user.Name\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n\n// getGame retrieves the game state by its ID\nfunc getGame(idx int) (*game, error) {\n\tv, ok := games.Get(seqid.ID(idx).String())\n\tif !ok {\n\t\treturn nil, errors.New(\"game not found\")\n\t}\n\treturn v.(*game), nil\n}\n\n// updateResult updates the player's stats and points based on the game outcome\nfunc (p *player) updateStats(result int) {\n\tswitch result {\n\tcase win:\n\t\tp.wins++\n\t\tp.points += 3\n\tcase loss:\n\t\tp.losses++\n\tcase draw:\n\t\tp.draws++\n\t\tp.points++\n\t}\n}\n\n// getPlayer retrieves a player or initializes a new one if they don't exist\nfunc getPlayer(addr std.Address) *player {\n\tv, ok := players.Get(addr.String())\n\tif !ok {\n\t\tplayer := \u0026player{\n\t\t\taddr: addr,\n\t\t}\n\t\tplayers.Set(addr.String(), player)\n\t\treturn player\n\t}\n\n\treturn v.(*player)\n}\n\n// getLeaderBoard generates a leaderboard sorted by points\nfunc getLeaderBoard() leaderBoard {\n\tboard := leaderBoard{}\n\tplayers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tplayer := value.(*player)\n\t\tboard = append(board, *player)\n\t\treturn false\n\t})\n\n\tsort.Sort(board)\n\n\treturn board\n}\n\n// Methods for sorting the leaderboard\nfunc (r leaderBoard) Len() int {\n\treturn len(r)\n}\n\nfunc (r leaderBoard) Less(i, j int) bool {\n\tif r[i].points != r[j].points {\n\t\treturn r[i].points \u003e r[j].points\n\t}\n\n\tif r[i].wins != r[j].wins {\n\t\treturn r[i].wins \u003e r[j].wins\n\t}\n\n\tif r[i].draws != r[j].draws {\n\t\treturn r[i].draws \u003e r[j].draws\n\t}\n\n\treturn false\n}\n\nfunc (r leaderBoard) Swap(i, j int) {\n\tr[i], r[j] = r[j], r[i]\n}\n" + }, + { + "name": "dice_roller_test.gno", + "body": "package dice_roller\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tplayer1 = testutils.TestAddress(\"alice\")\n\tplayer2 = testutils.TestAddress(\"bob\")\n\tunknownPlayer = testutils.TestAddress(\"unknown\")\n)\n\n// resetGameState resets the game state for testing\nfunc resetGameState() {\n\tgames = avl.Tree{}\n\tgameId = seqid.ID(0)\n\tplayers = avl.Tree{}\n}\n\n// TestNewGame tests the initialization of a new game\nfunc TestNewGame(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Verify that the game has been correctly initialized\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\turequire.Equal(t, player1.String(), g.player1.String())\n\turequire.Equal(t, player2.String(), g.player2.String())\n\turequire.Equal(t, 0, g.roll1)\n\turequire.Equal(t, 0, g.roll2)\n}\n\n// TestPlay tests the dice rolling functionality for both players\nfunc TestPlay(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\n\t// Simulate rolling dice for player 1\n\troll1 := Play(gameID)\n\n\t// Verify player 1's roll\n\turequire.NotEqual(t, 0, g.roll1)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet\n\n\t// Simulate rolling dice for player 2\n\tstd.TestSetOrigCaller(player2)\n\troll2 := Play(gameID)\n\n\t// Verify player 2's roll\n\turequire.NotEqual(t, 0, g.roll2)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, g.roll2, roll2)\n}\n\n// TestPlayAgainstSelf tests the scenario where a player plays against themselves\nfunc TestPlayAgainstSelf(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player1)\n\n\t// Simulate rolling dice twice by the same player\n\troll1 := Play(gameID)\n\troll2 := Play(gameID)\n\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, g.roll2, roll2)\n}\n\n// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play\nfunc TestPlayInvalidPlayer(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player1)\n\n\t// Attempt to play as an invalid player\n\tstd.TestSetOrigCaller(unknownPlayer)\n\turequire.PanicsWithMessage(t, \"invalid player\", func() {\n\t\tPlay(gameID)\n\t})\n}\n\n// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing\nfunc TestPlayAlreadyPlayed(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Player 1 rolls\n\tPlay(gameID)\n\n\t// Player 1 tries to roll again\n\turequire.PanicsWithMessage(t, \"already played\", func() {\n\t\tPlay(gameID)\n\t})\n}\n\n// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails\nfunc TestPlayBeyondGameEnd(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Play for both players\n\tstd.TestSetOrigCaller(player1)\n\tPlay(gameID)\n\tstd.TestSetOrigCaller(player2)\n\tPlay(gameID)\n\n\t// Check if the game is over\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\n\t// Attempt to play more should fail\n\tstd.TestSetOrigCaller(player1)\n\turequire.PanicsWithMessage(t, \"game over\", func() {\n\t\tPlay(gameID)\n\t})\n}\n" + }, + { + "name": "icon.gno", + "body": "package dice_roller\n\nimport (\n\t\"strconv\"\n)\n\n// diceIcon returns an icon of the dice roll\nfunc diceIcon(roll int) string {\n\tswitch roll {\n\tcase 1:\n\t\treturn \"🎲1\"\n\tcase 2:\n\t\treturn \"🎲2\"\n\tcase 3:\n\t\treturn \"🎲3\"\n\tcase 4:\n\t\treturn \"🎲4\"\n\tcase 5:\n\t\treturn \"🎲5\"\n\tcase 6:\n\t\treturn \"🎲6\"\n\tdefault:\n\t\treturn \"❓\"\n\t}\n}\n\n// resultIcon returns the icon representing the result of a game\nfunc resultIcon(result int) string {\n\tswitch result {\n\tcase ongoing:\n\t\treturn \"🔄\"\n\tcase win:\n\t\treturn \"🏆\"\n\tcase loss:\n\t\treturn \"❌\"\n\tcase draw:\n\t\treturn \"🤝\"\n\tdefault:\n\t\treturn \"❓\"\n\t}\n}\n\n// rankIcon returns the icon for a player's rank\nfunc rankIcon(rank int) string {\n\tswitch rank {\n\tcase 1:\n\t\treturn \"🥇\"\n\tcase 2:\n\t\treturn \"🥈\"\n\tcase 3:\n\t\treturn \"🥉\"\n\tdefault:\n\t\treturn strconv.Itoa(rank)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "shifumi", + "path": "gno.land/r/demo/games/shifumi", + "files": [ + { + "name": "shifumi.gno", + "body": "package shifumi\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst (\n\tempty = iota\n\trock\n\tpaper\n\tscissors\n\tlast\n)\n\ntype game struct {\n\tplayer1, player2 std.Address // shifumi is a 2 players game\n\tmove1, move2 int // can be empty, rock, paper, or scissors\n}\n\nvar games avl.Tree\nvar id seqid.ID\n\nfunc (g *game) play(player std.Address, move int) error {\n\tif !(move \u003e empty \u0026\u0026 move \u003c last) {\n\t\treturn errors.New(\"invalid move\")\n\t}\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\tif player == g.player1 \u0026\u0026 g.move1 == empty {\n\t\tg.move1 = move\n\t\treturn nil\n\t}\n\tif player == g.player2 \u0026\u0026 g.move2 == empty {\n\t\tg.move2 = move\n\t\treturn nil\n\t}\n\treturn errors.New(\"already played\")\n}\n\nfunc (g *game) winner() int {\n\tif g.move1 == empty || g.move2 == empty {\n\t\treturn -1\n\t}\n\tif g.move1 == g.move2 {\n\t\treturn 0\n\t}\n\tif g.move1 == rock \u0026\u0026 g.move2 == scissors ||\n\t\tg.move1 == paper \u0026\u0026 g.move2 == rock ||\n\t\tg.move1 == scissors \u0026\u0026 g.move2 == paper {\n\t\treturn 1\n\t}\n\treturn 2\n}\n\n// NewGame creates a new game where player1 is the caller and player2 the argument.\n// A new game index is returned.\nfunc NewGame(player std.Address) int {\n\tgames.Set(id.Next().String(), \u0026game{player1: std.PrevRealm().Addr(), player2: player})\n\treturn int(id)\n}\n\n// Play executes a move for the game at index idx, where move can be:\n// 1 (rock), 2 (paper), 3 (scissors).\nfunc Play(idx, move int) {\n\tv, ok := games.Get(seqid.ID(idx).String())\n\tif !ok {\n\t\tpanic(\"game not found\")\n\t}\n\tif err := v.(*game).play(std.PrevRealm().Addr(), move); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tmov1 := []string{\"\", \" 🤜 \", \" 🫱 \", \" 👉 \"}\n\tmov2 := []string{\"\", \" 🤛 \", \" 🫲 \", \" 👈 \"}\n\twin := []string{\"pending\", \"draw\", \"player1\", \"player2\"}\n\n\toutput := `# 👊 ✋ ✌️ Shifumi\nActions:\n* [NewGame](shifumi$help\u0026func=NewGame) opponentAddress\n* [Play](shifumi$help\u0026func=Play) gameIndex move (1=rock, 2=paper, 3=scissors)\n\n game | player1 | | player2 | | win \n --- | --- | --- | --- | --- | ---\n`\n\t// Output the 100 most recent games.\n\tmaxGames := 100\n\tfor n := int(id); n \u003e 0 \u0026\u0026 int(id)-n \u003c maxGames; n-- {\n\t\tv, ok := games.Get(seqid.ID(n).String())\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tg := v.(*game)\n\t\toutput += strconv.Itoa(n) + \" | \" +\n\t\t\tshortName(g.player1) + \" | \" + mov1[g.move1] + \" | \" +\n\t\t\tshortName(g.player2) + \" | \" + mov2[g.move2] + \" | \" +\n\t\t\twin[g.winner()+1] + \"\\n\"\n\t}\n\treturn output\n}\n\nfunc shortName(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user != nil {\n\t\treturn user.Name\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "groups", + "path": "gno.land/r/demo/groups", + "files": [ + { + "name": "README.md", + "body": "### - test package\n\n ./build/gno test examples/gno.land/r/demo/groups/\n\n### - add pkg\n\n ./build/gnokey maketx addpkg -pkgdir \"examples/gno.land/r/demo/groups\" -deposit 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1 \n\n### - create group\n\n ./build/gnokey maketx call -func \"CreateGroup\" -args \"dao_trinity_ngo\" -gas-fee \"1000000ugnot\" -gas-wanted 4000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1 \n\n### - add member\n\n ./build/gnokey maketx call -func \"AddMember\" -args \"1\" -args \"g1hd3gwzevxlqmd3jsf64mpfczag8a8e5j2wdn3c\" -args 12 -args \"i am new user\" -gas-fee \"1000000ugnot\" -gas-wanted \"4000000\" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1\n\n### - delete member\n\n ./build/gnokey maketx call -func \"DeleteMember\" -args \"1\" -args \"0\" -gas-fee \"1000000ugnot\" -gas-wanted \"4000000\" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1\n\n### - delete group\n\n ./build/gnokey maketx call -func \"DeleteGroup\" -args \"1\" -gas-fee \"1000000ugnot\" -gas-wanted \"4000000\" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1\n\n" + }, + { + "name": "group.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype GroupID uint64\n\nfunc (gid GroupID) String() string {\n\treturn strconv.Itoa(int(gid))\n}\n\ntype Group struct {\n\tid GroupID\n\turl string\n\tname string\n\tlastMemberID MemberID\n\tmembers avl.Tree\n\tcreator std.Address\n\tcreatedAt time.Time\n}\n\nfunc newGroup(url string, name string, creator std.Address) *Group {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\tif gGroupsByName.Has(name) {\n\t\tpanic(\"Group with such name already exists\")\n\t}\n\treturn \u0026Group{\n\t\tid: incGetGroupID(),\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\tmembers: avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) newMember(id MemberID, address std.Address, weight int, metadata string) *Member {\n\tif group.members.Has(address.String()) {\n\t\tpanic(\"this member for this group already exists\")\n\t}\n\treturn \u0026Member{\n\t\tid: id,\n\t\taddress: address,\n\t\tweight: weight,\n\t\tmetadata: metadata,\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) HasPermission(addr std.Address, perm Permission) bool {\n\tif group.creator != addr {\n\t\treturn false\n\t}\n\treturn isValidPermission(perm)\n}\n\nfunc (group *Group) RenderGroup() string {\n\tstr := \"Group ID: \" + groupIDKey(group.id) + \"\\n\\n\" +\n\t\t\"Group Name: \" + group.name + \"\\n\\n\" +\n\t\t\"Group Creator: \" + usernameOf(group.creator) + \"\\n\\n\" +\n\t\t\"Group createdAt: \" + group.createdAt.String() + \"\\n\\n\" +\n\t\t\"Group Last MemberID: \" + memberIDKey(group.lastMemberID) + \"\\n\\n\"\n\n\tstr += \"Group Members: \\n\\n\"\n\tgroup.members.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tmember := value.(*Member)\n\t\tstr += member.getMemberStr()\n\t\treturn false\n\t})\n\treturn str\n}\n\nfunc (group *Group) deleteGroup() {\n\tgidkey := groupIDKey(group.id)\n\t_, gGroupsRemoved := gGroups.Remove(gidkey)\n\tif !gGroupsRemoved {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\tgGroupsByName.Remove(group.name)\n}\n\nfunc (group *Group) deleteMember(mid MemberID) {\n\tgidkey := groupIDKey(group.id)\n\tif !gGroups.Has(gidkey) {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\n\tg := getGroup(group.id)\n\tmidkey := memberIDKey(mid)\n\tg.members.Remove(midkey)\n}\n" + }, + { + "name": "groups.gno", + "body": "package groups\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgGroups avl.Tree // id -\u003e *Group\n\tgGroupsCtr int // increments Group.id\n\tgGroupsByName avl.Tree // name -\u003e *Group\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n" + }, + { + "name": "member.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype MemberID uint64\n\ntype Member struct {\n\tid MemberID\n\taddress std.Address\n\tweight int\n\tmetadata string\n\tcreatedAt time.Time\n}\n\nfunc (mid MemberID) String() string {\n\treturn strconv.Itoa(int(mid))\n}\n\nfunc (member *Member) getMemberStr() string {\n\tmemberDataStr := \"\"\n\tmemberDataStr += \"\\t\\t\\t[\" + memberIDKey(member.id) + \", \" + member.address.String() + \", \" + strconv.Itoa(member.weight) + \", \" + member.metadata + \", \" + member.createdAt.String() + \"],\\n\\n\"\n\treturn memberDataStr\n}\n" + }, + { + "name": "misc.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/demo/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getGroup(gid GroupID) *Group {\n\tgidkey := groupIDKey(gid)\n\tgroup_, exists := gGroups.Get(gidkey)\n\tif !exists {\n\t\tpanic(\"group id (\" + gid.String() + \") does not exists\")\n\t}\n\tgroup := group_.(*Group)\n\treturn group\n}\n\nfunc incGetGroupID() GroupID {\n\tgGroupsCtr++\n\treturn GroupID(gGroupsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\" \", length-len(str)) + str\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\"0\", length-len(str)) + str\n}\n\nfunc groupIDKey(gid GroupID) string {\n\treturn padZero(uint64(gid), 10)\n}\n\nfunc memberIDKey(mid MemberID) string {\n\treturn padZero(uint64(mid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/r/demo/users:\" + addr.String() + \")\"\n\t}\n\treturn \"[@\" + user.Name + \"](/r/demo/users:\" + user.Name + \")\"\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\tpanic(\"user not found\")\n\t}\n\treturn user.Name\n}\n\nfunc isValidPermission(perm Permission) bool {\n\treturn perm == EditPermission || perm == DeletePermission\n}\n" + }, + { + "name": "public.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetGroupIDFromName(name string) (GroupID, bool) {\n\tgroupI, exists := gGroupsByName.Get(name)\n\tif !exists {\n\t\treturn 0, false\n\t}\n\treturn groupI.(*Group).id, true\n}\n\nfunc CreateGroup(name string) GroupID {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tusernameOf(caller)\n\turl := \"/r/demo/groups:\" + name\n\tgroup := newGroup(url, name, caller)\n\tgidkey := groupIDKey(group.id)\n\tgGroups.Set(gidkey, group)\n\tgGroupsByName.Set(name, group)\n\treturn group.id\n}\n\nfunc AddMember(gid GroupID, address string, weight int, metadata string) MemberID {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tusernameOf(caller)\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, EditPermission) {\n\t\tpanic(\"unauthorized to edit group\")\n\t}\n\tuser := users.GetUserByAddress(std.Address(address))\n\tif user == nil {\n\t\tpanic(\"unknown address \" + address)\n\t}\n\tmid := group.lastMemberID\n\tmember := group.newMember(mid, std.Address(address), weight, metadata)\n\tmidkey := memberIDKey(mid)\n\tgroup.members.Set(midkey, member)\n\tmid++\n\tgroup.lastMemberID = mid\n\treturn member.id\n}\n\nfunc DeleteGroup(gid GroupID) {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete group\")\n\t}\n\tgroup.deleteGroup()\n}\n\nfunc DeleteMember(gid GroupID, mid MemberID) {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete member\")\n\t}\n\tgroup.deleteMember(mid)\n}\n" + }, + { + "name": "render.gno", + "body": "package groups\n\nimport (\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderGroup(gid GroupID) string {\n\tgroup := getGroup(gid)\n\tif group == nil {\n\t\treturn \"missing Group\"\n\t}\n\treturn group.RenderGroup()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"List of all Groups:\\n\\n\"\n\t\tgGroups.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tgroup := value.(*Group)\n\t\t\tstr += \" * [\" + group.name + \"](\" + group.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/demo/groups:Group_NAME\n\t\tname := parts[0]\n\t\tgroupI, exists := gGroupsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"Group does not exist: \" + name\n\t\t}\n\t\treturn groupI.(*Group).RenderGroup()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n" + }, + { + "name": "role.gno", + "body": "package groups\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission Permission = \"role:edit\"\n)\n" + }, + { + "name": "z_0_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/r/demo/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// user not found\n" + }, + { + "name": "z_0_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// payment must not be less than 20000000\n" + }, + { + "name": "z_0_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n//\n// * [test_group](/r/demo/groups:test_group)\n" + }, + { + "name": "z_1_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser0\", \"my profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tusers.Register(caller, \"gnouser1\", \"my other profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\tusers.Invite(test2.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(caller, \"gnouser2\", \"my other profile 2\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\tusers.Invite(test3.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test3)\n\tusers.Register(caller, \"gnouser3\", \"my other profile 3\")\n\n\tstd.TestSetOrigCaller(caller)\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\tgroups.AddMember(gid, test3.String(), 32, \"i am from UAE\")\n\tprintln(groups.Render(\"test_group\"))\n}\n\n// Output:\n// 1\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: gnouser0\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n//\n// \t\t\t[0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001],\n" + }, + { + "name": "z_1_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.AddMember(2, \"g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy\", 55, \"metadata3\")\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (2) does not exists\n" + }, + { + "name": "z_1_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// add member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tgroups.AddMember(gid, test2.String(), 42, \"metadata3\")\n}\n\n// Error:\n// user not found\n" + }, + { + "name": "z_2_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser0\", \"my profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tusers.Register(caller, \"gnouser1\", \"my other profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\tusers.Invite(test2.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(caller, \"gnouser2\", \"my other profile 2\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\tusers.Invite(test3.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test3)\n\tusers.Register(caller, \"gnouser3\", \"my other profile 3\")\n\n\tstd.TestSetOrigCaller(caller)\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\tgroups.AddMember(gid, test2.String(), 42, \"metadata3\")\n\n\tgroups.DeleteMember(gid, 0)\n\tprintln(groups.RenderGroup(gid))\n}\n\n// Output:\n// 1\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: gnouser0\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n" + }, + { + "name": "z_2_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteMember(2, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (2) does not exists\n" + }, + { + "name": "z_2_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// delete member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tgroups.DeleteMember(gid, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// unauthorized to delete member\n" + }, + { + "name": "z_2_e_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteGroup(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n" + }, + { + "name": "z_2_f_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteGroup(20)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (20) does not exists\n" + }, + { + "name": "z_2_g_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// delete group via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tgroups.DeleteGroup(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// unauthorized to delete group\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "keystore", + "path": "gno.land/r/demo/keystore", + "files": [ + { + "name": "keystore.gno", + "body": "package keystore\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar data avl.Tree\n\nconst (\n\tBaseURL = \"/r/demo/keystore\"\n\tStatusOK = \"ok\"\n\tStatusNoUser = \"user not found\"\n\tStatusNotFound = \"key not found\"\n\tStatusNoWriteAccess = \"no write access\"\n\tStatusCouldNotExecute = \"could not execute\"\n\tStatusNoDatabases = \"no databases\"\n)\n\nfunc init() {\n\tdata = avl.Tree{} // user -\u003e avl.Tree\n}\n\n// KeyStore stores the owner-specific avl.Tree\ntype KeyStore struct {\n\tOwner std.Address\n\tData avl.Tree\n}\n\n// Set will set a value to a key\n// requires write-access (original caller must be caller)\nfunc Set(k, v string) string {\n\torigOwner := std.GetOrigCaller()\n\treturn set(origOwner.String(), k, v)\n}\n\n// set (private) will set a key to value\n// requires write-access (original caller must be caller)\nfunc set(owner, k, v string) string {\n\torigOwner := std.GetOrigCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData: avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\tkeystore.Data.Set(k, v)\n\treturn StatusOK\n}\n\n// Remove removes a key\n// requires write-access (original owner must be caller)\nfunc Remove(k string) string {\n\torigOwner := std.GetOrigCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// remove (private) removes a key\n// requires write-access (original owner must be caller)\nfunc remove(owner, k string) string {\n\torigOwner := std.GetOrigCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData: avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\t_, removed := keystore.Data.Remove(k)\n\tif !removed {\n\t\treturn StatusCouldNotExecute\n\t}\n\treturn StatusOK\n}\n\n// Get returns a value for a key\n// read-only\nfunc Get(k string) string {\n\torigOwner := std.GetOrigCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// get (private) returns a value for a key\n// read-only\nfunc get(owner, k string) string {\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\tval, found := keystore.Data.Get(k)\n\tif !found {\n\t\treturn StatusNotFound\n\t}\n\treturn val.(string)\n}\n\n// Size returns size of database\n// read-only\nfunc Size() string {\n\torigOwner := std.GetOrigCaller()\n\treturn size(origOwner.String())\n}\n\nfunc size(owner string) string {\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\treturn ufmt.Sprintf(\"%d\", keystore.Data.Size())\n}\n\n// Render provides read-only url access to the functions of the keystore\n// \"\" -\u003e show all keystores listed by owner\n// \"owner\" -\u003e show all keys for that owner's keystore\n// \"owner:size\" -\u003e returns size of owner's keystore\n// \"owner:get:key\" -\u003e show value for that key in owner's keystore\nfunc Render(p string) string {\n\tvar response string\n\targs := strings.Split(p, \":\")\n\tnumArgs := len(args)\n\tif p == \"\" {\n\t\tnumArgs = 0\n\t}\n\tswitch numArgs {\n\tcase 0:\n\t\tif data.Size() == 0 {\n\t\t\treturn StatusNoDatabases\n\t\t}\n\t\tdata.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tks := value.(*KeyStore)\n\t\t\tresponse += ufmt.Sprintf(\"- [%s](%s:%s) (%d keys)\\n\", ks.Owner, BaseURL, ks.Owner, ks.Data.Size())\n\t\t\treturn false\n\t\t})\n\tcase 1:\n\t\towner := args[0]\n\t\tkeystoreInterface, exists := data.Get(owner)\n\t\tif !exists {\n\t\t\treturn StatusNoUser\n\t\t}\n\t\tks := keystoreInterface.(*KeyStore)\n\t\ti := 0\n\t\tresponse += ufmt.Sprintf(\"# %s database\\n\\n\", ks.Owner)\n\t\tks.Data.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tresponse += ufmt.Sprintf(\"- %d [%s](%s:%s:get:%s)\\n\", i, key, BaseURL, ks.Owner, key)\n\t\t\ti++\n\t\t\treturn false\n\t\t})\n\tcase 2:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tif cmd == \"size\" {\n\t\t\treturn size(owner)\n\t\t}\n\tcase 3:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tkey := args[2]\n\t\tif cmd == \"get\" {\n\t\t\treturn get(owner, key)\n\t\t}\n\t}\n\n\treturn response\n}\n" + }, + { + "name": "keystore_test.gno", + "body": "package keystore\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestRender(t *testing.T) {\n\tconst (\n\t\tauthor1 std.Address = testutils.TestAddress(\"author1\")\n\t\tauthor2 std.Address = testutils.TestAddress(\"author2\")\n\t)\n\n\ttt := []struct {\n\t\tcaller std.Address\n\t\towner std.Address\n\t\tps []string\n\t\texp string\n\t}{\n\t\t// can set database if the owner is the caller\n\t\t{author1, author1, []string{\"set\", \"hello\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hello\", \"world\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\t\t// only owner can remove\n\t\t{author1, author1, []string{\"remove\", \"hi\"}, StatusOK},\n\t\t{author1, author1, []string{\"get\", \"hi\"}, StatusNotFound},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t// add back\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\n\t\t// different owner has different database\n\t\t{author2, author2, []string{\"set\", \"hello\", \"universe\"}, StatusOK},\n\t\t// either author can get the other info\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// either author can get the other info\n\t\t{author2, author1, []string{\"get\", \"hello\"}, \"world\"},\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// anyone can view the databases\n\t\t{author1, author2, []string{}, `- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/keystore:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6) (2 keys)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00) (1 keys)`},\n\t\t// anyone can view the keys in a database\n\t\t{author1, author2, []string{\"\"}, `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00 database\n\n- 0 [hello](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00:get:hello)`},\n\t}\n\tfor _, tc := range tt {\n\t\tp := \"\"\n\t\tif len(tc.ps) \u003e 0 {\n\t\t\tp = tc.owner.String()\n\t\t\tfor i, psv := range tc.ps {\n\t\t\t\tp += \":\" + psv\n\t\t\t}\n\t\t}\n\t\tp = strings.TrimSuffix(p, \":\")\n\t\tt.Run(p, func(t *testing.T) {\n\t\t\tstd.TestSetOrigCaller(tc.caller)\n\t\t\tvar act string\n\t\t\tif len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"set\" {\n\t\t\t\tact = strings.TrimSpace(Set(tc.ps[1], tc.ps[2]))\n\t\t\t} else if len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"remove\" {\n\t\t\t\tact = strings.TrimSpace(Remove(tc.ps[1]))\n\t\t\t} else {\n\t\t\t\tact = strings.TrimSpace(Render(p))\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tc.exp, act, ufmt.Sprintf(\"%v -\u003e '%s'\", tc.ps, p))\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "markdown", + "path": "gno.land/r/demo/markdown_test", + "files": [ + { + "name": "markdown.gno", + "body": "package markdown\n\n// this package can be used to test markdown rendering engines.\n\nfunc Render(path string) string {\n\toutput := `_imported from https://github.com/markedjs/marked/blob/master/docs/demo/quickref.md_\n\nMarkdown Quick Reference\n========================\n\nThis guide is a very brief overview, with examples, of the syntax that [Markdown] supports. It is itself written in Markdown and you can copy the samples over to the left-hand pane for experimentation. It's shown as *text* and not *rendered HTML*.\n\n[Markdown]: http://daringfireball.net/projects/markdown/\n\n\nSimple Text Formatting\n======================\n\nFirst thing is first. You can use *stars* or _underscores_ for italics. **Double stars** and __double underscores__ for bold. ***Three together*** for ___both___.\n\nParagraphs are pretty easy too. Just have a blank line between chunks of text.\n\n\u003e This chunk of text is in a block quote. Its multiple lines will all be\n\u003e indented a bit from the rest of the text.\n\u003e\n\u003e \u003e Multiple levels of block quotes also work.\n\nSometimes you want to include code, such as when you are explaining how ` + \"`\u003ch1\u003e`\" + ` HTML tags work, or maybe you are a programmer and you are discussing ` + \"`someMethod()`\" + `.\n\nIf you want to include code and have new\nlines preserved, indent the line with a tab\nor at least four spaces:\n\n Extra spaces work here too.\n This is also called preformatted text and it is useful for showing examples.\n The text will stay as text, so any *markdown* or \u003cu\u003eHTML\u003c/u\u003e you add will\n not show up formatted. This way you can show markdown examples in a\n markdown document.\n\n\u003e You can also use preformatted text with your blockquotes\n\u003e as long as you add at least five spaces.\n\n\nHeadings\n========\n\nThere are a couple of ways to make headings. Using three or more equals signs on a line under a heading makes it into an \"h1\" style. Three or more hyphens under a line makes it \"h2\" (slightly smaller). You can also use multiple pound symbols (` + \"`#`\" + `) before and after a heading. Pounds after the title are ignored. Here are some examples:\n\nThis is H1\n==========\n\nThis is H2\n----------\n\n# This is H1\n## This is H2\n### This is H3 with some extra pounds ###\n#### You get the idea ####\n##### I don't need extra pounds at the end\n###### H6 is the max\n\n\nLinks\n=====\n\nLet's link to a few sites. First, let's use the bare URL, like \u003chttps://www.github.com\u003e. Great for text, but ugly for HTML.\nNext is an inline link to [Google](https://www.google.com). A little nicer.\nThis is a reference-style link to [Wikipedia] [1].\nLastly, here's a pretty link to [Yahoo]. The reference-style and pretty links both automatically use the links defined below, but they could be defined *anywhere* in the markdown and are removed from the HTML. The names are also case insensitive, so you can use [YaHoO] and have it link properly.\n\n[1]: https://www.wikipedia.org\n[Yahoo]: https://www.yahoo.com\n\nTitle attributes may be added to links by adding text after a link.\nThis is the [inline link](https://www.bing.com \"Bing\") with a \"Bing\" title.\nYou can also go to [W3C] [2] and maybe visit a [friend].\n\n[2]: https://w3c.org (The W3C puts out specs for web-based things)\n[Friend]: https://facebook.com \"Facebook!\"\n\nEmail addresses in plain text are not linked: test@example.com.\nEmail addresses wrapped in angle brackets are linked: \u003ctest@example.com\u003e.\nThey are also obfuscated so that email harvesting spam robots hopefully won't get them.\n\n\nLists\n=====\n\n* This is a bulleted list\n* Great for shopping lists\n- You can also use hyphens\n+ Or plus symbols\n\nThe above is an \"unordered\" list. Now, on for a bit of order.\n\n1. Numbered lists are also easy\n2. Just start with a number\n3738762. However, the actual number doesn't matter when converted to HTML.\n1. This will still show up as 4.\n\nYou might want a few advanced lists:\n\n- This top-level list is wrapped in paragraph tags\n- This generates an extra space between each top-level item.\n\n- You do it by adding a blank line\n\n- This nested list also has blank lines between the list items.\n\n- How to create nested lists\n 1. Start your regular list\n 2. Indent nested lists with two spaces\n 3. Further nesting means you should indent with two more spaces\n * This line is indented with four spaces.\n\n- List items can be quite lengthy. You can keep typing and either continue\nthem on the next line with no indentation.\n\n- Alternately, if that looks ugly, you can also\n indent the next line a bit for a prettier look.\n\n- You can put large blocks of text in your list by just indenting with two spaces.\n\n This is formatted the same as code, but you can inspect the HTML\n and find that it's just wrapped in a ` + \"`\u003cp\u003e`\" + ` tag and *won't* be shown\n as preformatted text.\n\n You can keep adding more and more paragraphs to a single\n list item by adding the traditional blank line and then keep\n on indenting the paragraphs with two spaces.\n\n You really only need to indent the first line,\nbut that looks ugly.\n\n- Lists support blockquotes\n\n \u003e Just like this example here. By the way, you can\n \u003e nest lists inside blockquotes!\n \u003e - Fantastic!\n\n- Lists support preformatted text\n\n You just need to indent an additional four spaces.\n\n\nEven More\n=========\n\nHorizontal Rule\n---------------\n\nIf you need a horizontal rule you just need to put at least three hyphens, asterisks, or underscores on a line by themselves. You can also even put spaces between the characters.\n\n---\n****************************\n_ _ _ _ _ _ _\n\nThose three all produced horizontal lines. Keep in mind that three hyphens under any text turns that text into a heading, so add a blank like if you use hyphens.\n\nImages\n------\n\nImages work exactly like links, but they have exclamation points in front. They work with references and titles too.\n\n![Google Logo](https://www.google.com/images/errors/logo_sm.gif) and ![Happy].\n\n[Happy]: https://wpclipart.com/smiley/happy/simple_colors/smiley_face_simple_green_small.png (\"Smiley face\")\n\n\nInline HTML\n-----------\n\nIf markdown is too limiting, you can just insert your own \u003cstrike\u003ecrazy\u003c/strike\u003e HTML. Span-level HTML \u003cu\u003ecan *still* use markdown\u003c/u\u003e. Block level elements must be separated from text by a blank line and must not have any spaces before the opening and closing HTML.\n\n\u003cdiv style='font-family: \"Comic Sans MS\", \"Comic Sans\", cursive;'\u003e\nIt is a pity, but markdown does **not** work in here for most markdown parsers.\n[Marked] handles it pretty well.\n\u003c/div\u003e`\n\treturn output\n}\n" + }, + { + "name": "markdown_test.gno", + "body": "package markdown\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\tif !strings.Contains(output, \"\\nMarkdown Quick Reference\\n\") {\n\t\tt.Errorf(\"invalid output\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "eval", + "path": "gno.land/r/demo/math_eval", + "files": [ + { + "name": "math_eval.gno", + "body": "// eval realm is capable of evaluating 32-bit integer\n// expressions as they would appear in Go. For example:\n// /r/demo/math_eval:(4+12)/2-1+11*15\npackage eval\n\nimport (\n\tevalint32 \"gno.land/p/demo/math_eval/int32\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Render(p string) string {\n\tif len(p) == 0 {\n\t\treturn `\nevaluates 32-bit integer expressions. for example:\n\t\t\n[(4+12)/2-1+11*15](/r/demo/math_eval:(4+12)/2-1+11*15)\n\n`\n\t}\n\texpr, err := evalint32.Parse(p)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\tres, err := evalint32.Eval(expr, nil)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn ufmt.Sprintf(\"%s = %d\", p, res)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "memeland", + "path": "gno.land/r/demo/memeland", + "files": [ + { + "name": "memeland.gno", + "body": "package memeland\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/memeland\"\n)\n\nvar m *memeland.Memeland\n\nfunc init() {\n\tm = memeland.NewMemeland()\n\tm.TransferOwnership(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n}\n\nfunc PostMeme(data string, timestamp int64) string {\n\treturn m.PostMeme(data, timestamp)\n}\n\nfunc Upvote(id string) string {\n\treturn m.Upvote(id)\n}\n\nfunc GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\treturn m.GetPostsInRange(startTimestamp, endTimestamp, page, pageSize, sortBy)\n}\n\nfunc RemovePost(id string) string {\n\treturn m.RemovePost(id)\n}\n\nfunc GetOwner() std.Address {\n\treturn m.Owner()\n}\n\nfunc TransferOwnership(newOwner std.Address) {\n\tif err := m.TransferOwnership(newOwner); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tnumOfMemes := int(m.MemeCounter)\n\tif numOfMemes == 0 {\n\t\treturn \"No memes posted yet! :/\"\n\t}\n\n\t// Default render is get Posts since year 2000 to now\n\treturn m.GetPostsInRange(0, time.Now().Unix(), 1, 10, \"DATE_CREATED\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "microblog", + "path": "gno.land/r/demo/microblog", + "files": [ + { + "name": "README.md", + "body": "# microblog realm\n\n## Getting started:\n\n(One-time) Add the microblog package:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/p/demo/microblog\" --pkgdir \"examples/gno.land/p/demo/microblog\" \\\n --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\n(One-time) Add the microblog realm:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/r/demo/microblog\" --pkgdir \"examples/gno.land/r/demo/microblog\" \\\n --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\nAdd a microblog post:\n\n```\ngnokey maketx call --pkgpath \"gno.land/r/demo/microblog\" --func \"NewPost\" --args \"hello, world\" \\\n --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```" + }, + { + "name": "microblog.gno", + "body": "// Microblog is a website with shortform posts from users.\n// The API is simple - \"AddPost\" takes markdown and\n// adds it to the users site.\n// The microblog location is determined by the user address\n// /r/demo/microblog:\u003cYOUR-ADDRESS\u003e\npackage microblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/microblog\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\ttitle = \"gno-based microblog\"\n\tprefix = \"/r/demo/microblog:\"\n\tm *microblog.Microblog\n)\n\nfunc init() {\n\tm = microblog.NewMicroblog(title, prefix)\n}\n\nfunc renderHome() string {\n\toutput := ufmt.Sprintf(\"# %s\\n\\n\", m.Title)\n\toutput += \"# pages\\n\\n\"\n\n\tfor _, page := range m.GetPages() {\n\t\tif u := users.GetUserByAddress(page.Author); u != nil {\n\t\t\toutput += ufmt.Sprintf(\"- [%s (%s)](%s%s)\\n\", u.Name, page.Author.String(), m.Prefix, page.Author.String())\n\t\t} else {\n\t\t\toutput += ufmt.Sprintf(\"- [%s](%s%s)\\n\", page.Author.String(), m.Prefix, page.Author.String())\n\t\t}\n\t}\n\n\treturn output\n}\n\nfunc renderUser(user string) string {\n\tsilo, found := m.Pages.Get(user)\n\tif !found {\n\t\treturn \"404\" // StatusNotFound\n\t}\n\n\treturn PageToString((silo.(*microblog.Page)))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\n\tisHome := path == \"\"\n\tisUser := len(parts) == 1\n\n\tswitch {\n\tcase isHome:\n\t\treturn renderHome()\n\n\tcase isUser:\n\t\treturn renderUser(parts[0])\n\t}\n\n\treturn \"404\" // StatusNotFound\n}\n\nfunc PageToString(p *microblog.Page) string {\n\to := \"\"\n\tif u := users.GetUserByAddress(p.Author); u != nil {\n\t\to += ufmt.Sprintf(\"# [%s](/r/demo/users:%s)\\n\\n\", u, u)\n\t\to += ufmt.Sprintf(\"%s\\n\\n\", u.Profile)\n\t}\n\to += ufmt.Sprintf(\"## [%s](/r/demo/microblog:%s)\\n\\n\", p.Author, p.Author)\n\n\to += ufmt.Sprintf(\"joined %s, last updated %s\\n\\n\", p.CreatedAt.Format(\"2006-02-01\"), p.LastPosted.Format(\"2006-02-01\"))\n\to += \"## feed\\n\\n\"\n\tfor _, u := range p.GetPosts() {\n\t\to += u.String() + \"\\n\\n\"\n\t}\n\treturn o\n}\n\n// NewPost takes a single argument (post markdown) and\n// adds a post to the address of the caller.\nfunc NewPost(text string) string {\n\tif err := m.NewPost(text); err != nil {\n\t\treturn \"unable to add new post\"\n\t}\n\treturn \"added new post\"\n}\n\nfunc Register(name, profile string) string {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(caller, name, profile)\n\treturn \"OK\"\n}\n" + }, + { + "name": "microblog_test.gno", + "body": "package microblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestMicroblog(t *testing.T) {\n\tconst (\n\t\tauthor1 std.Address = testutils.TestAddress(\"author1\")\n\t\tauthor2 std.Address = testutils.TestAddress(\"author2\")\n\t)\n\n\tstd.TestSetOrigCaller(author1)\n\n\turequire.Equal(t, \"404\", Render(\"/wrongpath\"), \"rendering not giving 404\")\n\turequire.NotEqual(t, \"404\", Render(\"\"), \"rendering / should not give 404\")\n\turequire.NoError(t, m.NewPost(\"goodbyte, web2\"), \"could not create post\")\n\n\t_, err := m.GetPage(author1.String())\n\turequire.NoError(t, err, \"silo should exist\")\n\n\t_, err = m.GetPage(\"no such author\")\n\turequire.Error(t, err, \"silo should not exist\")\n\n\tstd.TestSetOrigCaller(author2)\n\n\turequire.NoError(t, m.NewPost(\"hello, web3\"), \"could not create post\")\n\turequire.NoError(t, m.NewPost(\"hello again, web3\"), \"could not create post\")\n\turequire.NoError(t, m.NewPost(\"hi again,\\n web4?\"), \"could not create post\")\n\n\tprintln(\"--- MICROBLOG ---\\n\\n\")\n\n\texpected := `# gno-based microblog\n\n# pages\n\n- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n`\n\turequire.Equal(t, expected, Render(\"\"), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e goodbyte, web2\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author1.String())), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e hi again,\n\u003e\n\u003e web4?\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello again, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author2.String())), \"incorrect rendering\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "nft", + "path": "gno.land/r/demo/nft", + "files": [ + { + "name": "README.md", + "body": "NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n- [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n- [gno.land/r/demo/nft/nft.go](https://gno.land/r/demo/nft/nft.go)\n- [zrealm_nft3.go test](https://github.com/gnolang/gno/blob/master/tests/files2/zrealm_nft3.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:gnolang/1)).\n" + }, + { + "name": "nft.gno", + "body": "package nft\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc721\"\n)\n\ntype token struct {\n\tgrc721.IGRC721 // implements the GRC721 interface\n\n\ttokenCounter int\n\ttokens avl.Tree // grc721.TokenID -\u003e *NFToken{}\n\toperators avl.Tree // owner std.Address -\u003e operator std.Address\n}\n\ntype NFToken struct {\n\towner std.Address\n\tapproved std.Address\n\ttokenID grc721.TokenID\n\tdata string\n}\n\nvar gToken = \u0026token{}\n\nfunc GetToken() *token { return gToken }\n\nfunc (grc *token) nextTokenID() grc721.TokenID {\n\tgrc.tokenCounter++\n\ts := strconv.Itoa(grc.tokenCounter)\n\treturn grc721.TokenID(s)\n}\n\nfunc (grc *token) getToken(tid grc721.TokenID) (*NFToken, bool) {\n\ttoken, ok := grc.tokens.Get(string(tid))\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn token.(*NFToken), true\n}\n\nfunc (grc *token) Mint(to std.Address, data string) grc721.TokenID {\n\ttid := grc.nextTokenID()\n\tgrc.tokens.Set(string(tid), \u0026NFToken{\n\t\towner: to,\n\t\ttokenID: tid,\n\t\tdata: data,\n\t})\n\treturn tid\n}\n\nfunc (grc *token) BalanceOf(owner std.Address) (count int64) {\n\tpanic(\"not yet implemented\")\n}\n\nfunc (grc *token) OwnerOf(tid grc721.TokenID) std.Address {\n\ttoken, ok := grc.getToken(tid)\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\treturn token.owner\n}\n\n// XXX not fully implemented yet.\nfunc (grc *token) SafeTransferFrom(from, to std.Address, tid grc721.TokenID) {\n\tgrc.TransferFrom(from, to, tid)\n\t// When transfer is complete, this function checks if `_to` is a smart\n\t// contract (code size \u003e 0). If so, it calls `onERC721Received` on\n\t// `_to` and throws if the return value is not\n\t// `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`.\n\t// XXX ensure \"to\" is a realm with onERC721Received() signature.\n}\n\nfunc (grc *token) TransferFrom(from, to std.Address, tid grc721.TokenID) {\n\tcaller := std.GetCallerAt(2)\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\t// Throws unless `msg.sender` is the current owner, an authorized\n\t// operator, or the approved address for this NFT.\n\tif caller != token.owner \u0026\u0026 caller != token.approved {\n\t\toperator, ok := grc.operators.Get(token.owner.String())\n\t\tif !ok || caller != operator.(std.Address) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t}\n\t// Throws if `_from` is not the current owner.\n\tif from != token.owner {\n\t\tpanic(\"from is not the current owner\")\n\t}\n\t// Throws if `_to` is the zero address.\n\tif to == \"\" {\n\t\tpanic(\"to cannot be empty\")\n\t}\n\t// Good.\n\ttoken.owner = to\n}\n\nfunc (grc *token) Approve(approved std.Address, tid grc721.TokenID) {\n\tcaller := std.GetCallerAt(2)\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\t// Throws unless `msg.sender` is the current owner,\n\t// or an authorized operator.\n\tif caller != token.owner {\n\t\toperator, ok := grc.operators.Get(token.owner.String())\n\t\tif !ok || caller != operator.(std.Address) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t}\n\t// Good.\n\ttoken.approved = approved\n}\n\n// XXX make it work for set of operators.\nfunc (grc *token) SetApprovalForAll(operator std.Address, approved bool) {\n\tcaller := std.GetCallerAt(2)\n\tgrc.operators.Set(caller.String(), operator)\n}\n\nfunc (grc *token) GetApproved(tid grc721.TokenID) std.Address {\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\treturn token.approved\n}\n\n// XXX make it work for set of operators\nfunc (grc *token) IsApprovedForAll(owner, operator std.Address) bool {\n\toperator2, ok := grc.operators.Get(owner.String())\n\tif !ok {\n\t\treturn false\n\t}\n\treturn operator == operator2.(std.Address)\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\taddr1 := testutils.TestAddress(\"addr1\")\n\t// addr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(addr1, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n}\n\n// Output:\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n\n// Realm:\n// switchrealm[\"gno.land/r/demo/nft\"]\n// switchrealm[\"gno.land/r/demo/nft\"]\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:11]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/grc/grc721.TokenID\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"NFT#1\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:10]={\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/nft.NFToken\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"564a9e78be869bd258fc3c9ad56f5a75ed68818f\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:11\"\n// }\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:9]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/nft.NFToken\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b53ffc464e1b5655d19b9d5277f3491717c24aca\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:8]={\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b1d928b3716b147c92730e8d234162bec2f0f2fc\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\"\n// }\n// }\n// }\n// u[67c479d3d51d4056b2f4111d5352912a00be311e:5]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b229b824842ec3e7f2341e33d0fa0ca77af2f480\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\",\n// \"ModTime\": \"7\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:4\",\n// \"RefCount\": \"1\"\n// }\n// }\n// u[67c479d3d51d4056b2f4111d5352912a00be311e:4]={\n// \"Fields\": [\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"1e0b9dddb406b4f50500a022266a4cb8a4ea38c6\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"05ab6746ea84b55ca133806af215d99a1c4b045e\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:6\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:4\",\n// \"ModTime\": \"7\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:3\",\n// \"RefCount\": \"1\"\n// }\n// }\n// switchrealm[\"gno.land/r/demo/nft\"]\n// switchrealm[\"gno.land/r/demo/nft_test\"]\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(addr1, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Error:\n// unauthorized\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.GetCallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\t// addr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.TransferFrom(caller, addr1, tid)\n}\n\n// Output:\n// g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.GetCallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.Approve(caller, tid) // approve self.\n\tgrc721.TransferFrom(caller, addr1, tid)\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Output:\n// g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n" + }, + { + "name": "z_4_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.GetCallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.Approve(caller, tid) // approve self.\n\tgrc721.TransferFrom(caller, addr1, tid)\n\tgrc721.Approve(\"\", tid) // approve addr1.\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Error:\n// unauthorized\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "profile", + "path": "gno.land/r/demo/profile", + "files": [ + { + "name": "profile.gno", + "body": "package profile\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tfields = avl.NewTree()\n\trouter = mux.NewRouter()\n)\n\n// Standard fields\nconst (\n\tDisplayName = \"DisplayName\"\n\tHomepage = \"Homepage\"\n\tBio = \"Bio\"\n\tAge = \"Age\"\n\tLocation = \"Location\"\n\tAvatar = \"Avatar\"\n\tGravatarEmail = \"GravatarEmail\"\n\tAvailableForHiring = \"AvailableForHiring\"\n\tInvalidField = \"InvalidField\"\n)\n\n// Events\nconst (\n\tProfileFieldCreated = \"ProfileFieldCreated\"\n\tProfileFieldUpdated = \"ProfileFieldUpdated\"\n)\n\n// Field types used when emitting event\nconst FieldType = \"FieldType\"\n\nconst (\n\tBoolField = \"BoolField\"\n\tStringField = \"StringField\"\n\tIntField = \"IntField\"\n)\n\nfunc init() {\n\trouter.HandleFunc(\"\", homeHandler)\n\trouter.HandleFunc(\"u/{addr}\", profileHandler)\n\trouter.HandleFunc(\"f/{addr}/{field}\", fieldHandler)\n}\n\n// List of supported string fields\nvar stringFields = map[string]bool{\n\tDisplayName: true,\n\tHomepage: true,\n\tBio: true,\n\tLocation: true,\n\tAvatar: true,\n\tGravatarEmail: true,\n}\n\n// List of support int fields\nvar intFields = map[string]bool{\n\tAge: true,\n}\n\n// List of support bool fields\nvar boolFields = map[string]bool{\n\tAvailableForHiring: true,\n}\n\n// Setters\n\nfunc SetStringField(field, value string) bool {\n\taddr := std.PrevRealm().Addr()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, StringField, field, value)\n\n\treturn updated\n}\n\nfunc SetIntField(field string, value int) bool {\n\taddr := std.PrevRealm().Addr()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, IntField, field, string(value))\n\n\treturn updated\n}\n\nfunc SetBoolField(field string, value bool) bool {\n\taddr := std.PrevRealm().Addr()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, BoolField, field, ufmt.Sprintf(\"%t\", value))\n\n\treturn updated\n}\n\n// Getters\n\nfunc GetStringField(addr std.Address, field, def string) string {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn def\n}\n\nfunc GetBoolField(addr std.Address, field string, def bool) bool {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(bool)\n\t}\n\n\treturn def\n}\n\nfunc GetIntField(addr std.Address, field string, def int) int {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(int)\n\t}\n\n\treturn def\n}\n" + }, + { + "name": "profile_test.gno", + "body": "package profile\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\n// Global addresses for test users\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n\tdave = testutils.TestAddress(\"dave\")\n\teve = testutils.TestAddress(\"eve\")\n\tfrank = testutils.TestAddress(\"frank\")\n\tuser1 = testutils.TestAddress(\"user1\")\n\tuser2 = testutils.TestAddress(\"user2\")\n)\n\nfunc TestStringFields(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\t// Get before setting\n\tname := GetStringField(alice, DisplayName, \"anon\")\n\tuassert.Equal(t, \"anon\", name)\n\n\t// Set new key\n\tupdated := SetStringField(DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, false)\n\tupdated = SetStringField(Homepage, \"https://example.com\")\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetStringField(DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tname = GetStringField(alice, DisplayName, \"anon\")\n\thomepage := GetStringField(alice, Homepage, \"\")\n\tbio := GetStringField(alice, Bio, \"42\")\n\n\tuassert.Equal(t, \"Alice foo\", name)\n\tuassert.Equal(t, \"https://example.com\", homepage)\n\tuassert.Equal(t, \"42\", bio)\n}\n\nfunc TestIntFields(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\n\t// Get before setting\n\tage := GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 25, age)\n\n\t// Set new key\n\tupdated := SetIntField(Age, 30)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetIntField(Age, 30)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tage = GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 30, age)\n}\n\nfunc TestBoolFields(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(charlie))\n\n\t// Get before setting\n\thiring := GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, false, hiring)\n\n\t// Set\n\tupdated := SetBoolField(AvailableForHiring, true)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetBoolField(AvailableForHiring, true)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\thiring = GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, true, hiring)\n}\n\nfunc TestMultipleProfiles(t *testing.T) {\n\t// Set profile for user1\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\tupdated := SetStringField(DisplayName, \"User One\")\n\tuassert.Equal(t, updated, false)\n\n\t// Set profile for user2\n\tstd.TestSetRealm(std.NewUserRealm(user2))\n\tupdated = SetStringField(DisplayName, \"User Two\")\n\tuassert.Equal(t, updated, false)\n\n\t// Get profiles\n\tstd.TestSetRealm(std.NewUserRealm(user1)) // Switch back to user1\n\tname1 := GetStringField(user1, DisplayName, \"anon\")\n\tstd.TestSetRealm(std.NewUserRealm(user2)) // Switch back to user2\n\tname2 := GetStringField(user2, DisplayName, \"anon\")\n\n\tuassert.Equal(t, \"User One\", name1)\n\tuassert.Equal(t, \"User Two\", name2)\n}\n\nfunc TestArbitraryStringField(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary string field\n\tupdated := SetStringField(\"MyEmail\", \"my@email.com\")\n\tuassert.Equal(t, updated, false)\n\n\tval := GetStringField(user1, \"MyEmail\", \"\")\n\tuassert.Equal(t, val, \"my@email.com\")\n}\n\nfunc TestArbitraryIntField(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetIntField(\"MyIncome\", 100_000)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetIntField(user1, \"MyIncome\", 0)\n\tuassert.Equal(t, val, 100_000)\n}\n\nfunc TestArbitraryBoolField(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetBoolField(\"IsWinner\", true)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetBoolField(user1, \"IsWinner\", false)\n\tuassert.Equal(t, val, true)\n}\n" + }, + { + "name": "render.gno", + "body": "package profile\n\nimport (\n\t\"bytes\"\n\t\"net/url\"\n\t\"std\"\n\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tBaseURL = \"/r/demo/profile\"\n\tSetStringFieldURL = BaseURL + \"$help\u0026func=SetStringField\u0026field=%s\"\n\tSetIntFieldURL = BaseURL + \"$help\u0026func=SetIntField\u0026field=%s\"\n\tSetBoolFieldURL = BaseURL + \"$help\u0026func=SetBoolField\u0026field=%s\"\n\tViewAllFieldsURL = BaseURL + \":u/%s\"\n\tViewFieldURL = BaseURL + \":f/%s/%s\"\n)\n\nfunc homeHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"## Setters\\n\")\n\tfor field := range stringFields {\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range intFields {\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s Field](%s)\\n\", field, link))\n\t}\n\n\tb.WriteString(\"\\n---\\n\\n\")\n\n\tres.Write(b.String())\n}\n\nfunc profileHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Profile %s\\n\", addr))\n\n\taddress := std.Address(addr)\n\n\tfor field := range stringFields {\n\t\tvalue := GetStringField(address, field, \"n/a\")\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range intFields {\n\t\tvalue := GetIntField(address, field, 0)\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %d [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tvalue := GetBoolField(address, field, false)\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %t [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tres.Write(b.String())\n}\n\nfunc fieldHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\tfield := req.GetVar(\"field\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Field %s for %s\\n\", field, addr))\n\n\taddress := std.Address(addr)\n\tvalue := \"n/a\"\n\tvar editLink string\n\n\tif _, ok := stringFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%s\", GetStringField(address, field, \"n/a\"))\n\t\teditLink = ufmt.Sprintf(SetStringFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, url.QueryEscape(value))\n\t} else if _, ok := intFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%d\", GetIntField(address, field, 0))\n\t\teditLink = ufmt.Sprintf(SetIntFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t} else if _, ok := boolFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%t\", GetBoolField(address, field, false))\n\t\teditLink = ufmt.Sprintf(SetBoolFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t}\n\n\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, editLink))\n\n\tres.Write(b.String())\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "releases_example", + "path": "gno.land/r/demo/releases_example", + "files": [ + { + "name": "dummy.gno", + "body": "package releases_example\n\nfunc init() {\n\t// dummy example data\n\tchangelog.NewRelease(\n\t\t\"v1\",\n\t\t\"r/demo/examples_example_v1\",\n\t\t\"initial release\",\n\t)\n\tchangelog.NewRelease(\n\t\t\"v2\",\n\t\t\"r/demo/examples_example_v2\",\n\t\t\"various improvements\",\n\t)\n}\n" + }, + { + "name": "example.gno", + "body": "// this package demonstrates a way to manage contract releases.\npackage releases_example\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/releases\"\n)\n\nvar (\n\tchangelog = releases.NewChangelog(\"example_app\")\n\tadmin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // @administrator\n)\n\nfunc init() {\n\t// FIXME: admin = std.GetCreator()\n}\n\nfunc NewRelease(name, url, notes string) {\n\tcaller := std.GetOrigCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tchangelog.NewRelease(name, url, notes)\n}\n\nfunc UpdateAdmin(address std.Address) {\n\tcaller := std.GetOrigCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tadmin = address\n}\n\nfunc Render(path string) string {\n\treturn changelog.Render(path)\n}\n" + }, + { + "name": "releases0_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/p/demo/releases\"\n)\n\nfunc main() {\n\tprintln(\"-----------\")\n\tchangelog := releases.NewChangelog(\"example\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v1\", \"r/blahblah\", \"* initial version\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v2\", \"r/blahblah2\", \"* various improvements\\n* new shiny logo\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(changelog.Latest().Render())\n}\n\n// Output:\n// -----------\n// # example\n//\n// -----------\n// # example\n//\n// ## [example v1 (latest)](r/blahblah)\n//\n// * initial version\n//\n// -----------\n// # example\n//\n// ## [example v2 (latest)](r/blahblah2)\n//\n// * various improvements\n// * new shiny logo\n//\n// ## [example v1](r/blahblah)\n//\n// * initial version\n//\n// -----------\n// ## [example v2 (latest)](r/blahblah2)\n//\n// * various improvements\n// * new shiny logo\n" + }, + { + "name": "releases1_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/releases_example\"\n)\n\nfunc main() {\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"v1\"))\n\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"v42\"))\n}\n\n// Output:\n// -----------\n// # example_app\n//\n// ## [example_app v2 (latest)](r/demo/examples_example_v2)\n//\n// various improvements\n//\n// ## [example_app v1](r/demo/examples_example_v1)\n//\n// initial release\n//\n// -----------\n// ## [example_app v1](r/demo/examples_example_v1)\n//\n// initial release\n//\n// -----------\n// no such release\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "tamagotchi", + "path": "gno.land/r/demo/tamagotchi", + "files": [ + { + "name": "realm.gno", + "body": "package tamagotchi\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar t *tamagotchi.Tamagotchi\n\nfunc init() {\n\tReset(\"gnome#0\")\n}\n\nfunc Reset(optionalName string) string {\n\tname := optionalName\n\tif name == \"\" {\n\t\theight := std.GetHeight()\n\t\tname = ufmt.Sprintf(\"gnome#%d\", height)\n\t}\n\n\tt = tamagotchi.New(name)\n\n\treturn ufmt.Sprintf(\"A new tamagotchi is born. Their name is %s %s.\", t.Name(), t.Face())\n}\n\nfunc Feed() string {\n\tt.Feed()\n\treturn t.Markdown()\n}\n\nfunc Play() string {\n\tt.Play()\n\treturn t.Markdown()\n}\n\nfunc Heal() string {\n\tt.Heal()\n\treturn t.Markdown()\n}\n\nfunc Render(path string) string {\n\ttama := t.Markdown()\n\tlinks := `Actions:\n* [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n* [Play](/r/demo/tamagotchi$help\u0026func=Play)\n* [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n* [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n`\n\n\treturn tama + \"\\n\\n\" + links\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/tamagotchi\"\n)\n\nfunc main() {\n\ttamagotchi.Reset(\"tamagnotchi\")\n\tprintln(tamagotchi.Render(\"\"))\n}\n\n// Output:\n// # tamagnotchi 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// Actions:\n// * [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n// * [Play](/r/demo/tamagotchi$help\u0026func=Play)\n// * [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n// * [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "crossrealm", + "path": "gno.land/r/demo/tests/crossrealm", + "files": [ + { + "name": "crossrealm.gno", + "body": "package crossrealm\n\nimport (\n\t\"gno.land/p/demo/tests/p_crossrealm\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype LocalStruct struct {\n\tA int\n}\n\nfunc (ls *LocalStruct) String() string {\n\treturn ufmt.Sprintf(\"LocalStruct{%d}\", ls.A)\n}\n\n// local is saved locally in this realm\nvar local *LocalStruct\n\nfunc init() {\n\tlocal = \u0026LocalStruct{A: 123}\n}\n\n// Make1 returns a local object wrapped by a p struct\nfunc Make1() *p_crossrealm.Container {\n\treturn \u0026p_crossrealm.Container{\n\t\tA: 1,\n\t\tB: local,\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "tests_foo", + "path": "gno.land/r/demo/tests_foo", + "files": [ + { + "name": "foo.gno", + "body": "package tests_foo\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\n// for testing gno.land/r/demo/tests/interfaces.go\n\ntype FooStringer struct {\n\tFieldA string\n}\n\nfunc (fs *FooStringer) String() string {\n\treturn \"\u0026FooStringer{\" + fs.FieldA + \"}\"\n}\n\nfunc AddFooStringer(fa string) {\n\ttests.AddStringer(\u0026FooStringer{fa})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "todolistrealm", + "path": "gno.land/r/demo/todolist", + "files": [ + { + "name": "todolist.gno", + "body": "package todolistrealm\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// State variables\nvar (\n\ttodolistTree *avl.Tree\n\ttlid seqid.ID\n)\n\n// Constructor\nfunc init() {\n\ttodolistTree = avl.NewTree()\n}\n\nfunc NewTodoList(title string) (int, string) {\n\t// Create new Todolist\n\ttl := todolist.NewTodoList(title)\n\t// Update AVL tree with new state\n\ttlid.Next()\n\ttodolistTree.Set(strconv.Itoa(int(tlid)), tl)\n\treturn int(tlid), \"created successfully\"\n}\n\nfunc AddTask(todolistID int, title string) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// get the number of tasks in the todolist\n\tid := tl.(*todolist.TodoList).Tasks.Size()\n\n\t// create the task\n\ttask := todolist.NewTask(title)\n\n\t// Cast raw data from tree into Todolist struct\n\ttl.(*todolist.TodoList).AddTask(id, task)\n\n\treturn \"task added successfully\"\n}\n\nfunc ToggleTaskStatus(todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\ttask, found := tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif !found {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttodolist.ToggleTaskStatus(task.(*todolist.Task))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTask(todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\t_, ok = tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif !ok {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttl.(*todolist.TodoList).RemoveTask(strconv.Itoa(taskID))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTodoList(todolistID int) string {\n\t// Get Todolist from AVL tree\n\t_, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Remove the todolist\n\ttodolistTree.Remove(strconv.Itoa(todolistID))\n\n\treturn \"Todolist removed successfully\"\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderHomepage()\n\t}\n\n\treturn \"unknown page\"\n}\n\nfunc renderHomepage() string {\n\t// Define empty buffer\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"# Welcome to ToDolist\\n\\n\")\n\n\t// If no todolists have been created\n\tif todolistTree.Size() == 0 {\n\t\tb.WriteString(\"### No todolists available currently!\")\n\t\treturn b.String()\n\t}\n\n\t// Iterate through AVL tree\n\ttodolistTree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t// cast raw data from tree into Todolist struct\n\t\ttl := value.(*todolist.TodoList)\n\n\t\t// Add Todolist name\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"## Todolist #%s: %s\\n\",\n\t\t\t\tkey, // Todolist ID\n\t\t\t\ttl.GetTodolistTitle(),\n\t\t\t),\n\t\t)\n\n\t\t// Add Todolist owner\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"#### Todolist owner : %s\\n\",\n\t\t\t\ttl.GetTodolistOwner(),\n\t\t\t),\n\t\t)\n\n\t\t// List all todos that are currently Todolisted\n\t\tif todos := tl.GetTasks(); len(todos) \u003e 0 {\n\t\t\tb.WriteString(\n\t\t\t\tufmt.Sprintf(\"Currently Todo tasks: %d\\n\\n\", len(todos)),\n\t\t\t)\n\n\t\t\tfor index, todo := range todos {\n\t\t\t\tb.WriteString(\n\t\t\t\t\tufmt.Sprintf(\"#%d - %s \", index, todo.Title),\n\t\t\t\t)\n\t\t\t\t// displays a checked box if task is marked as done, an empty box if not\n\t\t\t\tif todo.Done {\n\t\t\t\t\tb.WriteString(\n\t\t\t\t\t\t\"☑\\n\\n\",\n\t\t\t\t\t)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tb.WriteString(\n\t\t\t\t\t\"☐\\n\\n\",\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tb.WriteString(\"No tasks in this list currently\\n\")\n\t\t}\n\n\t\tb.WriteString(\"\\n\")\n\t\treturn false\n\t})\n\n\treturn b.String()\n}\n" + }, + { + "name": "todolist_test.gno", + "body": "package todolistrealm\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tnode interface{}\n\ttdl *todolist.TodoList\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttlid, _ := NewTodoList(title)\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\n\t// get the todolist node from the tree\n\tnode, _ = todolistTree.Get(strconv.Itoa(tlid))\n\t// convert the node to a TodoList struct\n\ttdl = node.(*todolist.TodoList)\n\n\tuassert.Equal(t, title, tdl.Title, \"title does not match\")\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\tuassert.Equal(t, tdl.Owner.String(), std.GetOrigCaller().String(), \"owner does not match\")\n\tuassert.Equal(t, 0, len(tdl.GetTasks()), \"Expected no tasks in the todo list\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\tAddTask(1, \"Task 1\")\n\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 1, len(tasks), \"total task does not match\")\n\tuassert.Equal(t, \"Task 1\", tasks[0].Title, \"task title does not match\")\n\tuassert.False(t, tasks[0].Done, \"Expected task to be not done\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\tToggleTaskStatus(1, 0)\n\ttask := tdl.GetTasks()[0]\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not marked as done\")\n\n\tToggleTaskStatus(1, 0)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is marked as done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\tRemoveTask(1, 0)\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 0, len(tasks), \"Expected no tasks in the todo list\")\n}\n\nfunc TestRemoveTodoList(t *testing.T) {\n\tRemoveTodoList(1)\n\tuassert.Equal(t, 0, todolistTree.Size(), \"Expected no tasks in the todo list\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "types", + "path": "gno.land/r/demo/types", + "files": [ + { + "name": "types.gno", + "body": "// package to test types behavior in various conditions (TXs, imports).\npackage types\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tgInt int = -42\n\tgUint uint = 42\n\tgString string = \"a string\"\n\tgStringSlice []string = []string{\"a\", \"string\", \"slice\"}\n\tgError error = errors.New(\"an error\")\n\tgIntSlice []int = []int{-42, 0, 42}\n\tgUintSlice []uint = []uint{0, 42, 84}\n\tgTree avl.Tree\n\t// gInterface = interface{}{-42, \"a string\", uint(42)}\n)\n\nfunc init() {\n\tgTree.Set(\"a\", \"content of A\")\n\tgTree.Set(\"b\", \"content of B\")\n}\n\nfunc Noop() {}\nfunc RetTimeNow() time.Time { return time.Now() }\nfunc RetString() string { return gString }\nfunc RetStringPointer() *string { return \u0026gString }\nfunc RetUint() uint { return gUint }\nfunc RetInt() int { return gInt }\nfunc RetUintPointer() *uint { return \u0026gUint }\nfunc RetIntPointer() *int { return \u0026gInt }\nfunc RetTree() avl.Tree { return gTree }\nfunc RetIntSlice() []int { return gIntSlice }\nfunc RetUintSlice() []uint { return gUintSlice }\nfunc RetStringSlice() []string { return gStringSlice }\nfunc RetError() error { return gError }\nfunc Panic() { panic(\"PANIC!\") }\n\n// TODO: floats\n// TODO: typed errors\n// TODO: ret interface\n// TODO: recover\n// TODO: take types as input\n\nfunc Render(path string) string {\n\treturn \"package to test data types.\"\n}\n" + }, + { + "name": "types_test.gno", + "body": "package types\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "ui", + "path": "gno.land/r/demo/ui", + "files": [ + { + "name": "ui.gno", + "body": "package ui\n\nimport \"gno.land/p/demo/ui\"\n\nfunc Render(path string) string {\n\t// TODO: build this realm as a demo one with one page per feature.\n\n\t// TODO: pagination\n\t// TODO: non-standard markdown\n\t// TODO: error, warn\n\t// TODO: header\n\t// TODO: HTML\n\t// TODO: toc\n\t// TODO: forms\n\t// TODO: comments\n\n\tdom := ui.DOM{\n\t\tPrefix: \"r/demo/ui:\",\n\t}\n\n\tdom.Title = \"UI Demo\"\n\n\tdom.Header.Append(ui.Breadcrumb{\n\t\tui.Link{Text: \"foo\", Path: \"foo\"},\n\t\tui.Link{Text: \"bar\", Path: \"foo/bar\"},\n\t})\n\n\tdom.Body.Append(\n\t\tui.Paragraph(\"Simple UI demonstration.\"),\n\t\tui.BulletList{\n\t\t\tui.Text(\"a text\"),\n\t\t\tui.Link{Text: \"a relative link\", Path: \"foobar\"},\n\t\t\tui.Text(\"another text\"),\n\t\t\t// ui.H1(\"a H1 text\"),\n\t\t\tui.Bold(\"a bold text\"),\n\t\t\tui.Italic(\"italic text\"),\n\t\t\tui.Text(\"raw markdown with **bold** text in the middle.\"),\n\t\t\tui.Code(\"some inline code\"),\n\t\t\tui.Link{Text: \"a remote link\", URL: \"https://gno.land\"},\n\t\t},\n\t)\n\n\tdom.Footer.Append(ui.Text(\"I'm the footer.\"))\n\tdom.Body.Append(ui.Text(\"another string.\"))\n\tdom.Body.Append(ui.Paragraph(\"a paragraph.\"), ui.HR{})\n\n\treturn dom.String()\n}\n" + }, + { + "name": "ui_test.gno", + "body": "package ui\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestRender(t *testing.T) {\n\tgot := Render(\"\")\n\texpected := \"# UI Demo\\n\\n[foo](r/demo/ui:foo) / [bar](r/demo/ui:foo/bar)\\n\\n\\nSimple UI demonstration.\\n\\n- a text\\n- [a relative link](r/demo/ui:foobar)\\n- another text\\n- **a bold text**\\n- _italic text_\\n- raw markdown with **bold** text in the middle.\\n- `some inline code`\\n- [a remote link](https://gno.land)\\n\\nanother string.\\n\\na paragraph.\\n\\n\\n---\\n\\n\\nI'm the footer.\\n\\n\"\n\tuassert.Equal(t, expected, got)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "userbook", + "path": "gno.land/r/demo/userbook", + "files": [ + { + "name": "userbook.gno", + "body": "// This realm demonstrates a small userbook system working with gnoweb\npackage userbook\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Signup struct {\n\taccount string\n\theight int64\n}\n\n// signups - keep a slice of signed up addresses efficient pagination\nvar signups []Signup\n\n// tracker - keep track of who signed up\nvar (\n\ttracker *avl.Tree\n\trouter *mux.Router\n)\n\nconst (\n\tdefaultPageSize = 20\n\tpathArgument = \"number\"\n\tsubPath = \"page/{\" + pathArgument + \"}\"\n\tsignUpEvent = \"SignUp\"\n)\n\nfunc init() {\n\t// Set up tracker tree\n\ttracker = avl.NewTree()\n\n\t// Set up route handling\n\trouter = mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHelper)\n\trouter.HandleFunc(subPath, renderHelper)\n\n\t// Sign up the deployer\n\tSignUp()\n}\n\nfunc SignUp() string {\n\t// Get transaction caller\n\tcaller := std.PrevRealm().Addr().String()\n\theight := std.GetHeight()\n\n\t// Check if the user is already signed up\n\tif _, exists := tracker.Get(caller); exists {\n\t\tpanic(caller + \" is already signed up!\")\n\t}\n\n\t// Sign up the user\n\ttracker.Set(caller, struct{}{})\n\tsignup := Signup{\n\t\tcaller,\n\t\theight,\n\t}\n\n\tsignups = append(signups, signup)\n\tstd.Emit(signUpEvent, \"SignedUpAccount\", signup.account)\n\n\treturn ufmt.Sprintf(\"%s added to userbook up at block #%d!\", signup.account, signup.height)\n}\n\nfunc GetSignupsInRange(page, pageSize int) ([]Signup, int) {\n\tif page \u003c 1 {\n\t\tpanic(\"page number cannot be less than 1\")\n\t}\n\n\tif pageSize \u003c 1 || pageSize \u003e 50 {\n\t\tpanic(\"page size must be from 1 to 50\")\n\t}\n\n\t// Pagination\n\t// Calculate indexes\n\tstartIndex := (page - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\n\t// If page does not contain any users\n\tif startIndex \u003e= len(signups) {\n\t\treturn nil, -1\n\t}\n\n\t// If page contains fewer users than the page size\n\tif endIndex \u003e len(signups) {\n\t\tendIndex = len(signups)\n\t}\n\n\treturn signups[startIndex:endIndex], endIndex\n}\n\nfunc renderHelper(res *mux.ResponseWriter, req *mux.Request) {\n\ttotalSignups := len(signups)\n\tres.Write(\"# Welcome to UserBook!\\n\\n\")\n\n\t// Get URL parameter\n\tpage, err := strconv.Atoi(req.GetVar(\"number\"))\n\tif err != nil {\n\t\tpage = 1 // render first page on bad input\n\t}\n\n\t// Fetch paginated signups\n\tfetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize)\n\t// Handle empty page case\n\tif len(fetchedSignups) == 0 {\n\t\tres.Write(\"No users on this page!\\n\\n\")\n\t\tres.Write(\"---\\n\\n\")\n\t\tres.Write(\"[Back to Page #1](/r/demo/userbook:page/1)\\n\\n\")\n\t\treturn\n\t}\n\n\t// Write page title\n\tres.Write(ufmt.Sprintf(\"## UserBook - Page #%d:\\n\\n\", page))\n\n\t// Write signups\n\tpageStartIndex := defaultPageSize * (page - 1)\n\tfor i, signup := range fetchedSignups {\n\t\tout := ufmt.Sprintf(\"#### User #%d - %s - signed up at Block #%d\\n\", pageStartIndex+i, signup.account, signup.height)\n\t\tres.Write(out)\n\t}\n\n\tres.Write(\"---\\n\\n\")\n\n\t// Write UserBook info\n\tlatestSignupIndex := totalSignups - 1\n\tres.Write(ufmt.Sprintf(\"#### Total users: %d\\n\", totalSignups))\n\tres.Write(ufmt.Sprintf(\"#### Latest signup: User #%d at Block #%d\\n\", latestSignupIndex, signups[latestSignupIndex].height))\n\n\tres.Write(\"---\\n\\n\")\n\n\t// Write page number\n\tres.Write(ufmt.Sprintf(\"You're viewing page #%d\", page))\n\n\t// Write navigation buttons\n\tvar prevPage string\n\tvar nextPage string\n\t// If we are on any page that is not the first page\n\tif page \u003e 1 {\n\t\tprevPage = ufmt.Sprintf(\" - [Previous page](/r/demo/userbook:page/%d)\", page-1)\n\t}\n\n\t// If there are more pages after the current one\n\tif endIndex \u003c totalSignups {\n\t\tnextPage = ufmt.Sprintf(\" - [Next page](/r/demo/userbook:page/%d)\\n\\n\", page+1)\n\t}\n\n\tres.Write(prevPage)\n\tres.Write(nextPage)\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n" + }, + { + "name": "userbook_test.gno", + "body": "package userbook\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestRender(t *testing.T) {\n\t// Sign up 20 users + deployer\n\tfor i := 0; i \u003c 20; i++ {\n\t\taddrName := ufmt.Sprintf(\"test%d\", i)\n\t\tcaller := testutils.TestAddress(addrName)\n\t\tstd.TestSetOrigCaller(caller)\n\t\tSignUp()\n\t}\n\n\ttestCases := []struct {\n\t\tname string\n\t\tnextPage bool\n\t\tprevPage bool\n\t\tpath string\n\t\texpectedNumberOfUsers int\n\t}{\n\t\t{\n\t\t\tname: \"1st page render\",\n\t\t\tnextPage: true,\n\t\t\tprevPage: false,\n\t\t\tpath: \"page/1\",\n\t\t\texpectedNumberOfUsers: 20,\n\t\t},\n\t\t{\n\t\t\tname: \"2nd page render\",\n\t\t\tnextPage: false,\n\t\t\tprevPage: true,\n\t\t\tpath: \"page/2\",\n\t\t\texpectedNumberOfUsers: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"Invalid path render\",\n\t\t\tnextPage: true,\n\t\t\tprevPage: false,\n\t\t\tpath: \"page/invalidtext\",\n\t\t\texpectedNumberOfUsers: 20,\n\t\t},\n\t\t{\n\t\t\tname: \"Empty Page\",\n\t\t\tnextPage: false,\n\t\t\tprevPage: false,\n\t\t\tpath: \"page/1000\",\n\t\t\texpectedNumberOfUsers: 0,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tnumUsers := countUsers(got)\n\n\t\t\tif tc.prevPage \u0026\u0026 !strings.Contains(got, \"Previous page\") {\n\t\t\t\tt.Fatalf(\"expected to find Previous page, didn't find it\")\n\t\t\t}\n\t\t\tif tc.nextPage \u0026\u0026 !strings.Contains(got, \"Next page\") {\n\t\t\t\tt.Fatalf(\"expected to find Next page, didn't find it\")\n\t\t\t}\n\n\t\t\tif tc.expectedNumberOfUsers != numUsers {\n\t\t\t\tt.Fatalf(\"expected %d, got %d users\", tc.expectedNumberOfUsers, numUsers)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc countUsers(input string) int {\n\treturn strings.Count(input, \"#### User #\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "wugnot", + "path": "gno.land/r/demo/wugnot", + "files": [ + { + "name": "wugnot.gno", + "body": "package wugnot\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n\tpusers \"gno.land/p/demo/users\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbanker *grc20.Banker = grc20.NewBanker(\"wrapped GNOT\", \"wugnot\", 0)\n\tToken = banker.Token()\n)\n\nconst (\n\tugnotMinDeposit uint64 = 1000\n\twugnotMinDeposit uint64 = 1\n)\n\nfunc Deposit() {\n\tcaller := std.PrevRealm().Addr()\n\tsent := std.GetOrigSend()\n\tamount := sent.AmountOf(\"ugnot\")\n\n\trequire(uint64(amount) \u003e= ugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d ugnot.\", amount, ugnotMinDeposit))\n\tcheckErr(banker.Mint(caller, uint64(amount)))\n}\n\nfunc Withdraw(amount uint64) {\n\trequire(amount \u003e= wugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d wugnot.\", amount, wugnotMinDeposit))\n\n\tcaller := std.PrevRealm().Addr()\n\tpkgaddr := std.CurrentRealm().Addr()\n\tcallerBal := Token.BalanceOf(caller)\n\trequire(amount \u003c= callerBal, ufmt.Sprintf(\"Insufficient balance: %d available, %d needed.\", callerBal, amount))\n\n\t// send swapped ugnots to qcaller\n\tstdBanker := std.GetBanker(std.BankerTypeRealmSend)\n\tsend := std.Coins{{\"ugnot\", int64(amount)}}\n\tstdBanker.SendCoins(pkgaddr, caller, send)\n\tcheckErr(banker.Burn(caller, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn banker.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := std.Address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\"\n\t}\n}\n\nfunc TotalSupply() uint64 { return Token.TotalSupply() }\n\nfunc BalanceOf(owner pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\treturn Token.BalanceOf(ownerAddr)\n}\n\nfunc Allowance(owner, spender pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\tspenderAddr := users.Resolve(spender)\n\treturn Token.Allowance(ownerAddr, spenderAddr)\n}\n\nfunc Transfer(to pusers.AddressOrName, amount uint64) {\n\ttoAddr := users.Resolve(to)\n\tcheckErr(Token.Transfer(toAddr, amount))\n}\n\nfunc Approve(spender pusers.AddressOrName, amount uint64) {\n\tspenderAddr := users.Resolve(spender)\n\tcheckErr(Token.Approve(spenderAddr, amount))\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, amount uint64) {\n\tfromAddr := users.Resolve(from)\n\ttoAddr := users.Resolve(to)\n\tcheckErr(Token.TransferFrom(fromAddr, toAddr, amount))\n}\n\nfunc require(condition bool, msg string) {\n\tif !condition {\n\t\tpanic(msg)\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/wugnot_test\npackage wugnot_test\n\nimport (\n\t\"fmt\"\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/wugnot\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nvar (\n\taddr1 = testutils.TestAddress(\"test1\")\n\taddrc = std.DerivePkgAddr(\"gno.land/r/demo/wugnot\")\n\taddrt = std.DerivePkgAddr(\"gno.land/r/demo/wugnot_test\")\n)\n\nfunc main() {\n\tstd.TestSetOrigPkgAddr(addrc)\n\tstd.TestIssueCoins(addrc, std.Coins{{\"ugnot\", 100000001}}) // TODO: remove this\n\n\t// issue ugnots\n\tstd.TestIssueCoins(addr1, std.Coins{{\"ugnot\", 100000001}})\n\n\t// print initial state\n\tprintBalances()\n\t// println(wugnot.Render(\"queues\"))\n\t// println(\"A -\", wugnot.Render(\"\"))\n\n\tstd.TestSetOrigCaller(addr1)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 123_400}}, nil)\n\twugnot.Deposit()\n\tprintBalances()\n\twugnot.Withdraw(4242)\n\tprintBalances()\n}\n\nfunc printBalances() {\n\tprintSingleBalance := func(name string, addr std.Address) {\n\t\twugnotBal := wugnot.BalanceOf(pusers.AddressOrName(addr))\n\t\tstd.TestSetOrigCaller(addr)\n\t\trobanker := std.GetBanker(std.BankerTypeReadonly)\n\t\tcoins := robanker.GetCoins(addr).AmountOf(\"ugnot\")\n\t\tfmt.Printf(\"| %-13s | addr=%s | wugnot=%-5d | ugnot=%-9d |\\n\",\n\t\t\tname, addr, wugnotBal, coins)\n\t}\n\tprintln(\"-----------\")\n\tprintSingleBalance(\"wugnot_test\", addrt)\n\tprintSingleBalance(\"wugnot\", addrc)\n\tprintSingleBalance(\"addr1\", addr1)\n\tprintln(\"-----------\")\n}\n\n// Output:\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=200000000 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=200000000 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=200004242 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=99995759 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "gnoblog", + "path": "gno.land/r/gnoland/blog", + "files": [ + { + "name": "admin.gno", + "body": "package gnoblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tcommenterList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // FIXME: delete instead?\n}\n\nfunc NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor {\n\tcallback := func() error {\n\t\taddPost(std.PrevRealm().Addr(), slug, title, body, publicationDate, authors, tags)\n\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\tcaller := std.GetOrigCaller()\n\taddPost(caller, slug, title, body, publicationDate, authors, tags)\n}\n\nfunc addPost(caller std.Address, slug, title, body, publicationDate, authors, tags string) {\n\tvar tagList []string\n\tif tags != \"\" {\n\t\ttagList = strings.Split(tags, \",\")\n\t}\n\tvar authorList []string\n\tif authors != \"\" {\n\t\tauthorList = strings.Split(authors, \",\")\n\t}\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModRemovePost(slug string) {\n\tassertIsModerator()\n\n\tb.RemovePost(slug)\n}\n\nfunc ModAddCommenter(addr std.Address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), true)\n}\n\nfunc ModDelCommenter(addr std.Address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), false) // FIXME: delete instead?\n}\n\nfunc ModDelComment(slug string, index int) {\n\tassertIsModerator()\n\n\terr := b.GetPost(slug).DeleteComment(index)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc isCommenter(addr std.Address) bool {\n\t_, found := commenterList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.GetOrigCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertIsCommenter() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) || isCommenter(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n" + }, + { + "name": "gnoblog.gno", + "body": "package gnoblog\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/blog\"\n)\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Gnoland's Blog\",\n\tPrefix: \"/r/gnoland/blog:\",\n}\n\nfunc AddComment(postSlug, comment string) {\n\tassertIsCommenter()\n\tassertNotInPause()\n\n\tcaller := std.GetOrigCaller()\n\terr := b.GetPost(postSlug).AddComment(caller, comment)\n\tcheckErr(err)\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n\nfunc RenderLastPostsWidget(limit int) string {\n\treturn b.RenderLastPostsWidget(limit)\n}\n\nfunc PostExists(slug string) bool {\n\tif b.GetPost(slug) == nil {\n\t\treturn false\n\t}\n\treturn true\n}\n" + }, + { + "name": "gnoblog_test.gno", + "body": "package gnoblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tstd.TestSetOrigCaller(std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"))\n\n\tauthor := std.GetOrigCaller()\n\n\t// by default, no posts.\n\t{\n\t\tgot := Render(\"\")\n\t\texpected := `\n# Gnoland's Blog\n\nNo posts.\n`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// create two posts, list post.\n\t{\n\t\tModAddPost(\"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\t\tModAddPost(\"slug2\", \"title2\", \"body2\", \"2022-05-20T13:17:23Z\", \"moul\", \"tag1,tag3\")\n\t\tgot := Render(\"\")\n\t\texpected := `\n\t# Gnoland's Blog\n\n\u003cdiv class='columns-3'\u003e\u003cdiv\u003e\n\n### [title2](/r/gnoland/blog:p/slug2)\n 20 May 2022\n\u003c/div\u003e\u003cdiv\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n 20 May 2022\n\u003c/div\u003e\u003c/div\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// view post.\n\t{\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\n\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)\n\nWritten by moul on 20 May 2022\n\nPublished by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003c/details\u003e\n\u003c/main\u003e\n\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// list by tags.\n\t{\n\t\tgot := Render(\"t/invalid\")\n\t\texpected := \"# [Gnoland's Blog](/r/gnoland/blog:) / t / invalid\\n\\nNo posts.\"\n\t\tassertMDEquals(t, got, expected)\n\n\t\tgot = Render(\"t/tag2\")\n\t\texpected = `\n# [Gnoland's Blog](/r/gnoland/blog:) / t / tag2\n\n\u003cdiv\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n 20 May 2022\n\u003c/div\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// add comments.\n\t{\n\t\tAddComment(\"slug1\", \"comment1\")\n\t\tAddComment(\"slug2\", \"comment2\")\n\t\tAddComment(\"slug1\", \"comment3\")\n\t\tAddComment(\"slug2\", \"comment4\")\n\t\tAddComment(\"slug1\", \"comment5\")\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)\n\nWritten by moul on 20 May 2022\n\nPublished by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003ch5\u003ecomment4\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// edit post.\n\t{\n\t\toldTitle := \"title2\"\n\t\toldDate := \"2022-05-20T13:17:23Z\"\n\n\t\tModEditPost(\"slug2\", oldTitle, \"body2++\", oldDate, \"manfred\", \"tag1,tag4\")\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2++\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4)\n\nWritten by manfred on 20 May 2022\n\nPublished by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003ch5\u003ecomment4\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\n\t\thome := Render(\"\")\n\n\t\tif strings.Count(home, oldTitle) != 1 {\n\t\t\tt.Errorf(\"post not edited properly\")\n\t\t}\n\t\t// Edits work everything except title, slug, and publicationDate\n\t\t// Edits to the above will cause duplication on the blog home page\n\t}\n\n\t{ // Test remove functionality\n\t\ttitle := \"example title\"\n\t\tslug := \"testSlug1\"\n\t\tModAddPost(slug, title, \"body1\", \"2022-05-25T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\n\t\tgot := Render(\"\")\n\n\t\tif !strings.Contains(got, title) {\n\t\t\tt.Errorf(\"post was not added properly\")\n\t\t}\n\n\t\tpostRender := Render(\"p/\" + slug)\n\n\t\tif !strings.Contains(postRender, title) {\n\t\t\tt.Errorf(\"post not rendered properly\")\n\t\t}\n\n\t\tModRemovePost(slug)\n\t\tgot = Render(\"\")\n\n\t\tif strings.Contains(got, title) {\n\t\t\tt.Errorf(\"post was not removed\")\n\t\t}\n\n\t\tpostRender = Render(\"p/\" + slug)\n\n\t\tassertMDEquals(t, postRender, \"404\")\n\t}\n\n\t// TODO: pagination.\n\t// TODO: ?format=...\n\n\t// all 404s\n\t{\n\t\tnotFoundPaths := []string{\n\t\t\t\"p/slug3\",\n\t\t\t\"p\",\n\t\t\t\"p/\",\n\t\t\t\"x/x\",\n\t\t\t\"t\",\n\t\t\t\"t/\",\n\t\t\t\"/\",\n\t\t\t\"p/slug1/\",\n\t\t}\n\t\tfor _, notFoundPath := range notFoundPaths {\n\t\t\tgot := Render(notFoundPath)\n\t\t\texpected := \"404\"\n\t\t\tif got != expected {\n\t\t\t\tt.Errorf(\"path %q: expected %q, got %q.\", notFoundPath, expected, got)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc assertMDEquals(t *testing.T, got, expected string) {\n\tt.Helper()\n\texpected = strings.TrimSpace(expected)\n\tgot = strings.TrimSpace(got)\n\tif expected != got {\n\t\tt.Errorf(\"invalid render output.\\nexpected %q.\\ngot %q.\", expected, got)\n\t}\n}\n" + }, + { + "name": "util.gno", + "body": "package gnoblog\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "events", + "path": "gno.land/r/gnoland/events", + "files": [ + { + "name": "administration.gno", + "body": "package events\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable/exts/authorizable\"\n)\n\nvar (\n\tsu = std.Address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\tauth = authorizable.NewAuthorizableWithAddress(su)\n)\n\n// GetOwner gets the owner of the events realm\nfunc GetOwner() std.Address {\n\treturn auth.Owner()\n}\n\n// AddModerator adds a moderator to the events realm\nfunc AddModerator(mod std.Address) {\n\tauth.AssertCallerIsOwner()\n\n\tif err := auth.AddToAuthList(mod); err != nil {\n\t\tpanic(err)\n\t}\n}\n" + }, + { + "name": "errors.gno", + "body": "package events\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n)\n\nvar (\n\tErrEmptyName = errors.New(\"event name cannot be empty\")\n\tErrNoSuchID = errors.New(\"event with specified ID does not exist\")\n\tErrMinWidgetSize = errors.New(\"you need to request at least 1 event to render\")\n\tErrMaxWidgetSize = errors.New(\"maximum number of events in widget is\" + strconv.Itoa(MaxWidgetSize))\n\tErrDescriptionTooLong = errors.New(\"event description is too long\")\n\tErrInvalidStartTime = errors.New(\"invalid start time format\")\n\tErrInvalidEndTime = errors.New(\"invalid end time format\")\n\tErrEndBeforeStart = errors.New(\"end time cannot be before start time\")\n\tErrStartEndTimezonemMismatch = errors.New(\"start and end timezones are not the same\")\n)\n" + }, + { + "name": "events.gno", + "body": "// Package events allows you to upload data about specific IRL/online events\n// It includes dynamic support for updating rendering events based on their\n// status, ie if they are upcoming, in progress, or in the past.\npackage events\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype (\n\tEvent struct {\n\t\tid string\n\t\tname string // name of event\n\t\tdescription string // short description of event\n\t\tlink string // link to auth corresponding web2 page, ie eventbrite/luma or conference page\n\t\tlocation string // location of the event\n\t\tstartTime time.Time // given in RFC3339\n\t\tendTime time.Time // end time of the event, given in RFC3339\n\t}\n\n\teventsSlice []*Event\n)\n\nvar (\n\tevents = make(eventsSlice, 0) // sorted\n\tidCounter seqid.ID\n)\n\nconst (\n\tmaxDescLength = 100\n\tEventAdded = \"EventAdded\"\n\tEventDeleted = \"EventDeleted\"\n\tEventEdited = \"EventEdited\"\n)\n\n// AddEvent adds auth new event\n// Start time \u0026 end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00\nfunc AddEvent(name, description, link, location, startTime, endTime string) (string, error) {\n\tauth.AssertOnAuthList()\n\n\tif strings.TrimSpace(name) == \"\" {\n\t\treturn \"\", ErrEmptyName\n\t}\n\n\tif len(description) \u003e maxDescLength {\n\t\treturn \"\", ufmt.Errorf(\"%s: provided length is %d, maximum is %d\", ErrDescriptionTooLong, len(description), maxDescLength)\n\t}\n\n\t// Parse times\n\tst, et, err := parseTimes(startTime, endTime)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid := idCounter.Next().String()\n\te := \u0026Event{\n\t\tid: id,\n\t\tname: name,\n\t\tdescription: description,\n\t\tlink: link,\n\t\tlocation: location,\n\t\tstartTime: st,\n\t\tendTime: et,\n\t}\n\n\tevents = append(events, e)\n\tsort.Sort(events)\n\n\tstd.Emit(EventAdded,\n\t\t\"id\", e.id,\n\t)\n\n\treturn id, nil\n}\n\n// DeleteEvent deletes an event with auth given ID\nfunc DeleteEvent(id string) {\n\tauth.AssertOnAuthList()\n\n\te, idx, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tevents = append(events[:idx], events[idx+1:]...)\n\n\tstd.Emit(EventDeleted,\n\t\t\"id\", e.id,\n\t)\n}\n\n// EditEvent edits an event with auth given ID\n// It only updates values corresponding to non-empty arguments sent with the call\n// Note: if you need to update the start time or end time, you need to provide both every time\nfunc EditEvent(id string, name, description, link, location, startTime, endTime string) {\n\tauth.AssertOnAuthList()\n\n\te, _, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Set only valid values\n\tif strings.TrimSpace(name) != \"\" {\n\t\te.name = name\n\t}\n\n\tif strings.TrimSpace(description) != \"\" {\n\t\te.description = description\n\t}\n\n\tif strings.TrimSpace(link) != \"\" {\n\t\te.link = link\n\t}\n\n\tif strings.TrimSpace(location) != \"\" {\n\t\te.location = location\n\t}\n\n\tif strings.TrimSpace(startTime) != \"\" || strings.TrimSpace(endTime) != \"\" {\n\t\tst, et, err := parseTimes(startTime, endTime)\n\t\tif err != nil {\n\t\t\tpanic(err) // need to also revert other state changes\n\t\t}\n\n\t\toldStartTime := e.startTime\n\t\te.startTime = st\n\t\te.endTime = et\n\n\t\t// If sort order was disrupted, sort again\n\t\tif oldStartTime != e.startTime {\n\t\t\tsort.Sort(events)\n\t\t}\n\t}\n\n\tstd.Emit(EventEdited,\n\t\t\"id\", e.id,\n\t)\n}\n\nfunc GetEventByID(id string) (*Event, int, error) {\n\tfor i, event := range events {\n\t\tif event.id == id {\n\t\t\treturn event, i, nil\n\t\t}\n\t}\n\n\treturn nil, -1, ErrNoSuchID\n}\n\n// Len returns the length of the slice\nfunc (m eventsSlice) Len() int {\n\treturn len(m)\n}\n\n// Less compares the startTime fields of two elements\n// In this case, events will be sorted by largest startTime first (upcoming \u003e past)\nfunc (m eventsSlice) Less(i, j int) bool {\n\treturn m[i].startTime.After(m[j].startTime)\n}\n\n// Swap swaps two elements in the slice\nfunc (m eventsSlice) Swap(i, j int) {\n\tm[i], m[j] = m[j], m[i]\n}\n\n// parseTimes parses the start and end time for an event and checks for possible errors\nfunc parseTimes(startTime, endTime string) (time.Time, time.Time, error) {\n\tst, err := time.Parse(time.RFC3339, startTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidStartTime, err.Error())\n\t}\n\n\tet, err := time.Parse(time.RFC3339, endTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidEndTime, err.Error())\n\t}\n\n\tif et.Before(st) {\n\t\treturn time.Time{}, time.Time{}, ErrEndBeforeStart\n\t}\n\n\t_, stOffset := st.Zone()\n\t_, etOffset := et.Zone()\n\tif stOffset != etOffset {\n\t\treturn time.Time{}, time.Time{}, ErrStartEndTimezonemMismatch\n\t}\n\n\treturn st, et, nil\n}\n" + }, + { + "name": "events_test.gno", + "body": "package events\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tsuRealm = std.NewUserRealm(su)\n\n\tnow = \"2009-02-13T23:31:30Z\" // time.Now() is hardcoded to this value in the gno test machine currently\n\tparsedTimeNow, _ = time.Parse(time.RFC3339, now)\n)\n\nfunc TestAddEvent(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tAddEvent(\"Event 1\", \"this event is upcoming\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"Event 1\") {\n\t\tt.Fatalf(\"Expected to find Event 1 in render\")\n\t}\n\n\te2Start := parsedTimeNow.Add(-time.Hour * 24 * 5)\n\te2End := e2Start.Add(time.Hour * 4)\n\n\tAddEvent(\"Event 2\", \"this event is in the past\", \"gno.land\", \"gnome land\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\n\tgot = renderHome(false)\n\n\tupcomingPos := strings.Index(got, \"## Upcoming events\")\n\tpastPos := strings.Index(got, \"## Past events\")\n\n\te1Pos := strings.Index(got, \"Event 1\")\n\te2Pos := strings.Index(got, \"Event 2\")\n\n\t// expected index ordering: upcoming \u003c e1 \u003c past \u003c e2\n\tif e1Pos \u003c upcomingPos || e1Pos \u003e pastPos {\n\t\tt.Fatalf(\"Expected to find Event 1 in Upcoming events\")\n\t}\n\n\tif e2Pos \u003c upcomingPos || e2Pos \u003c pastPos || e2Pos \u003c e1Pos {\n\t\tt.Fatalf(\"Expected to find Event 2 on auth different pos\")\n\t}\n\n\t// larger index =\u003e smaller startTime (future =\u003e past)\n\tif events[0].startTime.Unix() \u003c events[1].startTime.Unix() {\n\t\tt.Fatalf(\"expected ordering to be different\")\n\t}\n}\n\nfunc TestAddEventErrors(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\t_, err := AddEvent(\"\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorIs(t, err, ErrEmptyName)\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:30:31Z\")\n\tuassert.ErrorIs(t, err, ErrEndBeforeStart)\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31+06:00\", \"2009-02-13T23:33:31+02:00\")\n\tuassert.ErrorIs(t, err, ErrStartEndTimezonemMismatch)\n\n\ttooLongDesc := `Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean ma`\n\t_, err = AddEvent(\"sample name\", tooLongDesc, \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrDescriptionTooLong.Error())\n}\n\nfunc TestDeleteEvent(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tid, _ := AddEvent(\"ToDelete\", \"description\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Expected to find ToDelete event in render\")\n\t}\n\n\tDeleteEvent(id)\n\tgot = renderHome(false)\n\n\tif strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Did not expect to find ToDelete event in render\")\n\t}\n}\n\nfunc TestEditEvent(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\tloc := \"gnome land\"\n\n\tid, _ := AddEvent(\"ToDelete\", \"description\", \"gno.land\", loc, e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tnewName := \"New Name\"\n\tnewDesc := \"Normal description\"\n\tnewLink := \"new Link\"\n\tnewST := e1Start.Add(time.Hour)\n\tnewET := newST.Add(time.Hour)\n\n\tEditEvent(id, newName, newDesc, newLink, \"\", newST.Format(time.RFC3339), newET.Format(time.RFC3339))\n\tedited, _, _ := GetEventByID(id)\n\n\t// Check updated values\n\tuassert.Equal(t, edited.name, newName)\n\tuassert.Equal(t, edited.description, newDesc)\n\tuassert.Equal(t, edited.link, newLink)\n\tuassert.True(t, edited.startTime.Equal(newST))\n\tuassert.True(t, edited.endTime.Equal(newET))\n\n\t// Check if the old values are the same\n\tuassert.Equal(t, edited.location, loc)\n}\n\nfunc TestInvalidEdit(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\tuassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() {\n\t\tEditEvent(\"123123\", \"\", \"\", \"\", \"\", \"\", \"\")\n\t})\n}\n\nfunc TestParseTimes(t *testing.T) {\n\t// times not provided\n\t// end time before start time\n\t// timezone Missmatch\n\n\t_, _, err := parseTimes(\"\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, _, err = parseTimes(now, \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, _, err = parseTimes(\"2009-02-13T23:30:30Z\", \"2009-02-13T21:30:30Z\")\n\tuassert.ErrorContains(t, err, ErrEndBeforeStart.Error())\n\n\t_, _, err = parseTimes(\"2009-02-10T23:30:30+02:00\", \"2009-02-13T21:30:33+05:00\")\n\tuassert.ErrorContains(t, err, ErrStartEndTimezonemMismatch.Error())\n}\n\nfunc TestRenderEventWidget(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\t// No events yet\n\tevents = nil\n\tout, err := RenderEventWidget(1)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, out, \"No events.\")\n\n\t// Too many events\n\tout, err = RenderEventWidget(MaxWidgetSize + 1)\n\tuassert.ErrorIs(t, err, ErrMaxWidgetSize)\n\n\t// Too little events\n\tout, err = RenderEventWidget(0)\n\tuassert.ErrorIs(t, err, ErrMinWidgetSize)\n\n\t// Ordering \u0026 if requested amt is larger than the num of events that exist\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\te2Start := parsedTimeNow.Add(time.Hour * 24 * 10) // event 2 is after event 1\n\te2End := e2Start.Add(time.Hour * 4)\n\n\t_, err = AddEvent(\"Event 1\", \"description\", \"gno.land\", \"loc\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\t_, err = AddEvent(\"Event 2\", \"description\", \"gno.land\", \"loc\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\tout, err = RenderEventWidget(MaxWidgetSize)\n\turequire.NoError(t, err)\n\n\tuniqueSequence := \"- [\" // sequence that is displayed once per each event as per the RenderEventWidget function\n\tuassert.Equal(t, 2, strings.Count(out, uniqueSequence))\n\n\tuassert.True(t, strings.Index(out, \"Event 1\") \u003e strings.Index(out, \"Event 2\"))\n}\n" + }, + { + "name": "rendering.gno", + "body": "package events\n\nimport (\n\t\"bytes\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tMaxWidgetSize = 5\n)\n\n// RenderEventWidget shows up to eventsToRender of the latest events to a caller\nfunc RenderEventWidget(eventsToRender int) (string, error) {\n\tnumOfEvents := len(events)\n\tif numOfEvents == 0 {\n\t\treturn \"No events.\", nil\n\t}\n\n\tif eventsToRender \u003e MaxWidgetSize {\n\t\treturn \"\", ErrMaxWidgetSize\n\t}\n\n\tif eventsToRender \u003c 1 {\n\t\treturn \"\", ErrMinWidgetSize\n\t}\n\n\tif eventsToRender \u003e numOfEvents {\n\t\teventsToRender = numOfEvents\n\t}\n\n\toutput := \"\"\n\n\tfor _, event := range events[:eventsToRender] {\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", event.name, event.link)\n\t}\n\n\treturn output, nil\n}\n\n// renderHome renders the home page of the events realm\nfunc renderHome(admin bool) string {\n\toutput := \"# gno.land events\\n\\n\"\n\n\tif len(events) == 0 {\n\t\toutput += \"No upcoming or past events.\"\n\t\treturn output\n\t}\n\n\toutput += \"Below is a list of all gno.land events, including in progress, upcoming, and past ones.\\n\\n\"\n\toutput += \"---\\n\\n\"\n\n\tvar (\n\t\tinProgress = \"\"\n\t\tupcoming = \"\"\n\t\tpast = \"\"\n\t\tnow = time.Now()\n\t)\n\n\tfor _, e := range events {\n\t\tif now.Before(e.startTime) {\n\t\t\tupcoming += e.Render(admin)\n\t\t} else if now.After(e.endTime) {\n\t\t\tpast += e.Render(admin)\n\t\t} else {\n\t\t\tinProgress += e.Render(admin)\n\t\t}\n\t}\n\n\tif upcoming != \"\" {\n\t\t// Add upcoming events\n\t\toutput += \"## Upcoming events\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += upcoming\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif inProgress != \"\" {\n\t\toutput += \"## Currently in progress\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += inProgress\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif past != \"\" {\n\t\t// Add past events\n\t\toutput += \"## Past events\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += past\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t}\n\n\treturn output\n}\n\n// Render returns the markdown representation of a single event instance\nfunc (e Event) Render(admin bool) string {\n\tvar buf bytes.Buffer\n\n\tbuf.WriteString(\"\u003cdiv\u003e\\n\\n\")\n\tbuf.WriteString(ufmt.Sprintf(\"### %s\\n\\n\", e.name))\n\tbuf.WriteString(ufmt.Sprintf(\"%s\\n\\n\", e.description))\n\tbuf.WriteString(ufmt.Sprintf(\"**Location:** %s\\n\\n\", e.location))\n\n\t_, offset := e.startTime.Zone() // offset is in seconds\n\thoursOffset := offset / (60 * 60)\n\tsign := \"\"\n\tif offset \u003e= 0 {\n\t\tsign = \"+\"\n\t}\n\n\tbuf.WriteString(ufmt.Sprintf(\"**Starts:** %s UTC%s%d\\n\\n\", e.startTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\tbuf.WriteString(ufmt.Sprintf(\"**Ends:** %s UTC%s%d\\n\\n\", e.endTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\n\tif admin {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[EDIT](/r/gnoland/events$help\u0026func=EditEvent\u0026id=%s)\\n\\n\", e.id))\n\t\tbuf.WriteString(ufmt.Sprintf(\"[DELETE](/r/gnoland/events$help\u0026func=DeleteEvent\u0026id=%s)\\n\\n\", e.id))\n\t}\n\n\tif e.link != \"\" {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[See more](%s)\\n\\n\", e.link))\n\t}\n\n\tbuf.WriteString(\"\u003c/div\u003e\")\n\n\treturn buf.String()\n}\n\n// Render is the main rendering entry point\nfunc Render(path string) string {\n\tif path == \"admin\" {\n\t\treturn renderHome(true)\n\t}\n\n\treturn renderHome(false)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "faucet", + "path": "gno.land/r/gnoland/faucet", + "files": [ + { + "name": "admin.gno", + "body": "package faucet\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nfunc AdminSetInPause(inPause bool) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgInPause = inPause\n\treturn \"\"\n}\n\nfunc AdminSetMessage(message string) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgMessage = message\n\treturn \"\"\n}\n\nfunc AdminSetTransferLimit(amount int64) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgLimit = std.NewCoin(\"ugnot\", amount)\n\treturn \"\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgAdminAddr = addr\n\treturn \"\"\n}\n\nfunc AdminAddController(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tsize := gControllers.Size()\n\n\tif size \u003e= gControllersMaxSize {\n\t\treturn \"can not add more controllers than allowed\"\n\t}\n\n\tif gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" exists, no need to add.\"\n\t}\n\n\tgControllers.Set(addr.String(), addr)\n\n\treturn \"\"\n}\n\nfunc AdminRemoveController(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif !gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\t_, ok := gControllers.Remove(addr.String())\n\n\t// it not should happen.\n\t// we will check anyway to prevent issues in the underline implementation.\n\n\tif !ok {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\treturn \"\"\n}\n\nfunc assertIsAdmin() error {\n\tcaller := std.GetOrigCaller()\n\tif caller != gAdminAddr {\n\t\treturn errors.New(\"restricted for admin\")\n\t}\n\treturn nil\n}\n" + }, + { + "name": "faucet.gno", + "body": "package faucet\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\t// configurable by admin.\n\tgAdminAddr std.Address = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tgControllers = avl.NewTree()\n\tgControllersMaxSize = 10 // limit it to 10\n\tgInPause = false\n\tgMessage = \"# Community Faucet.\\n\\n\"\n\n\t// internal vars, for stats.\n\tgTotalTransferred std.Coins\n\tgTotalTransfers = uint(0)\n\n\t// per request limit, 350 gnot\n\tgLimit std.Coin = std.NewCoin(\"ugnot\", 350000000)\n)\n\nfunc Transfer(to std.Address, send int64) string {\n\tif err := assertIsController(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif gInPause {\n\t\treturn errors.New(\"faucet in pause\").Error()\n\t}\n\n\t// limit the per request\n\tif send \u003e gLimit.Amount {\n\t\treturn errors.New(\"Per request limit \" + gLimit.String() + \" exceed\").Error()\n\t}\n\tsendCoins := std.Coins{std.NewCoin(\"ugnot\", send)}\n\n\tgTotalTransferred = gTotalTransferred.Add(sendCoins)\n\tgTotalTransfers++\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tpkgaddr := std.CurrentRealm().Addr()\n\tbanker.SendCoins(pkgaddr, to, sendCoins)\n\treturn \"\"\n}\n\nfunc GetPerTransferLimit() int64 {\n\treturn gLimit.Amount\n}\n\nfunc Render(_ string) string {\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tbalance := banker.GetCoins(std.CurrentRealm().Addr())\n\n\toutput := gMessage\n\tif gInPause {\n\t\toutput += \"Status: inactive.\\n\"\n\t} else {\n\t\toutput += \"Status: active.\\n\"\n\t}\n\toutput += ufmt.Sprintf(\"Balance: %s.\\n\", balance.String())\n\toutput += ufmt.Sprintf(\"Total transfers: %s (in %d times).\\n\\n\", gTotalTransferred.String(), gTotalTransfers)\n\n\toutput += \"Package address: \" + std.CurrentRealm().Addr().String() + \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Admin: %s\\n\\n \", gAdminAddr.String())\n\toutput += ufmt.Sprintf(\"Controllers:\\n\\n \")\n\n\tfor i := 0; i \u003c gControllers.Size(); i++ {\n\t\t_, v := gControllers.GetByIndex(i)\n\t\toutput += ufmt.Sprintf(\"%s \", v.(std.Address))\n\t}\n\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Per request limit: %s\\n\\n\", gLimit.String())\n\n\treturn output\n}\n\nfunc assertIsController() error {\n\tcaller := std.GetOrigCaller()\n\n\tok := gControllers.Has(caller.String())\n\tif !ok {\n\t\treturn errors.New(caller.String() + \" is not on the controller list\")\n\t}\n\treturn nil\n}\n" + }, + { + "name": "faucet_test.gno", + "body": "package faucet\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tfaucetaddr = std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\tcontrolleraddr3 = testutils.TestAddress(\"controller3\")\n\t\tcontrolleraddr4 = testutils.TestAddress(\"controller4\")\n\t\tcontrolleraddr5 = testutils.TestAddress(\"controller5\")\n\t\tcontrolleraddr6 = testutils.TestAddress(\"controller6\")\n\t\tcontrolleraddr7 = testutils.TestAddress(\"controller7\")\n\t\tcontrolleraddr8 = testutils.TestAddress(\"controller8\")\n\t\tcontrolleraddr9 = testutils.TestAddress(\"controller9\")\n\t\tcontrolleraddr10 = testutils.TestAddress(\"controller10\")\n\t\tcontrolleraddr11 = testutils.TestAddress(\"controller11\")\n\n\t\ttest1addr = testutils.TestAddress(\"test1\")\n\t)\n\t// deposit 1000gnot to faucet contract\n\tstd.TestIssueCoins(faucetaddr, std.Coins{{\"ugnot\", 1000000000}})\n\tassertBalance(t, faucetaddr, 1200000000)\n\n\t// by default, balance is empty, and as a user I cannot call Transfer, or Admin commands.\n\n\tassertBalance(t, test1addr, 0)\n\tstd.TestSetOrigCaller(test1addr)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\tassertErr(t, faucet.AdminAddController(controlleraddr1))\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\t// as an admin, add the controller to contract and deposit more 2000gnot to contract\n\tstd.TestSetOrigCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr1))\n\tassertBalance(t, faucetaddr, 1200000000)\n\n\t// now, send some tokens as controller.\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertNoErr(t, faucet.Transfer(test1addr, 1000000))\n\tassertBalance(t, test1addr, 1000000)\n\tassertNoErr(t, faucet.Transfer(test1addr, 1000000))\n\tassertBalance(t, test1addr, 2000000)\n\tassertBalance(t, faucetaddr, 1198000000)\n\n\t// remove controller\n\t// as an admin, remove controller\n\tstd.TestSetOrigCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminRemoveController(controlleraddr1))\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\t// duplicate controller\n\tstd.TestSetOrigCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr1))\n\tassertErr(t, faucet.AdminAddController(controlleraddr1))\n\t// add more than more than allowed controllers\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr2))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr3))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr4))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr5))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr6))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr7))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr8))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr9))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr10))\n\tassertErr(t, faucet.AdminAddController(controlleraddr11))\n\n\t// send more than per transfer limit\n\tstd.TestSetOrigCaller(adminaddr)\n\tfaucet.AdminSetTransferLimit(300000000)\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 301000000))\n\n\t// block transefer from the address not on the controllers list.\n\tstd.TestSetOrigCaller(controlleraddr11)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n}\n\nfunc assertErr(t *testing.T, err string) {\n\tt.Helper()\n\n\tif err == \"\" {\n\t\tt.Logf(\"info: got err: %v\", err)\n\t\tt.Errorf(\"expected an error, got nil.\")\n\t}\n}\n\nfunc assertNoErr(t *testing.T, err string) {\n\tt.Helper()\n\tif err != \"\" {\n\t\tt.Errorf(\"got err: %v.\", err)\n\t}\n}\n\nfunc assertBalance(t *testing.T, addr std.Address, expectedBal int64) {\n\tt.Helper()\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(addr)\n\tgot := coins.AmountOf(\"ugnot\")\n\n\tif expectedBal != got {\n\t\tt.Errorf(\"invalid balance: expected %d, got %d.\", expectedBal, got)\n\t}\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with empty path and no controllers\nfunc main() {\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with a path and no controllers\nfunc main() {\n\tprintln(faucet.Render(\"path\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with empty path and 2 controllers\nfunc main() {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t)\n\tstd.TestSetOrigCaller(adminaddr)\n\terr := faucet.AdminAddController(controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n" + }, + { + "name": "z3_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints coints to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with 2 controllers and 2 transfers\nfunc main() {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\ttestaddr1 = testutils.TestAddress(\"test1\")\n\t\ttestaddr2 = testutils.TestAddress(\"test2\")\n\t)\n\tstd.TestSetOrigCaller(adminaddr)\n\terr := faucet.AdminAddController(controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tstd.TestSetOrigCaller(controlleraddr1)\n\terr = faucet.Transfer(testaddr1, 1000000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tstd.TestSetOrigCaller(controlleraddr2)\n\terr = faucet.Transfer(testaddr1, 2000000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 197000000ugnot.\n// Total transfers: 3000000ugnot (in 2 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "ghverify", + "path": "gno.land/r/gnoland/ghverify", + "files": [ + { + "name": "README.md", + "body": "# ghverify\n\nThis realm is intended to enable off chain gno address to github handle verification.\nThe steps are as follows:\n- A user calls `RequestVerification` and provides a github handle. This creates a new static oracle feed.\n- An off-chain agent controlled by the owner of this realm requests current feeds using the `GnorkleEntrypoint` function and provides a message of `\"request\"`\n- The agent receives the task information that includes the github handle and the gno address. It performs the verification step by checking whether this github user has the address in a github repository it controls.\n- The agent publishes the result of the verification by calling `GnorkleEntrypoint` with a message structured like: `\"ingest,\u003ctask id\u003e,\u003cverification status\u003e\"`. The verification status is `OK` if verification succeeded and any other value if it failed.\n- The oracle feed's ingester processes the verification and the handle to address mapping is written to the avl trees that exist as ghverify realm variables." + }, + { + "name": "contract.gno", + "body": "package ghverify\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\nconst (\n\t// The agent should send this value if it has verified the github handle.\n\tverifiedResult = \"OK\"\n)\n\nvar (\n\townerAddress = std.GetOrigCaller()\n\toracle *gnorkle.Instance\n\tpostHandler postGnorkleMessageHandler\n\n\thandleToAddressMap = avl.NewTree()\n\taddressToHandleMap = avl.NewTree()\n)\n\nfunc init() {\n\toracle = gnorkle.NewInstance()\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\ntype postGnorkleMessageHandler struct{}\n\n// Handle does post processing after a message is ingested by the oracle feed. It extracts the value to realm\n// storage and removes the feed from the oracle.\nfunc (h postGnorkleMessageHandler) Handle(i *gnorkle.Instance, funcType message.FuncType, feed gnorkle.Feed) error {\n\tif funcType != message.FuncTypeIngest {\n\t\treturn nil\n\t}\n\n\tresult, _, consumable := feed.Value()\n\tif !consumable {\n\t\treturn nil\n\t}\n\n\t// The value is consumable, meaning the ingestion occurred, so we can remove the feed from the oracle\n\t// after saving it to realm storage.\n\tdefer oracle.RemoveFeed(feed.ID())\n\n\t// Couldn't verify; nothing to do.\n\tif result.String != verifiedResult {\n\t\treturn nil\n\t}\n\n\tfeedTasks := feed.Tasks()\n\tif len(feedTasks) != 1 {\n\t\treturn errors.New(\"expected feed to have exactly one task\")\n\t}\n\n\ttask, ok := feedTasks[0].(*verificationTask)\n\tif !ok {\n\t\treturn errors.New(\"expected ghverify task\")\n\t}\n\n\thandleToAddressMap.Set(task.githubHandle, task.gnoAddress)\n\taddressToHandleMap.Set(task.gnoAddress, task.githubHandle)\n\treturn nil\n}\n\n// RequestVerification creates a new static feed with a single task that will\n// instruct an agent to verify the github handle / gno address pair.\nfunc RequestVerification(githubHandle string) {\n\tgnoAddress := string(std.GetOrigCaller())\n\tif err := oracle.AddFeeds(\n\t\tstatic.NewSingleValueFeed(\n\t\t\tgnoAddress,\n\t\t\t\"string\",\n\t\t\t\u0026verificationTask{\n\t\t\t\tgnoAddress: gnoAddress,\n\t\t\t\tgithubHandle: githubHandle,\n\t\t\t},\n\t\t),\n\t); err != nil {\n\t\tpanic(err)\n\t}\n\tstd.Emit(\n\t\t\"verification_requested\",\n\t\t\"from\", gnoAddress,\n\t\t\"handle\", githubHandle,\n\t)\n}\n\n// GnorkleEntrypoint is the entrypoint to the gnorkle oracle handler.\nfunc GnorkleEntrypoint(message string) string {\n\tresult, err := oracle.HandleMessage(message, postHandler)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn result\n}\n\n// SetOwner transfers ownership of the contract to the given address.\nfunc SetOwner(owner std.Address) {\n\tif ownerAddress != std.GetOrigCaller() {\n\t\tpanic(\"only the owner can set a new owner\")\n\t}\n\n\townerAddress = owner\n\n\t// In the context of this contract, the owner is the only one that can\n\t// add new feeds to the oracle.\n\toracle.ClearWhitelist(\"\")\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\n// GetHandleByAddress returns the github handle associated with the given gno address.\nfunc GetHandleByAddress(address string) string {\n\tif value, ok := addressToHandleMap.Get(address); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// GetAddressByHandle returns the gno address associated with the given github handle.\nfunc GetAddressByHandle(handle string) string {\n\tif value, ok := handleToAddressMap.Get(handle); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// Render returns a json object string will all verified handle -\u003e address mappings.\nfunc Render(_ string) string {\n\tresult := \"{\"\n\tvar appendComma bool\n\thandleToAddressMap.Iterate(\"\", \"\", func(handle string, address interface{}) bool {\n\t\tif appendComma {\n\t\t\tresult += \",\"\n\t\t}\n\n\t\tresult += `\"` + handle + `\": \"` + address.(string) + `\"`\n\t\tappendComma = true\n\n\t\treturn false\n\t})\n\n\treturn result + \"}\"\n}\n" + }, + { + "name": "contract_test.gno", + "body": "package ghverify\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestVerificationLifecycle(t *testing.T) {\n\tdefaultAddress := std.GetOrigCaller()\n\tuser1Address := std.Address(testutils.TestAddress(\"user 1\"))\n\tuser2Address := std.Address(testutils.TestAddress(\"user 2\"))\n\n\t// Verify request returns no feeds.\n\tresult := GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\tstd.TestSetOrigCaller(user1Address)\n\tRequestVerification(\"deelawn\")\n\n\t// A subsequent request from the same address should panic because there is\n\t// already a feed with an ID of this user's address.\n\tvar errMsg string\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tRequestVerification(\"deelawn\")\n\t}()\n\tif errMsg != \"feed already exists\" {\n\t\tt.Fatalf(\"expected feed already exists, got %s\", errMsg)\n\t}\n\n\t// Verify the request returns no feeds for this non-whitelisted user.\n\tresult = GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\tstd.TestSetOrigCaller(user2Address)\n\tRequestVerification(\"omarsy\")\n\n\t// Set the caller back to the whitelisted user and verify that the feed data\n\t// returned matches what should have been created by the `RequestVerification`\n\t// invocation.\n\tstd.TestSetOrigCaller(defaultAddress)\n\tresult = GnorkleEntrypoint(\"request\")\n\texpResult := `[{\"id\":\"` + string(user1Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user1Address) + `\",\"github_handle\":\"deelawn\"}]},` +\n\t\t`{\"id\":\"` + string(user2Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user2Address) + `\",\"github_handle\":\"omarsy\"}]}]`\n\tif result != expResult {\n\t\tt.Fatalf(\"expected request result %s, got %s\", expResult, result)\n\t}\n\n\t// Try to trigger feed ingestion from the non-authorized user.\n\tstd.TestSetOrigCaller(user1Address)\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tGnorkleEntrypoint(\"ingest,\" + string(user1Address) + \",OK\")\n\t}()\n\tif errMsg != \"caller not whitelisted\" {\n\t\tt.Fatalf(\"expected caller not whitelisted, got %s\", errMsg)\n\t}\n\n\t// Set the caller back to the whitelisted user and transfer contract ownership.\n\tstd.TestSetOrigCaller(defaultAddress)\n\tSetOwner(defaultAddress)\n\n\t// Now trigger the feed ingestion from the user and new owner and only whitelisted address.\n\tGnorkleEntrypoint(\"ingest,\" + string(user1Address) + \",OK\")\n\tGnorkleEntrypoint(\"ingest,\" + string(user2Address) + \",OK\")\n\n\t// Verify the ingestion autocommitted the value and triggered the post handler.\n\tdata := Render(\"\")\n\texpResult = `{\"deelawn\": \"` + string(user1Address) + `\",\"omarsy\": \"` + string(user2Address) + `\"}`\n\tif data != expResult {\n\t\tt.Fatalf(\"expected render data %s, got %s\", expResult, data)\n\t}\n\n\t// Finally make sure the feed was cleaned up after the data was committed.\n\tresult = GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Check that the accessor functions are working as expected.\n\tif handle := GetHandleByAddress(string(user1Address)); handle != \"deelawn\" {\n\t\tt.Fatalf(\"expected deelawn, got %s\", handle)\n\t}\n\tif address := GetAddressByHandle(\"deelawn\"); address != string(user1Address) {\n\t\tt.Fatalf(\"expected %s, got %s\", string(user1Address), address)\n\t}\n}\n" + }, + { + "name": "task.gno", + "body": "package ghverify\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n)\n\ntype verificationTask struct {\n\tgnoAddress string\n\tgithubHandle string\n}\n\n// MarshalJSON marshals the task contents to JSON.\nfunc (t *verificationTask) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write(\n\t\t[]byte(`{\"gno_address\":\"` + t.gnoAddress + `\",\"github_handle\":\"` + t.githubHandle + `\"}`),\n\t)\n\n\tw.Flush()\n\treturn buf.Bytes(), nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "home", + "path": "gno.land/r/gnoland/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/ui\"\n\tblog \"gno.land/r/gnoland/blog\"\n\tevents \"gno.land/r/gnoland/events\"\n)\n\n// XXX: p/demo/ui API is crappy, we need to make it more idiomatic\n// XXX: use an updatable block system to update content from a DAO\n// XXX: var blocks avl.Tree\n\nvar (\n\toverride string\n\tadmin = ownable.NewWithAddress(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\") // @manfred by default\n)\n\nfunc Render(_ string) string {\n\tif override != \"\" {\n\t\treturn override\n\t}\n\n\tdom := ui.DOM{Prefix: \"r/gnoland/home:\"}\n\tdom.Title = \"Welcome to gno.land\"\n\tdom.Classes = []string{\"gno-tmpl-section\"}\n\n\t// body\n\tdom.Body.Append(introSection()...)\n\n\tdom.Body.Append(ui.Jumbotron(discoverLinks()))\n\n\tdom.Body.Append(\n\t\tui.Columns{3, []ui.Element{\n\t\t\tlastBlogposts(4),\n\t\t\tupcomingEvents(),\n\t\t\tlastContributions(4),\n\t\t}},\n\t)\n\n\tdom.Body.Append(ui.HR{})\n\tdom.Body.Append(playgroundSection()...)\n\tdom.Body.Append(ui.HR{})\n\tdom.Body.Append(packageStaffPicks()...)\n\tdom.Body.Append(ui.HR{})\n\tdom.Body.Append(worxDAO()...)\n\tdom.Body.Append(ui.HR{})\n\t// footer\n\tdom.Footer.Append(\n\t\tui.Columns{2, []ui.Element{\n\t\t\tsocialLinks(),\n\t\t\tquoteOfTheBlock(),\n\t\t}},\n\t)\n\n\t// Testnet disclaimer\n\tdom.Footer.Append(\n\t\tui.HR{},\n\t\tui.Bold(\"This is a testnet.\"),\n\t\tui.Text(\"Package names are not guaranteed to be available for production.\"),\n\t)\n\n\treturn dom.String()\n}\n\nfunc lastBlogposts(limit int) ui.Element {\n\tposts := blog.RenderLastPostsWidget(limit)\n\treturn ui.Element{\n\t\tui.H3(\"[Latest Blogposts](/r/gnoland/blog)\"),\n\t\tui.Text(posts),\n\t}\n}\n\nfunc lastContributions(limit int) ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"Latest Contributions\"),\n\t\t// TODO: import r/gh to\n\t\tui.Link{Text: \"View latest contributions\", URL: \"https://github.com/gnolang/gno/pulls\"},\n\t}\n}\n\nfunc upcomingEvents() ui.Element {\n\tout, _ := events.RenderEventWidget(events.MaxWidgetSize)\n\treturn ui.Element{\n\t\tui.H3(\"[Latest Events](/r/gnoland/events)\"),\n\t\tui.Text(out),\n\t}\n}\n\nfunc introSection() ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts.\"),\n\t\tui.Paragraph(\"With transparent and timeless code, gno.land is the next generation of smart contract platforms, serving as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse.\"),\n\t\tui.Paragraph(\"Intuitive and easy to use, gno.land lowers the barrier to web3 and makes censorship-resistant platforms accessible to everyone. If you want to help lay the foundations of a fairer and freer world, join us today.\"),\n\t}\n}\n\nfunc worxDAO() ui.Element {\n\t// WorxDAO\n\t// XXX(manfred): please, let me finish a v0, then we can iterate\n\t// highest level == highest responsibility\n\t// teams are responsible for components they don't owne\n\t// flag : realm maintainers VS facilitators\n\t// teams\n\t// committee of trustees to create the directory\n\t// each directory is a name, has a parent and have groups\n\t// homepage team - blocks aggregating events\n\t// XXX: TODO\n\t/*`\n\t# Directory\n\n\t* gno.land (owned by group)\n\t *\n\t* gnovm\n\t * gnolang (language)\n\t * gnovm\n\t - current challenges / concerns / issues\n\t* tm2\n\t * amino\n\t *\n\n\t## Contributors\n\t``*/\n\treturn ui.Element{\n\t\tui.H3(\"Contributions (WorxDAO \u0026 GoR)\"),\n\t\t// TODO: GoR dashboard + WorxDAO topics\n\t\tui.Text(`coming soon`),\n\t}\n}\n\nfunc quoteOfTheBlock() ui.Element {\n\tquotes := []string{\n\t\t\"Gno is for Truth.\",\n\t\t\"Gno is for Social Coordination.\",\n\t\t\"Gno is _not only_ for DeFi.\",\n\t\t\"Now, you Gno.\",\n\t\t\"Come for the Go, Stay for the Gno.\",\n\t}\n\theight := std.GetHeight()\n\tidx := int(height) % len(quotes)\n\tqotb := quotes[idx]\n\n\treturn ui.Element{\n\t\tui.H3(ufmt.Sprintf(\"Quote of the ~Day~ Block#%d\", height)),\n\t\tui.Quote(qotb),\n\t}\n}\n\nfunc socialLinks() ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"Socials\"),\n\t\tui.BulletList{\n\t\t\t// XXX: improve UI to support a nice GO api for such links\n\t\t\tui.Text(\"Check out our [community projects](https://github.com/gnolang/awesome-gno)\"),\n\t\t\tui.Text(\"![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)\"),\n\t\t\tui.Text(\"![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)\"),\n\t\t\tui.Text(\"![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)\"),\n\t\t\tui.Text(\"![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)\"),\n\t\t},\n\t}\n}\n\nfunc playgroundSection() ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"[Gno Playground](https://play.gno.land)\"),\n\t\tui.Paragraph(`Gno Playground is a web application designed for building, running, testing, and interacting\nwith your Gno code, enhancing your understanding of the Gno language. With Gno Playground, you can share your code,\nexecute tests, deploy your realms and packages to gno.land, and explore a multitude of other features.`),\n\t\tui.Paragraph(\"Experience the convenience of code sharing and rapid experimentation with [Gno Playground](https://play.gno.land).\"),\n\t}\n}\n\nfunc packageStaffPicks() ui.Element {\n\t// XXX: make it modifiable from a DAO\n\treturn ui.Element{\n\t\tui.H3(\"Explore New Packages and Realms\"),\n\t\tui.Columns{\n\t\t\t3,\n\t\t\t[]ui.Element{\n\t\t\t\t{\n\t\t\t\t\tui.H4(\"[r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/blog\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/dao\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/faucet\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/home\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/pages\"},\n\t\t\t\t\t},\n\t\t\t\t\tui.H4(\"[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"r/sys/names\"},\n\t\t\t\t\t\tui.Link{URL: \"r/sys/rewards\"},\n\t\t\t\t\t\tui.Link{URL: \"/r/sys/validators/v2\"},\n\t\t\t\t\t},\n\t\t\t\t}, {\n\t\t\t\t\tui.H4(\"[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"r/demo/boards\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/users\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/banktest\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/foo20\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/foo721\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/microblog\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/nft\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/types\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/art/gnoface\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/art/millipede\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/groups\"},\n\t\t\t\t\t\tui.Text(\"...\"),\n\t\t\t\t\t},\n\t\t\t\t}, {\n\t\t\t\t\tui.H4(\"[p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"p/demo/avl\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/blog\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/ui\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/ufmt\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/merkle\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/bf\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/flow\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/gnode\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/grc/grc20\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/grc/grc721\"},\n\t\t\t\t\t\tui.Text(\"...\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc discoverLinks() ui.Element {\n\treturn ui.Element{\n\t\tui.Text(`\u003cdiv class=\"columns-3\"\u003e\n\u003cdiv class=\"column\"\u003e\n\n### Learn about gno.land\n\n- [About](/about)\n- [GitHub](https://github.com/gnolang)\n- [Blog](/blog)\n- [Events](/events)\n- Tokenomics (soon)\n- [Partners, Fund, Grants](/partners)\n- [Explore the Ecosystem](/ecosystem)\n- [Careers](https://jobs.ashbyhq.com/allinbits)\n\n\u003c/div\u003e\u003c!-- end column--\u003e\n\n\u003cdiv class=\"column\"\u003e\n\n### Build with Gno\n\n- [Write Gno in the browser](https://play.gno.land)\n- [Read about the Gno Language](/gnolang)\n- [Visit the official documentation](https://docs.gno.land)\n- [Gno by Example](https://gno-by-example.com/)\n- [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev)\n- [Get testnet GNOTs](https://faucet.gno.land)\n\n\u003c/div\u003e\u003c!-- end column--\u003e\n\u003cdiv class=\"column\"\u003e\n\n### Explore the universe\n\n- [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n- [Gnoscan](https://gnoscan.io)\n- [Portal Loop](https://docs.gno.land/concepts/portal-loop)\n- [Testnet 4](https://test4.gno.land/)\n- Testnet Faucet Hub (soon)\n\n\u003c/div\u003e\u003c!-- end column--\u003e\n\u003c/div\u003e\u003c!-- end columns-3--\u003e`),\n\t}\n}\n\nfunc AdminSetOverride(content string) {\n\tadmin.AssertCallerIsOwner()\n\toverride = content\n}\n\nfunc AdminTransferOwnership(newAdmin std.Address) {\n\tadmin.AssertCallerIsOwner()\n\tadmin.TransferOwnership(newAdmin)\n}\n" + }, + { + "name": "home_filetest.gno", + "body": "package main\n\nimport \"gno.land/r/gnoland/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// \u003cmain class='gno-tmpl-section'\u003e\n//\n// # Welcome to gno.land\n//\n// ### We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts.\n//\n//\n// With transparent and timeless code, gno.land is the next generation of smart contract platforms, serving as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse.\n//\n//\n// Intuitive and easy to use, gno.land lowers the barrier to web3 and makes censorship-resistant platforms accessible to everyone. If you want to help lay the foundations of a fairer and freer world, join us today.\n//\n// \u003cdiv class=\"jumbotron\"\u003e\n//\n// \u003cdiv class=\"columns-3\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Learn about gno.land\n//\n// - [About](/about)\n// - [GitHub](https://github.com/gnolang)\n// - [Blog](/blog)\n// - [Events](/events)\n// - Tokenomics (soon)\n// - [Partners, Fund, Grants](/partners)\n// - [Explore the Ecosystem](/ecosystem)\n// - [Careers](https://jobs.ashbyhq.com/allinbits)\n//\n// \u003c/div\u003e\u003c!-- end column--\u003e\n//\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Build with Gno\n//\n// - [Write Gno in the browser](https://play.gno.land)\n// - [Read about the Gno Language](/gnolang)\n// - [Visit the official documentation](https://docs.gno.land)\n// - [Gno by Example](https://gno-by-example.com/)\n// - [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev)\n// - [Get testnet GNOTs](https://faucet.gno.land)\n//\n// \u003c/div\u003e\u003c!-- end column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Explore the universe\n//\n// - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n// - [Gnoscan](https://gnoscan.io)\n// - [Portal Loop](https://docs.gno.land/concepts/portal-loop)\n// - [Testnet 4](https://test4.gno.land/)\n// - Testnet Faucet Hub (soon)\n//\n// \u003c/div\u003e\u003c!-- end column--\u003e\n// \u003c/div\u003e\u003c!-- end columns-3--\u003e\n// \u003c/div\u003e\u003c!-- /jumbotron --\u003e\n//\n// \u003cdiv class=\"columns-3\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### [Latest Blogposts](/r/gnoland/blog)\n//\n// No posts.\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### [Latest Events](/r/gnoland/events)\n//\n// No events.\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Latest Contributions\n//\n// [View latest contributions](https://github.com/gnolang/gno/pulls)\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003c/div\u003e\u003c!-- /columns-3 --\u003e\n//\n//\n// ---\n//\n// ### [Gno Playground](https://play.gno.land)\n//\n//\n// Gno Playground is a web application designed for building, running, testing, and interacting\n// with your Gno code, enhancing your understanding of the Gno language. With Gno Playground, you can share your code,\n// execute tests, deploy your realms and packages to gno.land, and explore a multitude of other features.\n//\n//\n// Experience the convenience of code sharing and rapid experimentation with [Gno Playground](https://play.gno.land).\n//\n//\n// ---\n//\n// ### Explore New Packages and Realms\n//\n// \u003cdiv class=\"columns-3\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// #### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)\n//\n// - [r/gnoland/blog](r/gnoland/blog)\n// - [r/gnoland/dao](r/gnoland/dao)\n// - [r/gnoland/faucet](r/gnoland/faucet)\n// - [r/gnoland/home](r/gnoland/home)\n// - [r/gnoland/pages](r/gnoland/pages)\n//\n// #### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)\n//\n// - [r/sys/names](r/sys/names)\n// - [r/sys/rewards](r/sys/rewards)\n// - [/r/sys/validators/v2](/r/sys/validators/v2)\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// #### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)\n//\n// - [r/demo/boards](r/demo/boards)\n// - [r/demo/users](r/demo/users)\n// - [r/demo/banktest](r/demo/banktest)\n// - [r/demo/foo20](r/demo/foo20)\n// - [r/demo/foo721](r/demo/foo721)\n// - [r/demo/microblog](r/demo/microblog)\n// - [r/demo/nft](r/demo/nft)\n// - [r/demo/types](r/demo/types)\n// - [r/demo/art/gnoface](r/demo/art/gnoface)\n// - [r/demo/art/millipede](r/demo/art/millipede)\n// - [r/demo/groups](r/demo/groups)\n// - ...\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// #### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)\n//\n// - [p/demo/avl](p/demo/avl)\n// - [p/demo/blog](p/demo/blog)\n// - [p/demo/ui](p/demo/ui)\n// - [p/demo/ufmt](p/demo/ufmt)\n// - [p/demo/merkle](p/demo/merkle)\n// - [p/demo/bf](p/demo/bf)\n// - [p/demo/flow](p/demo/flow)\n// - [p/demo/gnode](p/demo/gnode)\n// - [p/demo/grc/grc20](p/demo/grc/grc20)\n// - [p/demo/grc/grc721](p/demo/grc/grc721)\n// - ...\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003c/div\u003e\u003c!-- /columns-3 --\u003e\n//\n//\n// ---\n//\n// ### Contributions (WorxDAO \u0026 GoR)\n//\n// coming soon\n//\n// ---\n//\n//\n// \u003cdiv class=\"columns-2\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Socials\n//\n// - Check out our [community projects](https://github.com/gnolang/awesome-gno)\n// - ![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)\n// - ![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)\n// - ![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)\n// - ![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Quote of the ~Day~ Block#123\n//\n// \u003e Now, you Gno.\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003c/div\u003e\u003c!-- /columns-2 --\u003e\n//\n//\n// ---\n//\n// **This is a testnet.**\n// Package names are not guaranteed to be available for production.\n//\n// \u003c/main\u003e\n" + }, + { + "name": "overide_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/home\"\n)\n\nfunc main() {\n\tstd.TestSetOrigCaller(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\thome.AdminSetOverride(\"Hello World!\")\n\tprintln(home.Render(\"\"))\n\thome.AdminTransferOwnership(testutils.TestAddress(\"newAdmin\"))\n\tdefer func() {\n\t\tr := recover()\n\t\tprintln(\"r: \", r)\n\t}()\n\thome.AdminSetOverride(\"Not admin anymore\")\n}\n\n// Output:\n// Hello World!\n// r: ownable: caller is not owner\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "monit", + "path": "gno.land/r/gnoland/monit", + "files": [ + { + "name": "monit.gno", + "body": "// Package monit links a monitoring system with the chain in both directions.\n//\n// The agent will periodically call Incr() and verify that the value is always\n// higher than the previously known one. The contract will store the last update\n// time and use it to detect whether or not the monitoring agent is functioning\n// correctly.\npackage monit\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/watchdog\"\n)\n\nvar (\n\tcounter int\n\tlastUpdate time.Time\n\tlastCaller std.Address\n\twd = watchdog.Watchdog{Duration: 5 * time.Minute}\n\towner = ownable.New() // TODO: replace with -\u003e ownable.NewWithAddress...\n\twatchdogDuration = 5 * time.Minute\n)\n\n// Incr increments the counter and informs the watchdog that we're alive.\n// This function can be called by anyone.\nfunc Incr() int {\n\tcounter++\n\tlastUpdate = time.Now()\n\tlastCaller = std.PrevRealm().Addr()\n\twd.Alive()\n\treturn counter\n}\n\n// Reset resets the realm state.\n// This function can only be called by the admin.\nfunc Reset() {\n\tif owner.CallerIsOwner() != nil { // TODO: replace with owner.AssertCallerIsOwner\n\t\tpanic(\"unauthorized\")\n\t}\n\tcounter = 0\n\tlastCaller = std.PrevRealm().Addr()\n\tlastUpdate = time.Now()\n\twd = watchdog.Watchdog{Duration: 5 * time.Minute}\n}\n\nfunc Render(_ string) string {\n\tstatus := wd.Status()\n\treturn ufmt.Sprintf(\n\t\t\"counter=%d\\nlast update=%s\\nlast caller=%s\\nstatus=%s\",\n\t\tcounter, lastUpdate, lastCaller, status,\n\t)\n}\n\n// TransferOwnership transfers ownership to a new owner. This is a proxy to\n// ownable.Ownable.TransferOwnership.\nfunc TransferOwnership(newOwner std.Address) { owner.TransferOwnership(newOwner) }\n" + }, + { + "name": "monit_test.gno", + "body": "package monit\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPackage(t *testing.T) {\n\t// initial state, watchdog is KO.\n\t{\n\t\texpected := `counter=0\nlast update=0001-01-01 00:00:00 +0000 UTC\nlast caller=\nstatus=KO`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t// call Incr(), watchdog is OK.\n\tIncr()\n\tIncr()\n\tIncr()\n\t{\n\t\texpected := `counter=3\nlast update=2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\nlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\nstatus=OK`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t/* XXX: improve tests once we've the missing std.TestSkipTime feature\n\t\t// wait 1h, watchdog is KO.\n\t\tuse std.TestSkipTime(time.Hour)\n\t\t{\n\t\t\texpected := `counter=3\n\tlast update=2009-02-13 22:31:30 +0000 UTC m=+1234564290.000000001\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=KO`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\n\t\t// call Incr(), watchdog is OK.\n\t\tIncr()\n\t\t{\n\t\t\texpected := `counter=4\n\tlast update=2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=OK`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\t*/\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "gnopages", + "path": "gno.land/r/gnoland/pages", + "files": [ + { + "name": "admin.gno", + "body": "package gnopages\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // XXX: delete instead?\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\tcaller := std.GetOrigCaller()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.GetOrigCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n" + }, + { + "name": "page_about.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"about\"\n\ttitle := \"gno.land Is A Platform To Write Smart Contracts In Gno\"\n\t// XXX: description := \"On gno.land, developers write smart contracts and other blockchain apps using Gno without learning a language that’s exclusive to a single ecosystem.\"\n\tbody := `\ngno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language. On gno.land, smart contracts can be uploaded on-chain only by publishing their full source code,\nmaking it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code\nlibraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent,\nauditable code that anyone can inspect and reuse.\n\ngno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of\nsmart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive\nto a single ecosystem or limited by design. Go developers can easily port their existing web apps to gno.land or build\nnew ones from scratch, making web3 vastly more accessible.\n\nSecured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes\nfairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that\noften corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and\nalignment.\n\nOne of our inspirations for gno.land is the gospels, which built a system of moral code that lasted thousands of years.\nBy observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for\nfuture generations with censorship-resistant tools that improve their understanding of the world.\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:22Z\", nil, nil)\n}\n" + }, + { + "name": "page_contribute.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"contribute\"\n\ttitle := \"Contributor Ecosystem: Call for Contributions\"\n\tbody := `\n\ngno.land puts at the center of its identity the contributors that help to create and shape the project into what it is; incentivizing those who contribute the most and help advance its vision. Eventually, contributions will be incentivized directly on-chain; in the meantime, this page serves to illustrate our current off-chain initiatives.\n\ngno.land is still in full-steam development. For now, we're looking for the earliest of adopters; curious to explore a new way to build smart contracts and eager to make an impact. Joining gno.land's development now means you can help to shape the base of its development ecosystem, which will pave the way for the next generation of blockchain programming.\n\nAs an open-source project, we welcome all contributions. On this page you can find some pointers on where to get started; as well as some incentives for the most valuable and important contributions.\n\n## Where to get started\n\nIf you are interested in contributing to gno.land, you can jump on in on our [GitHub monorepo](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md) - where most development happens.\n\nA good place where to start are the issues tagged [\"good first issue\"](https://github.com/gnolang/gno/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). They should allow you to make some impact on the Gno repository while you're still exploring the details of how everything works.\n\n## Gno Bounties\n\nAdditionally, you can look out to help on specific issues labeled as bounties. All contributions will then concur to form your profile for Game of Realms.\n\nThe Gno bounty program is a good way to find interesting challenges in Gno, and get rewarded for helping us advance the project. We will maintain open and rewardable bounties in the gnolang/gno repository, and you can search all available bounties by using the [\"bounty\" label](https://github.com/gnolang/gno/labels/bounty).\n\nRecommendations on participating in the gno.land Bounty Program:\n\n- Identify the bounty you want to work on, and join in the discussion on the issue for anything that is unclear; or where you want to more clearly define the work to be done. At this stage, you can also start working on an initial implementation in your local enviornment.\n- Once you have spent time on the code related to the bounty, we recommend submitting a 'draft' PR as soon as possible.\n - The draft PR doesn't indicate that the bounty has been assigned to you, others are free to work on other draft PRs for the bounty.\n - Make sure to reference the bounty issue on the PR description you're writing.\n - After submitting the 'draft' PR, continue working until you are ready to mark the PR as \"ready for review\".\n - The core team will review the bounty PR submission after the work on the bounty has been completed, and determine if it qualifies for the bounty reward.\n- Ask for clarification early if an element on the requirements or implementation design is unclear.\n - Aside from publishing the PR early, keeping regular updates with the core team on the bounty issue is key to being on the right track.\n - As part of the requirements, you must adhere to the [contributing guidelines](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md); additionally, it is expected that any newly added code or functionality is properly documented, tested and covered, at least in 80% of added code.\n - You're welcome to propose additional features and work on an issue should you envision a plausible expansion or change in scope. The core team may assign a bounty to the additional work, or change the bounty with respect to the changed scope.\n\nYou may make your submission at any time; however we invite you to publish your draft PR very early in the development process. This will make your work public, so you can easily get help by the core team and other community members. Additionally, your work can be continued by other people should you get stuck or no longer be willing to work on the bounty. Likewise, you can continue the abandoned or stuck work that someone else worked on.\n\nDon't fear your work being \"stolen\": if a submission is the result of multiple people's efforts, we will look to split the bounty in a way that is fair and recognises each participant in creating the final outcome. Here are some examples of how that can happen:\n\n- If Alice does most of the work and abandons it; then Bob comes around and finishes the job, then Bob's PR will be merged. But the core team will propose a split like 70% for Alice and 30% for Bob (depending, of course, on the relative effort undertaken by both).\n- If Alice makes a PR that does only 50% of the work outlined in the requirements for the original issue, she will get 50%. Someone can still come up and finish the job; and claim the remaining part.\n\t- If you, for instance, cannot complete the entirety of the task or, as a non-developer, can only contribute a part of the specification/implementation, you may still be awarded a bounty for your input in the contribution.\n- If Alice makes a PR that aside from implementing what's required, also undertakes creating useful tools among the way, she may qualify for an \"outstanding contribution\"; and may be awarded up to 25% more of the original bounty's value. Or she may also ask if the team would be willing to offer a different bounty for the implementation of the tools.\n\nParticipants in the gno.land Bounty Program must meet the legal Terms and Conditions referenced [here](https://docs.google.com/document/d/e/2PACX-1vSUF-JwIXGscrNsc5QBD7Pa6i83mXUGogAEIf1wkeb_w42UgL3Lj6jFKMlNTdwEMUnhsLkjRlhe25K4/pub).\n\n### Bounty sizes\n\nEach bounty is associated with a size, to which corresponds the maximum compensation for the work involved on the bounty. A bounty size may under rare occasion be revisited to a bigger or smaller size; hence why it's important to talk about your proposed solution with the core team ahead of time.\n\nIn some cases, the work associated with a bounty may be outstanding. When that happens, the core team can decide to award up to 25% of the bounty's value to the recipient.\n\nThe value of the bounty, aside from the material completion of the task, considers the involved time in managing the created pull request and iterating on feedback.\n\n\nt-shirt size | expected compensation\n-------------|-----------------------\n[XS] | $ 500\n[S] | $ 1000\n[M] | $ 2000\n[L] | $ 4000\n[XL] | $ 8000\n_[XXL]_ \\* | $ 16000\n_[3XL]_ \\* | $ 32000\n\n[XS]: https://github.com/gnolang/gno/labels/bounty%2FXS\n[S]: https://github.com/gnolang/gno/labels/bounty%2FS\n[M]: https://github.com/gnolang/gno/labels/bounty%2FM\n[L]: https://github.com/gnolang/gno/labels/bounty%2FL\n[XL]: https://github.com/gnolang/gno/labels/bounty%2FXL\n[XXL]: https://github.com/gnolang/gno/labels/bounty%2FXXL\n[3XL]: https://github.com/gnolang/gno/labels/bounty%2F3XL\n\n\\*: XXL and 3XL bounties are exceptional. Almost no issues will have these sizes; most will be broken down into smaller bounties.\n\n## gno.land Grants\n\nThe gno.land grants program is to encourage and support the growth of the gno.land contributor community, and build out the usability of the platform and smart contract library. The program provides financial resources to contributors to explore the Gno tech stack, and build dApps, tooling, infrastructure, products, and smart contract libraries in gno.land.\n\nFor more details on gno.land grants, suggested topics, and how to apply, visit our grants [repository](https://github.com/gnolang/grants). \n\n## Join Game of Realms\n\nGame of Realms is the overarching contributor network of gnomes, currently running off-chain, and will eventually transition on-chain. At this stage, a Game of Realms contribution is comprised of high-impact contributions identified as ['notable contributions'](https://github.com/gnolang/game-of-realms/tree/main/contributors).\n\nThese contributions are not linked to immediate financial rewards, but are notable in nature, in the sense they are a challenge, make a significant addition to the project, and require persistence, with minimal feedback loops from the core team.\n\nThe selection of a notable contribution or the sum of contributions that equal 'notable' is based on the impact it has on the development of the project. For now, it is focused on code contributions, and will evolve over time. The Gno development teams will initially qualify and evaluate notable contributions, and vote off-chain on adding them to the 'notable contributions' folder on GitHub.\n\nYou can always contribute to the project, and all contributions will be noticed. Contributing now is a way to build your personal contributor profile in gno.land early on in the ecosystem, and signal your commitment to the project, the community, and its future.\n\nThere are a variety of ways to make your contributions count:\n\n- Core code contributions\n- Realm and pure package development\n- Validator tooling\n- Developer tooling\n- Tutorials and documentation\n\nTo start, we recommend you create a PR in the Game of Realms [repository](https://github.com/gnolang/game-of-realms) to create your profile page for all your contributions.`\n\n\t_ = b.NewPost(\"\", path, title, body, \"2024-09-05T00:00:00Z\", nil, nil)\n}\n" + }, + { + "name": "page_ecosystem.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"ecosystem\"\n\t\ttitle = \"Discover gno.land Ecosystem Projects \u0026 Initiatives\"\n\t\t// XXX: description = \"Dive further into the gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building.\"\n\t\tbody = `\n### [Gno Playground](https://play.gno.land)\n\nGno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your\nunderstanding of the Gno language. You can share your code, run unit tests, deploy your realms and packages, and execute\nfunctions in your code using the repo.\n\nVisit the playground at [play.gno.land](https://play.gno.land)!\n\n### [Gno Studio Connect](https://gno.studio/connect)\n\nGno Studio Connect provides seamless access to realms, making it simple to explore, interact, and engage\nwith gno.land’s smart contracts through function calls. Connect focuses on function calls, enabling users to interact\nwith any realm’s exposed function(s) on gno.land.\n\nSee your realm interactions in [Gno Studio Connect](https://gno.studio/connect)\n\n### [Gnoscan](https://gnoscan.io)\n\nDeveloped by the Onbloc team, Gnoscan is gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find\ninformation that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts.\nGnoscan makes our on-chain data easy to read and intuitive to discover.\n\nExplore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)!\n\n### Adena\n\nAdena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to\ninteract easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a\nhigh-quality interface, support for NFTs and custom tokens, and seamless integration. Install Adena via the [official website](https://www.adena.app/)\n\n### Gnoswap\n\nGnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an\nautomated market maker (AMM) protocol written in Gno that allows for permissionless token exchanges on the platform.\n\n### Flippando\n\nFlippando is a simple on-chain memory game, ported from Solidity to Gno, which starts with an empty matrix to flip tiles\non to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player\nmust memorize their colors until the entire matrix is uncovered. The end result can be minted as an NFT, which can later\nbe assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles. Play the game at [Flippando](https://gno.flippando.xyz/flip)\n\n### Gno Native Kit\n\n[Gno Native Kit](https://github.com/gnolang/gnonative) is a framework that allows developers to build and port gno.land (d)apps written in the (d)app's native language.\n\n\n`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:23Z\", nil, nil)\n}\n" + }, + { + "name": "page_gnolang.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"gnolang\"\n\t\ttitle = \"About the Gno, the Language for gno.land\"\n\t\t// TODO fix broken images\n\t\tbody = `\n\n[Gno](https://github.com/gnolang/gno) is an interpretation of the widely-used Go (Golang) programming language for blockchain created by Cosmos co-founder Jae Kwon in 2022 to mark a new era in smart contracting. Gno is ~99% identical to Go, so Go programmers can start coding in Gno right away, with a minimal learning curve. For example, Gno comes with blockchain-specific standard libraries, but any code that doesn’t use blockchain-specific logic can run in Go with minimal processing. Libraries that don’t make sense in the blockchain context, such as network or operating-system access, are not available in Gno. Otherwise, Gno loads and uses many standard libraries that power Go, so most of the parsing of the source code is the same.\n\nUnder the hood, the Gno code is parsed into an abstract syntax tree (AST) and the AST itself is used in the interpreter, rather than bytecode as in many virtual machines such as Java, Python, or Wasm. This makes even the GnoVM accessible to any Go programmer. The novel design of the intuitive GnoVM interpreter allows Gno to freeze and resume the program by persisting and loading the entire memory state. Gno is deterministic, auto-persisted, and auto-Merkle-ized, allowing (smart contract) programs to be succinct, as the programmer doesn’t have to serialize and deserialize objects to persist them into a database (unlike programming applications with the Cosmos SDK).\n\n## How Gno Differs from Go\n\n![Gno and Go differences](static/img/gno-language/go-and-gno.jpg)\n\nThe composable nature of Go/Gno allows for type-checked interactions between contracts, making gno.land safer and more powerful, as well as operationally cheaper and faster. Smart contracts on gno.land are light, simple, more focused, and easily interoperable—a network of interconnected contracts rather than siloed monoliths that limit interactions with other contracts.\n\n![Example of Gno code](static/img/gno-language/code-example.jpg)\n\n## Gno Inherits Go’s Built-in Security Features\n\nGo supports secure programming through exported/non-exported fields, enabling a “least-authority” design. It is easy to create objects and APIs that expose only what should be accessible to callers while hiding what should not be simply by the capitalization of letters, thus allowing a succinct representation of secure logic that can be called by multiple users.\n\nAnother major advantage of Go is that the language comes with an ecosystem of great tooling, like the compiler and third-party tools that statically analyze code. Gno inherits these advantages from Go directly to create a smart contract programming language that provides embedding, composability, type-check safety, and garbage collection, helping developers to write secure code relying on the compiler, parser, and interpreter to give warning alerts for common mistakes.\n\n## Gno vs Solidity\n\nThe most widely-adopted smart contract language today is Ethereum’s EVM-compatible Solidity. With bytecode built from the ground up and Turing complete, Solidity opened up a world of possibilities for decentralized applications (dApps) and there are currently more than 10 million contracts deployed on Ethereum. However, Solidity provides limited tooling and its EVM has a stack limit and computational inefficiencies.\n\nSolidity is designed for one purpose only (writing smart contracts) and is bound by the limitations of the EVM. In addition, developers have to learn several languages if they want to understand the whole stack or work across different ecosystems. Gno aspires to exceed Solidity on multiple fronts (and other smart contract languages like CosmWasm or Substrate) as every part of the stack is written in Gno. It’s easy for developers to understand the entire system just by studying a relatively small code base.\n\n## Gno Is Essential for the Wider Adoption of Web3\n\nGno makes imports as easy as they are in web2 with runtime-based imports for seamless dependency flow comprehension, and support for complex structs, beyond primitive types. Gno is ultimately cost-effective as dependencies are loaded once, enabling remote function calls as local, and providing automatic and independent per-realm state persistence.\n\nUsing Gno, developers can rapidly accelerate application development and adopt a modular structure by reusing and reassembling existing modules without building from scratch. They can embed one structure inside another in an intuitive way while preserving localism, and the language specification is simple, successfully balancing practicality and minimalism.\n\nThe Go language is so well designed that the Gno smart contract system will become the new gold standard for smart contract development and other blockchain applications. As a programming language that is universally adopted, secure, composable, and complete, Gno is essential for the broader adoption of web3 and its sustainable growth.`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:25Z\", nil, nil)\n}\n" + }, + { + "name": "page_license.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"license\"\n\t\ttitle = \"Gno Network General Public License\"\n\t\tbody = `Copyright (C) 2024 NewTendermint, LLC\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of the GNO Network General Public License as published by\nNewTendermint, LLC, either version 4 of the License, or (at your option) any\nlater version published by NewTendermint, LLC.\n\nThis program is distributed in the hope that it will be useful, but is provided\nas-is and WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNO Network\nGeneral Public License for more details.\n\nYou should have received a copy of the GNO Network General Public License along\nwith this program. If not, see \u003chttps://gno.land/license\u003e.\n\nAttached below are the terms of the GNO Network General Public License, Version\n4 (a fork of the GNU Affero General Public License 3).\n\n## Additional Terms\n\n### Strong Attribution\n\nIf any of your user interfaces, such as websites and mobile applications, serve\nas the primary point of entry to a platform or blockchain that 1) offers users\nthe ability to upload their own smart contracts to the platform or blockchain,\nand 2) leverages any Covered Work (including the GNO virtual machine) to run\nthose smart contracts on the platform or blockchain (\"Applicable Work\"), then\nthe Applicable Work must prominently link to (1) gno.land or (2) any other URL\ndesignated by NewTendermint, LLC that has not been rejected by the governance of\nthe first chain known as gno.land, provided that the identity of the first chain\nis not ambiguous. In the event the identity of the first chain is ambiguous,\nthen NewTendermint, LLC's designation shall control. Such link must appear\nconspicuously in the header or footer of the Applicable Work, such that all\nusers may learn of gno.land or the URL designated by NewTendermint, LLC.\n\nThis additional attribution requirement shall remain in effect for (1) 7\nyears from the date of publication of the Applicable Work, or (2) 7 years from\nthe date of publication of the Covered Work (including republication of new\nversions), whichever is later, but no later than 12 years after the application\nof this strong attribution requirement to the publication of the Applicable\nWork. For purposes of this Strong Attribution requirement, Covered Work shall\nmean any work that is licensed under the GNO Network General Public License,\nVersion 4 or later, by NewTendermint, LLC.\n\n\n# GNO NETWORK GENERAL PUBLIC LICENSE\n\nVersion 4, 7 May 2024\n\nModified from the GNU AFFERO GENERAL PUBLIC LICENSE.\nGNU is not affiliated with GNO or NewTendermint, LLC.\nCopyright (C) 2022 NewTendermint, LLC.\n\n## Preamble\n\nThe GNO Network General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works. By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\nWhen we speak of free software, we are referring to freedom, not\nprice. Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nDevelopers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\nA secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate. Many developers of free software are heartened and\nencouraged by the resulting cooperation. However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\nThe GNO Network General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community. It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server. Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n## TERMS AND CONDITIONS\n\n### 0. Definitions.\n\n\"This License\" refers to version 4 of the GNO Network General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense. Each licensee is addressed as \"you\". \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy. The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy. Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies. Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License. If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n### 1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it. \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form. A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities. However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work. For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n### 2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met. This License explicitly affirms your unlimited\npermission to run the unmodified Program. The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work. This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force. You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright. Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below. Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n### 4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n### 5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n- a) The work must carry prominent notices stating that you modified\n it, and giving a relevant date.\n- b) The work must carry prominent notices stating that it is\n released under this License and any conditions added under section\n 7. This requirement modifies the requirement in section 4 to\n \"keep intact all notices\".\n- c) You must license the entire work, as a whole, under this\n License to anyone who comes into possession of a copy. This\n License will therefore apply, along with any applicable section 7\n additional terms, to the whole of the work, and all its parts,\n regardless of how they are packaged. This License gives no\n permission to license the work in any other way, but it does not\n invalidate such permission if you have separately received it.\n- d) If the work has interactive user interfaces, each must display\n Appropriate Legal Notices; however, if the Program has interactive\n interfaces that do not display Appropriate Legal Notices, your\n work need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit. Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n### 6. Conveying Non-Source Forms.\n\n You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n- a) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by the\n Corresponding Source fixed on a durable physical medium\n customarily used for software interchange.\n- b) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by a\n written offer, valid for at least three years and valid for as\n long as you offer spare parts or customer support for that product\n model, to give anyone who possesses the object code either (1) a\n copy of the Corresponding Source for all the software in the\n product that is covered by this License, on a durable physical\n medium customarily used for software interchange, for a price no\n more than your reasonable cost of physically performing this\n conveying of source, or (2) access to copy the\n Corresponding Source from a network server at no charge.\n- c) Convey individual copies of the object code with a copy of the\n written offer to provide the Corresponding Source. This\n alternative is allowed only occasionally and noncommercially, and\n only if you received the object code with such an offer, in accord\n with subsection 6b.\n- d) Convey the object code by offering access from a designated\n place (gratis or for a charge), and offer equivalent access to the\n Corresponding Source in the same way through the same place at no\n further charge. You need not require recipients to copy the\n Corresponding Source along with the object code. If the place to\n copy the object code is a network server, the Corresponding Source\n may be on a different server (operated by you or a third party)\n that supports equivalent copying facilities, provided you maintain\n clear directions next to the object code saying where to find the\n Corresponding Source. Regardless of what server hosts the\n Corresponding Source, you remain obligated to ensure that it is\n available for as long as needed to satisfy these requirements.\n- e) Convey the object code using peer-to-peer transmission, provided\n you inform other peers where the object code and Corresponding\n Source of the work are being offered to the general public at no\n charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling. In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage. For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product. A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source. The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information. But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed. Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n### 7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law. If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit. (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.) You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n- a) Disclaiming warranty or limiting liability differently from the\n terms of sections 15 and 16 of this License; or\n- b) Requiring preservation of specified reasonable legal notices or\n author attributions in that material or in the Appropriate Legal\n Notices displayed by works containing it; or\n- c) Prohibiting misrepresentation of the origin of that material, or\n requiring that modified versions of such material be marked in\n reasonable ways as different from the original version; or\n- d) Limiting the use for publicity purposes of names of licensors or\n authors of the material; or\n- e) Declining to grant rights under trademark law for use of some\n trade names, trademarks, or service marks; or\n- f) Requiring indemnification of licensors and authors of that\n material by anyone who conveys the material (or modified versions of\n it) with contractual assumptions of liability to the recipient, for\n any liability that these contractual assumptions directly impose on\n those licensors and authors; or\n- g) Requiring strong attribution such as notices on any user interfaces\n that run or convey any covered work, such as a prominent link to a URL\n on the header of a website, such that all users of the covered work may\n become aware of the notice, for a period no longer than 20 years.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10. If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term. If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n### 8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License. Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License. If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n### 9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program. Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance. However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work. These actions infringe copyright if you do\nnot accept this License. Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n### 10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License. You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations. If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License. For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n### 11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based. The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version. For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement). To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients. \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License. You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n### 12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot convey a\ncovered work so as to simultaneously satisfy your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all. For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n### 13. Remote Network Interaction; Use with the GNU General Public License.\n\nNotwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software. This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work. The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n### 14. Revised Versions of this License.\n\nNewTendermint LLC may publish revised and/or new versions of\nthe GNO Network General Public License from time to time. Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nProgram specifies that a certain numbered version of the GNO Network General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Gno Software\nFoundation. If the Program does not specify a version number of the\nGNO Network General Public License, you may choose any version ever published\nby NewTendermint LLC.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNO Network General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions. However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n### 15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n### 16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n### 17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\n## How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n \u003cone line to give the program's name and a brief idea of what it does.\u003e\n Copyright (C) \u003cyear\u003e \u003cname of author\u003e\n\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNO Network General Public License as published by\n NewTendermint LLC, either version 4 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNO Network General Public License for more details.\n\n You should have received a copy of the GNO Network General Public License\n along with this program. If not, see \u003chttps://gno.land/license\u003e.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source. For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code. There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2024-04-22T00:00:00Z\", nil, nil)\n}\n" + }, + { + "name": "page_partners.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"partners\"\n\ttitle := \"Partnerships\"\n\tbody := `### Fund and Grants Program\n\nAre you a builder, tinkerer, or researcher? If you’re looking to create awesome dApps, tooling, infrastructure, \nor smart contract libraries on gno.land, you can apply for a grant. The gno.land Ecosystem Fund and Grants program \nprovides financial contributions for individuals and teams to innovate on the platform.\n\nRead more about our Funds and Grants program [here](https://github.com/gnolang/ecosystem-fund-grants).\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:27Z\", nil, nil)\n}\n" + }, + { + "name": "page_start.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"start\"\n\ttitle := \"Getting Started with Gno\"\n\t// XXX: description := \"\"\n\n\t// TODO: codegen to use README files here\n\n\t/* TODO: port previous message: This is a demo of Gno smart contract programming. This document was\n\tconstructed by Gno onto a smart contract hosted on the data Realm\n\tname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n\t([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\t*/\n\tbody := `## Getting Started with Gno\n\n- [Install Gno Key](/r/demo/boards:testboard/5)\n- TODO: add more links\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:28Z\", nil, nil)\n}\n" + }, + { + "name": "page_testnets.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"testnets\"\n\ttitle := \"gno.land Testnet List\"\n\tbody := `\n- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet\n- [staging.gno.land](https://staging.gno.land) - wiped every commit to monorepo master\n- _[test4.gno.land](https://test4.gno.land) (latest)_\n\nFor a list of RPC endpoints, see the [reference documentation](https://docs.gno.land/reference/rpc-endpoints).\n\n## Local development\n\nSee the \"Getting started\" section in the [official documentation](https://docs.gno.land/getting-started/local-setup).\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:29Z\", nil, nil)\n}\n" + }, + { + "name": "page_tokenomics.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"tokenomics\"\n\t\ttitle = \"gno.land Tokenomics\"\n\t\t// XXX: description = \"\"\"\n\t\tbody = `Lorem Ipsum`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:30Z\", nil, nil)\n}\n" + }, + { + "name": "pages.gno", + "body": "package gnopages\n\nimport (\n\t\"gno.land/p/demo/blog\"\n)\n\n// TODO: switch from p/blog to p/pages\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Gnoland's Pages\",\n\tPrefix: \"/r/gnoland/pages:\",\n\tNoBreadcrumb: true,\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n" + }, + { + "name": "pages_test.gno", + "body": "package gnopages\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestHome(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"\")\n\texpectedSubtrings := []string{\n\t\t\"/r/gnoland/pages:p/tokenomics\",\n\t\t\"/r/gnoland/pages:p/start\",\n\t\t\"/r/gnoland/pages:p/contribute\",\n\t\t\"/r/gnoland/pages:p/about\",\n\t\t\"/r/gnoland/pages:p/gnolang\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintln(got)\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n\nfunc TestAbout(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"p/about\")\n\texpectedSubtrings := []string{\n\t\t\"gno.land Is A Platform To Write Smart Contracts In Gno\",\n\t\t\"gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\\nprogramming language.\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintln(got)\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n" + }, + { + "name": "util.gno", + "body": "package gnopages\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "validators", + "path": "gno.land/r/sys/validators/v2", + "files": [ + { + "name": "doc.gno", + "body": "// Package validators implements the on-chain validator set management through Proof of Contribution.\n// The Realm exposes only a public executor for govdao proposals, that can suggest validator set changes.\npackage validators\n" + }, + { + "name": "gnosdk.gno", + "body": "package validators\n\nimport (\n\t\"gno.land/p/sys/validators\"\n)\n\n// GetChanges returns the validator changes stored on the realm, since the given block number.\n// This function is intended to be called by gno.land through the GnoSDK\nfunc GetChanges(from int64) []validators.Validator {\n\tvalsetChanges := make([]validators.Validator, 0)\n\n\t// Gather the changes from the specified block\n\tchanges.Iterate(getBlockID(from), \"\", func(_ string, value interface{}) bool {\n\t\tchs := value.([]change)\n\n\t\tfor _, ch := range chs {\n\t\t\tvalsetChanges = append(valsetChanges, ch.validator)\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn valsetChanges\n}\n" + }, + { + "name": "init.gno", + "body": "package validators\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/nt/poa\"\n\t\"gno.land/p/sys/validators\"\n)\n\nfunc init() {\n\t// Prepare the initial validator set\n\tset := []validators.Validator{\n\t\t// core-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1qn3jwvdpva622j3fyudqy65zstnqx2wnqhrs3s\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpndqtjh5dcsnd0gcez3frs3w6rsttmlekj4cyywegyh0n8uprwvj5n8688\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g1gtu9czw9qavrtdnf936usvwjwyjz0x0jk243au\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq4y0ppxhxazdwxhnsxxzdmh9rxht888n4fl0mskwcpq7y2404dm2h0lamk\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-3\n\t\t{\n\t\t\tAddress: std.Address(\"g19emxxnzzfa0pkffvthrss5drgccjnwj8mdme4f\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq288fe7pd2yy3h2h8qjh0elu3pxuamf3wpa9qt9s6jja0r3k64ue4mh636\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-4\n\t\t{\n\t\t\tAddress: std.Address(\"g1hyxtsgjr5zt06jcx4z0xenn3u442ad2xgzu7lp\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpy4mst534500z7k6xk5u7c9ex8zs44rjjhmxaxtw9zzjv82qkfhkhx2rfs\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-5\n\t\t{\n\t\t\tAddress: std.Address(\"g1l072ma0vfhx7s4vpevfvuxd6wzkv5ztt7gh99w\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqtvz3g6nvu3d6wdz97w7jdw2sjc65us5u8gj8pm4mhasw7zxakjhjn9qkm\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-6\n\t\t{\n\t\t\tAddress: std.Address(\"g1uwqd3284kuzm56auwyc9d87jf3953tp9pnt506\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp8xm09ura7mwyntee78cl64hgzq0x75f05tv7fkxpqvc797j37hsr3vgjg\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// berty-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1ut590acnamvhkrh4qz6dz9zt9e3hyu499u0gvl\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq2gncppkfzmx7s22mn60mf0uxzzpl23yx97hwmwm8yc6lupepqqnlexfch\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// onbloc-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1arkzjfrte9l97v9q2qye07v0lw07039gaa3hfy\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpkvdy7n9744qay76fzekpu9l6g3mp4hzhqjmp6k2as72ghlzc546ju3a09\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// onbloc-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g1x0m33nyne064xdx7tvlfcjwd4xkajjar6h523z\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp6s70v4wurhg699w6f9emkwxdlm2eyf2uv64annj47npq85tjeucedmky9\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1mxguhd5zacar64txhfm0v7hhtph5wur5hx86vs\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqz6fwulsygvu9xypka3zqxhkxllm467e3adphmj6y44vn3yy8qq34vxnse\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g1t9ctfa468hn6czff8kazw08crazehcxaqa2uaa\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpsq650w975vqsf6ajj5x4wdzfnrh64kmw7sljqz7wts6k0p6l36d0huls3\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-3\n\t\t{\n\t\t\tAddress: std.Address(\"g1sll4rtvrepdyzcvg5ml0kjtl7fnwgcsxgg9s5q\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zplr4zg2smgha4n9huwcywm6pnkuny2x2j44kk4srxcf0rrmpql3035k8s2\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-4\n\t\t{\n\t\t\tAddress: std.Address(\"g1aa5pp94eaextkump38766hpdra74xtfh805msv\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqe85el3ardhel5vruywsdjw0vj2zjyhqhsyhcnuh0dy8xhuj8mxjg5h7uw\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// tori-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1r2lwzu0y0na4686a0lz4f2zqxlffqkfm7lqqqp\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq2quztlp2pffjsun3jeqyesru8rx9yc6tfj9na3hnw9qgn4zlrpul5mhd0\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// aib-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1ecdu2gwz9d46srrhpu7k60pnrquvle5z2a5nn0\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqnrer4hlsq7q22egx9ur357hg8ftsntyh4z2g7x69u2s4ay25vdw4tredd\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// aib-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g169wsuqlrscnvxtsu6wrc0zuwn39tmctw7q9f0q\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpv6k4a2r6x6gt7eqp70l5vxluk9zkdmlqvkxztnc8zp2llq73e6ukxvsf6\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// aib-val-3\n\t\t{\n\t\t\tAddress: std.Address(\"g1hfwh3ufph3zczs5wu4qvpgtv79fzh30rgzdux8\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqxx3qdzl9f6lee42vhtka5luujhxg22tesyww52af68f75zzp0snyhl8mw\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t}\n\n\t// The default valset protocol is PoA\n\tvp = poa.NewPoA(poa.WithInitialSet(set))\n\n\t// No changes to apply initially\n\tchanges = avl.NewTree()\n}\n" + }, + { + "name": "poc.gno", + "body": "package validators\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nconst errNoChangesProposed = \"no set changes proposed\"\n\n// NewPropExecutor creates a new executor that wraps a changes closure\n// proposal. This wrapper is required to ensure the GovDAO Realm actually\n// executed the callback.\n//\n// Concept adapted from:\n// https://github.com/gnolang/gno/pull/1945\nfunc NewPropExecutor(changesFn func() []validators.Validator) dao.Executor {\n\tif changesFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\tfor _, change := range changesFn() {\n\t\t\tif change.VotingPower == 0 {\n\t\t\t\t// This change request is to remove the validator\n\t\t\t\tremoveValidator(change.Address)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// This change request is to add the validator\n\t\t\taddValidator(change)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\n\n// IsValidator returns a flag indicating if the given bech32 address\n// is part of the validator set\nfunc IsValidator(addr std.Address) bool {\n\treturn vp.IsValidator(addr)\n}\n\n// GetValidators returns the typed validator set\nfunc GetValidators() []validators.Validator {\n\treturn vp.GetValidators()\n}\n" + }, + { + "name": "validators.gno", + "body": "package validators\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar (\n\tvp validators.ValsetProtocol // p is the underlying validator set protocol\n\tchanges *avl.Tree // changes holds any valset changes; seqid(block number) -\u003e []change\n)\n\n// change represents a single valset change, tied to a specific block number\ntype change struct {\n\tblockNum int64 // the block number associated with the valset change\n\tvalidator validators.Validator // the validator update\n}\n\n// addValidator adds a new validator to the validator set.\n// If the validator is already present, the method errors out\nfunc addValidator(validator validators.Validator) {\n\tval, err := vp.AddValidator(validator.Address, validator.PubKey, validator.VotingPower)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator added, note the change\n\tch := change{\n\t\tblockNum: std.GetHeight(),\n\t\tvalidator: val,\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tstd.Emit(validators.ValidatorAddedEvent)\n}\n\n// removeValidator removes the given validator from the set.\n// If the validator is not present in the set, the method errors out\nfunc removeValidator(address std.Address) {\n\tval, err := vp.RemoveValidator(address)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator removed, note the change\n\tch := change{\n\t\tblockNum: std.GetHeight(),\n\t\tvalidator: validators.Validator{\n\t\t\tAddress: val.Address,\n\t\t\tPubKey: val.PubKey,\n\t\t\tVotingPower: 0, // nullified the voting power indicates removal\n\t\t},\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tstd.Emit(validators.ValidatorRemovedEvent)\n}\n\n// saveChange saves the valset change\nfunc saveChange(ch change) {\n\tid := getBlockID(ch.blockNum)\n\n\tsetRaw, exists := changes.Get(id)\n\tif !exists {\n\t\tchanges.Set(id, []change{ch})\n\n\t\treturn\n\t}\n\n\t// Save the change\n\tset := setRaw.([]change)\n\tset = append(set, ch)\n\n\tchanges.Set(id, set)\n}\n\n// getBlockID converts the block number to a sequential ID\nfunc getBlockID(blockNum int64) string {\n\treturn seqid.ID(uint64(blockNum)).String()\n}\n\nfunc Render(_ string) string {\n\tvar (\n\t\tsize = changes.Size()\n\t\tmaxDisplay = 10\n\t)\n\n\tif size == 0 {\n\t\treturn \"No valset changes to apply.\"\n\t}\n\n\toutput := \"Valset changes:\\n\"\n\tchanges.ReverseIterateByOffset(size-maxDisplay, maxDisplay, func(_ string, value interface{}) bool {\n\t\tchs := value.([]change)\n\n\t\tfor _, ch := range chs {\n\t\t\toutput += ufmt.Sprintf(\n\t\t\t\t\"- #%d: %s (%d)\\n\",\n\t\t\t\tch.blockNum,\n\t\t\t\tch.validator.Address.String(),\n\t\t\t\tch.validator.VotingPower,\n\t\t\t)\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn output\n}\n" + }, + { + "name": "validators_test.gno", + "body": "package validators\n\nimport (\n\t\"testing\"\n\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/sys/validators\"\n)\n\n// generateTestValidators generates a dummy validator set\nfunc generateTestValidators(count int) []validators.Validator {\n\tvals := make([]validators.Validator, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tval := validators.Validator{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"%d\", i)),\n\t\t\tPubKey: \"public-key\",\n\t\t\tVotingPower: 10,\n\t\t}\n\n\t\tvals = append(vals, val)\n\t}\n\n\treturn vals\n}\n\nfunc TestValidators_AddRemove(t *testing.T) {\n\t// Clear any changes\n\tchanges = avl.NewTree()\n\n\tvar (\n\t\tvals = generateTestValidators(100)\n\t\tinitialHeight = int64(123)\n\t)\n\n\t// Add in the validators\n\tfor _, val := range vals {\n\t\taddValidator(val)\n\n\t\t// Make sure the validator is added\n\t\tuassert.True(t, vp.IsValidator(val.Address))\n\n\t\tstd.TestSkipHeights(1)\n\t}\n\n\tfor i := initialHeight; i \u003c initialHeight+int64(len(vals)); i++ {\n\t\t// Make sure the changes are saved\n\t\tchs := GetChanges(i)\n\n\t\t// We use the funky index calculation to make sure\n\t\t// changes are properly handled for each block span\n\t\tuassert.Equal(t, initialHeight+int64(len(vals))-i, int64(len(chs)))\n\n\t\tfor index, val := range vals[i-initialHeight:] {\n\t\t\t// Make sure the changes are equal to the additions\n\t\t\tch := chs[index]\n\n\t\t\tuassert.Equal(t, val.Address, ch.Address)\n\t\t\tuassert.Equal(t, val.PubKey, ch.PubKey)\n\t\t\tuassert.Equal(t, val.VotingPower, ch.VotingPower)\n\t\t}\n\t}\n\n\t// Save the beginning height for the removal\n\tinitialRemoveHeight := std.GetHeight()\n\n\t// Clear any changes\n\tchanges = avl.NewTree()\n\n\t// Remove the validators\n\tfor _, val := range vals {\n\t\tremoveValidator(val.Address)\n\n\t\t// Make sure the validator is removed\n\t\tuassert.False(t, vp.IsValidator(val.Address))\n\n\t\tstd.TestSkipHeights(1)\n\t}\n\n\tfor i := initialRemoveHeight; i \u003c initialRemoveHeight+int64(len(vals)); i++ {\n\t\t// Make sure the changes are saved\n\t\tchs := GetChanges(i)\n\n\t\t// We use the funky index calculation to make sure\n\t\t// changes are properly handled for each block span\n\t\tuassert.Equal(t, initialRemoveHeight+int64(len(vals))-i, int64(len(chs)))\n\n\t\tfor index, val := range vals[i-initialRemoveHeight:] {\n\t\t\t// Make sure the changes are equal to the additions\n\t\t\tch := chs[index]\n\n\t\t\tuassert.Equal(t, val.Address, ch.Address)\n\t\t\tuassert.Equal(t, val.PubKey, ch.PubKey)\n\t\t\tuassert.Equal(t, uint64(0), ch.VotingPower)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "valopers", + "path": "gno.land/r/gnoland/valopers/v2", + "files": [ + { + "name": "init.gno", + "body": "package valopers\n\nimport \"gno.land/p/demo/avl\"\n\nfunc init() {\n\tvalopers = avl.NewTree()\n}\n" + }, + { + "name": "valopers.gno", + "body": "// Package valopers is designed around the permissionless lifecycle of valoper profiles.\n// It also includes parts designed for govdao to propose valset changes based on registered valopers.\npackage valopers\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ufmt\"\n\tpVals \"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao/bridge\"\n\tvalidators \"gno.land/r/sys/validators/v2\"\n)\n\nconst (\n\terrValoperExists = \"valoper already exists\"\n\terrValoperMissing = \"valoper does not exist\"\n\terrInvalidAddressUpdate = \"valoper updated address exists\"\n\terrValoperNotCaller = \"valoper is not the caller\"\n)\n\n// valopers keeps track of all the active validator operators\nvar valopers *avl.Tree // Address -\u003e Valoper\n\n// Valoper represents a validator operator profile\ntype Valoper struct {\n\tName string // the display name of the valoper\n\tMoniker string // the moniker of the valoper\n\tDescription string // the description of the valoper\n\n\tAddress std.Address // The bech32 gno address of the validator\n\tPubKey string // the bech32 public key of the validator\n\tP2PAddresses []string // the publicly reachable P2P addresses of the validator\n\tActive bool // flag indicating if the valoper is active\n}\n\n// Register registers a new valoper\nfunc Register(v Valoper) {\n\t// Check if the valoper is already registered\n\tif isValoper(v.Address) {\n\t\tpanic(errValoperExists)\n\t}\n\n\t// TODO add address derivation from public key\n\t// (when the laws of gno make it possible)\n\n\t// Save the valoper to the set\n\tvalopers.Set(v.Address.String(), v)\n}\n\n// Update updates an existing valoper\nfunc Update(address std.Address, v Valoper) {\n\t// Check if the valoper is present\n\tif !isValoper(address) {\n\t\tpanic(errValoperMissing)\n\t}\n\n\t// Check that the valoper wouldn't be\n\t// overwriting an existing one\n\tisAddressUpdate := address != v.Address\n\tif isAddressUpdate \u0026\u0026 isValoper(v.Address) {\n\t\tpanic(errInvalidAddressUpdate)\n\t}\n\n\t// Remove the old valoper info\n\t// in case the address changed\n\tif address != v.Address {\n\t\tvalopers.Remove(address.String())\n\t}\n\n\t// Save the new valoper info\n\tvalopers.Set(v.Address.String(), v)\n}\n\n// GetByAddr fetches the valoper using the address, if present\nfunc GetByAddr(address std.Address) Valoper {\n\tvaloperRaw, exists := valopers.Get(address.String())\n\tif !exists {\n\t\tpanic(errValoperMissing)\n\t}\n\n\treturn valoperRaw.(Valoper)\n}\n\n// Render renders the current valoper set\nfunc Render(_ string) string {\n\tif valopers.Size() == 0 {\n\t\treturn \"No valopers to display.\"\n\t}\n\n\toutput := \"Valset changes to apply:\\n\"\n\tvalopers.Iterate(\"\", \"\", func(_ string, value interface{}) bool {\n\t\tvaloper := value.(Valoper)\n\n\t\toutput += valoper.Render()\n\n\t\treturn false\n\t})\n\n\treturn output\n}\n\n// Render renders a single valoper with their information\nfunc (v Valoper) Render() string {\n\toutput := ufmt.Sprintf(\"## %s (%s)\\n\", v.Name, v.Moniker)\n\toutput += ufmt.Sprintf(\"%s\\n\\n\", v.Description)\n\toutput += ufmt.Sprintf(\"- Address: %s\\n\", v.Address.String())\n\toutput += ufmt.Sprintf(\"- PubKey: %s\\n\", v.PubKey)\n\toutput += \"- P2P Addresses: [\\n\"\n\n\tif len(v.P2PAddresses) == 0 {\n\t\toutput += \"]\\n\"\n\n\t\treturn output\n\t}\n\n\tfor index, addr := range v.P2PAddresses {\n\t\toutput += addr\n\n\t\tif index == len(v.P2PAddresses)-1 {\n\t\t\toutput += \"]\\n\"\n\n\t\t\tcontinue\n\t\t}\n\n\t\toutput += \",\\n\"\n\t}\n\n\treturn output\n}\n\n// isValoper checks if the valoper exists\nfunc isValoper(address std.Address) bool {\n\t_, exists := valopers.Get(address.String())\n\n\treturn exists\n}\n\n// GovDAOProposal creates a proposal to the GovDAO\n// for adding the given valoper to the validator set.\n// This function is meant to serve as a helper\n// for generating the govdao proposal\nfunc GovDAOProposal(address std.Address) {\n\tvar (\n\t\tvaloper = GetByAddr(address)\n\t\tvotingPower = uint64(1)\n\t)\n\n\t// Make sure the valoper is the caller\n\tif std.GetOrigCaller() != address {\n\t\tpanic(errValoperNotCaller)\n\t}\n\n\t// Determine the voting power\n\tif !valoper.Active {\n\t\tvotingPower = uint64(0)\n\t}\n\n\tchangesFn := func() []pVals.Validator {\n\t\treturn []pVals.Validator{\n\t\t\t{\n\t\t\t\tAddress: valoper.Address,\n\t\t\t\tPubKey: valoper.PubKey,\n\t\t\t\tVotingPower: votingPower,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Create the executor\n\texecutor := validators.NewPropExecutor(changesFn)\n\n\t// Craft the proposal description\n\tdescription := ufmt.Sprintf(\n\t\t\"Add valoper %s (Address: %s; PubKey: %s) to the valset\",\n\t\tvaloper.Name,\n\t\tvaloper.Address.String(),\n\t\tvaloper.PubKey,\n\t)\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\t// Create the govdao proposal\n\tbridge.GovDAO().Propose(prop)\n}\n" + }, + { + "name": "valopers_test.gno", + "body": "package valopers\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestValopers_Register(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"already a valoper\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tv := Valoper{\n\t\t\tAddress: testutils.TestAddress(\"valoper\"),\n\t\t}\n\n\t\t// Add the valoper\n\t\tvalopers.Set(v.Address.String(), v)\n\n\t\tuassert.PanicsWithMessage(t, errValoperExists, func() {\n\t\t\tRegister(v)\n\t\t})\n\t})\n\n\tt.Run(\"successful registration\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tv := Valoper{\n\t\t\tAddress: testutils.TestAddress(\"valoper\"),\n\t\t\tName: \"new valoper\",\n\t\t\tMoniker: \"val-1\",\n\t\t\tPubKey: \"pub key\",\n\t\t}\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(v)\n\t\t})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(v.Address)\n\n\t\t\tuassert.Equal(t, v.Address, valoper.Address)\n\t\t\tuassert.Equal(t, v.Name, valoper.Name)\n\t\t\tuassert.Equal(t, v.Moniker, valoper.Moniker)\n\t\t\tuassert.Equal(t, v.PubKey, valoper.PubKey)\n\t\t})\n\t})\n}\n\nfunc TestValopers_Update(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"non-existing valoper\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tv := Valoper{}\n\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, errValoperMissing, func() {\n\t\t\tUpdate(v.Address, v)\n\t\t})\n\t})\n\n\tt.Run(\"overwrite valoper\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tone := Valoper{\n\t\t\tAddress: testutils.TestAddress(\"valoper 1\"),\n\t\t}\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(one)\n\t\t})\n\n\t\tinitialAddress := testutils.TestAddress(\"valoper 2\")\n\t\ttwo := Valoper{\n\t\t\tAddress: initialAddress,\n\t\t}\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(two)\n\t\t})\n\n\t\t// Update the valoper address\n\t\t// so it overlaps\n\t\ttwo = Valoper{\n\t\t\tAddress: one.Address,\n\t\t}\n\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, errInvalidAddressUpdate, func() {\n\t\t\tUpdate(initialAddress, two)\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tvar (\n\t\t\tname = \"new valoper\"\n\t\t\tv = Valoper{\n\t\t\t\tAddress: testutils.TestAddress(\"valoper\"),\n\t\t\t\tName: name,\n\t\t\t\tPubKey: \"pub key\",\n\t\t\t}\n\t\t)\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(v)\n\t\t})\n\n\t\t// Update the valoper name\n\t\tv.Name = \"new name\"\n\t\tv.Active = false\n\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdate(v.Address, v)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(v.Address)\n\n\t\t\tuassert.Equal(t, v.Name, valoper.Name)\n\t\t\tuassert.Equal(t, v.Active, valoper.Active)\n\t\t})\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "config", + "path": "gno.land/r/leon/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar (\n\tmain std.Address // leon's main address\n\tbackup std.Address // backup address\n\n\tErrInvalidAddr = errors.New(\"leon's config: invalid address\")\n\tErrUnauthorized = errors.New(\"leon's config: unauthorized\")\n)\n\nfunc init() {\n\tmain = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n}\n\nfunc Address() std.Address {\n\treturn main\n}\n\nfunc Backup() std.Address {\n\treturn backup\n}\n\nfunc SetAddress(a std.Address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tmain = a\n\treturn nil\n}\n\nfunc SetBackup(a std.Address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tbackup = a\n\treturn nil\n}\n\nfunc checkAuthorized() error {\n\tcaller := std.PrevRealm().Addr()\n\tisAuthorized := caller == main || caller == backup\n\n\tif !isAuthorized {\n\t\treturn ErrUnauthorized\n\t}\n\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "home", + "path": "gno.land/r/leon/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/demo/art/gnoface\"\n\t\"gno.land/r/demo/art/millipede\"\n\t\"gno.land/r/leon/config\"\n)\n\nvar (\n\tpfp string // link to profile picture\n\tpfpCaption string // profile picture caption\n\tabtMe [2]string\n)\n\nfunc init() {\n\tpfp = \"https://i.imgflip.com/91vskx.jpg\"\n\tpfpCaption = \"[My favourite painting \u0026 pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)\"\n\tabtMe = [2]string{\n\t\t`### About me\nHi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast, \nlife-long learner, and sharer of knowledge.`,\n\t\t`### Contributions\nMy contributions to gno.land can mainly be found \n[here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn).\n\nTODO import r/gh\n`,\n\t}\n}\n\nfunc UpdatePFP(url, caption string) {\n\tif !isAuthorized(std.PrevRealm().Addr()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tpfp = url\n\tpfpCaption = caption\n}\n\nfunc UpdateAboutMe(col1, col2 string) {\n\tif !isAuthorized(std.PrevRealm().Addr()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tabtMe[0] = col1\n\tabtMe[1] = col2\n}\n\nfunc Render(path string) string {\n\tout := \"# Leon's Homepage\\n\\n\"\n\n\tout += renderAboutMe()\n\tout += renderBlogPosts()\n\tout += \"\\n\\n\"\n\tout += renderArt()\n\n\treturn out\n}\n\nfunc renderBlogPosts() string {\n\tout := \"\"\n\t//out += \"## Leon's Blog Posts\"\n\n\t// todo fetch blog posts authored by @leohhhn\n\t// and render them\n\treturn out\n}\n\nfunc renderAboutMe() string {\n\tout := \"\u003cdiv class='columns-3'\u003e\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += ufmt.Sprintf(\"![my profile pic](%s)\\n\\n%s\\n\\n\", pfp, pfpCaption)\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += abtMe[0] + \"\\n\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += abtMe[1] + \"\\n\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-3 --\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderArt() string {\n\tout := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tout += \"# Gno Art\\n\\n\"\n\n\tout += \"\u003cdiv class='columns-3'\u003e\"\n\n\tout += renderGnoFace()\n\tout += renderMillipede()\n\tout += \"Empty spot :/\"\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-3 --\u003e\\n\\n\"\n\n\tout += \"This art is dynamic; it will change with every new block.\\n\\n\"\n\tout += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\n\treturn out\n}\n\nfunc renderGnoFace() string {\n\tout := \"\u003cdiv\u003e\\n\\n\"\n\tout += gnoface.Render(strconv.Itoa(int(std.GetHeight())))\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderMillipede() string {\n\tout := \"\u003cdiv\u003e\\n\\n\"\n\tout += \"Millipede\\n\\n\"\n\tout += \"```\\n\" + millipede.Draw(int(std.GetHeight())%10+1) + \"```\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc isAuthorized(addr std.Address) bool {\n\treturn addr == config.Address() || addr == config.Backup()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "config", + "path": "gno.land/r/manfred/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport \"std\"\n\nvar addr = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc Addr() std.Address {\n\treturn addr\n}\n\nfunc UpdateAddr(newAddr std.Address) {\n\tAssertIsAdmin()\n\taddr = newAddr\n}\n\nfunc AssertIsAdmin() {\n\tif std.GetOrigCaller() != addr {\n\t\tpanic(\"restricted area\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "home", + "path": "gno.land/r/manfred/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport \"gno.land/r/manfred/config\"\n\nvar (\n\ttodos []string\n\tstatus string\n\tmemeImgURL string\n)\n\nfunc init() {\n\ttodos = append(todos, \"fill this todo list...\")\n\tstatus = \"Online\" // Initial status set to \"Online\"\n\tmemeImgURL = \"https://i.imgflip.com/7ze8dc.jpg\"\n}\n\nfunc Render(path string) string {\n\tcontent := \"# Manfred's (gn)home Dashboard\\n\\n\"\n\n\tcontent += \"## Meme\\n\"\n\tcontent += \"![](\" + memeImgURL + \")\\n\\n\"\n\n\tcontent += \"## Status\\n\"\n\tcontent += status + \"\\n\\n\"\n\n\tcontent += \"## Personal ToDo List\\n\"\n\tfor _, todo := range todos {\n\t\tcontent += \"- [ ] \" + todo + \"\\n\"\n\t}\n\tcontent += \"\\n\"\n\n\t// TODO: Implement a feature to list replies on r/boards on my posts\n\t// TODO: Maybe integrate a calendar feature for upcoming events?\n\n\treturn content\n}\n\nfunc AddNewTodo(todo string) {\n\tconfig.AssertIsAdmin()\n\ttodos = append(todos, todo)\n}\n\nfunc DeleteTodo(todoIndex int) {\n\tconfig.AssertIsAdmin()\n\tif todoIndex \u003e= 0 \u0026\u0026 todoIndex \u003c len(todos) {\n\t\t// Remove the todo from the list by merging slices from before and after the todo\n\t\ttodos = append(todos[:todoIndex], todos[todoIndex+1:]...)\n\t} else {\n\t\tpanic(\"Invalid todo index\")\n\t}\n}\n\nfunc UpdateStatus(newStatus string) {\n\tconfig.AssertIsAdmin()\n\tstatus = newStatus\n}\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport \"gno.land/r/manfred/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// # Manfred's (gn)home Dashboard\n//\n// ## Meme\n// ![](https://i.imgflip.com/7ze8dc.jpg)\n//\n// ## Status\n// Online\n//\n// ## Personal ToDo List\n// - [ ] fill this todo list...\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/manfred/home\"\n)\n\nfunc main() {\n\tstd.TestSetOrigCaller(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\thome.AddNewTodo(\"aaa\")\n\thome.AddNewTodo(\"bbb\")\n\thome.AddNewTodo(\"ccc\")\n\thome.AddNewTodo(\"ddd\")\n\thome.AddNewTodo(\"eee\")\n\thome.UpdateStatus(\"Lorem Ipsum\")\n\thome.DeleteTodo(3)\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// # Manfred's (gn)home Dashboard\n//\n// ## Meme\n// ![](https://i.imgflip.com/7ze8dc.jpg)\n//\n// ## Status\n// Lorem Ipsum\n//\n// ## Personal ToDo List\n// - [ ] fill this todo list...\n// - [ ] aaa\n// - [ ] bbb\n// - [ ] ddd\n// - [ ] eee\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "present", + "path": "gno.land/r/manfred/present", + "files": [ + { + "name": "admin.gno", + "body": "package present\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // XXX: delete instead?\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\tcaller := std.GetOrigCaller()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.GetOrigCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + }, + { + "name": "present_miami23.gno", + "body": "package present\n\nfunc init() {\n\tpath := \"miami23\"\n\ttitle := \"Portal Loop Demo (Miami 2023)\"\n\tbody := `\nRendered by Gno.\n\n[Source (WIP)](https://github.com/gnolang/gno/pull/1176)\n\n## Portal Loop\n\n- DONE: Dynamic homepage, key pages, aliases, and redirects.\n- TODO: Deploy with history, complete worxdao v0.\n- Will replace the static gno.land site.\n- Enhances local development.\n\n[GitHub Issue](https://github.com/gnolang/gno/issues/1108)\n\n## Roadmap\n\n- Crafting the roadmap this week, open to collaboration.\n- Combining onchain (portal loop) and offchain (GitHub).\n- Next week: Unveiling the official v0 roadmap.\n\n## Teams, DAOs, Projects\n\n- Developing worxDAO contracts for directories of projects and teams.\n- GitHub teams and projects align with this structure.\n- CODEOWNER file updates coming.\n- Initial teams announced next week.\n\n## Tech Team Retreat Plan\n\n- Continue Portal Loop.\n- Consider dApp development.\n- Explore new topics [here](https://github.com/orgs/gnolang/projects/15/).\n- Engage in workshops.\n- Connect and have fun with colleagues.\n`\n\t_ = b.NewPost(adminAddr, path, title, body, \"2023-10-15T13:17:24Z\", []string{\"moul\"}, []string{\"demo\", \"portal-loop\", \"miami\"})\n}\n" + }, + { + "name": "present_miami23_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/manfred/present\"\n)\n\nfunc main() {\n\tprintln(present.Render(\"\"))\n\tprintln(\"------------------------------------\")\n\tprintln(present.Render(\"p/miami23\"))\n}\n" + }, + { + "name": "presentations.gno", + "body": "package present\n\nimport (\n\t\"gno.land/p/demo/blog\"\n)\n\n// TODO: switch from p/blog to p/present\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Manfred's Presentations\",\n\tPrefix: \"/r/manfred/present:\",\n\tNoBreadcrumb: true,\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "guestbook", + "path": "gno.land/r/morgan/guestbook", + "files": [ + { + "name": "admin.gno", + "body": "package guestbook\n\nimport (\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nvar owner = ownable.New()\n\n// AdminDelete removes the guestbook message with the given ID.\n// The user will still be marked as having submitted a message, so they\n// won't be able to re-submit a new message.\nfunc AdminDelete(signatureID string) {\n\towner.AssertCallerIsOwner()\n\n\tid, err := seqid.FromString(signatureID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tidb := id.Binary()\n\tif !guestbook.Has(idb) {\n\t\tpanic(\"signature does not exist\")\n\t}\n\tguestbook.Remove(idb)\n}\n" + }, + { + "name": "guestbook.gno", + "body": "// Realm guestbook contains an implementation of a simple guestbook.\n// Come and sign yourself up!\npackage guestbook\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n)\n\n// Signature is a single entry in the guestbook.\ntype Signature struct {\n\tMessage string\n\tAuthor std.Address\n\tTime time.Time\n}\n\nconst (\n\tmaxMessageLength = 140\n\tmaxPerPage = 25\n)\n\nvar (\n\tsignatureID seqid.ID\n\tguestbook avl.Tree // id -\u003e Signature\n\thasSigned avl.Tree // address -\u003e struct{}\n)\n\nfunc init() {\n\tSign(\"You reached the end of the guestbook!\")\n}\n\nconst (\n\terrNotAUser = \"this guestbook can only be signed by users\"\n\terrAlreadySigned = \"you already signed the guestbook!\"\n\terrInvalidCharacterInMessage = \"invalid character in message\"\n)\n\n// Sign signs the guestbook, with the specified message.\nfunc Sign(message string) {\n\tprev := std.PrevRealm()\n\tswitch {\n\tcase !prev.IsUser():\n\t\tpanic(errNotAUser)\n\tcase hasSigned.Has(prev.Addr().String()):\n\t\tpanic(errAlreadySigned)\n\t}\n\tmessage = validateMessage(message)\n\n\tguestbook.Set(signatureID.Next().Binary(), Signature{\n\t\tMessage: message,\n\t\tAuthor: prev.Addr(),\n\t\t// NOTE: time.Now() will yield the \"block time\", which is deterministic.\n\t\tTime: time.Now(),\n\t})\n\thasSigned.Set(prev.Addr().String(), struct{}{})\n}\n\nfunc validateMessage(msg string) string {\n\tif len(msg) \u003e maxMessageLength {\n\t\tpanic(\"Keep it brief! (max \" + strconv.Itoa(maxMessageLength) + \" bytes!)\")\n\t}\n\tout := \"\"\n\tfor _, ch := range msg {\n\t\tswitch {\n\t\tcase unicode.IsLetter(ch),\n\t\t\tunicode.IsNumber(ch),\n\t\t\tunicode.IsSpace(ch),\n\t\t\tunicode.IsPunct(ch):\n\t\t\tout += string(ch)\n\t\tdefault:\n\t\t\tpanic(errInvalidCharacterInMessage)\n\t\t}\n\t}\n\treturn out\n}\n\nfunc Render(maxID string) string {\n\tvar bld strings.Builder\n\n\tbld.WriteString(\"# Guestbook 📝\\n\\n[Come sign the guestbook!](./guestbook$help\u0026func=Sign)\\n\\n---\\n\\n\")\n\n\tvar maxIDBinary string\n\tif maxID != \"\" {\n\t\tmid, err := seqid.FromString(maxID)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// AVL iteration is exclusive, so we need to decrease the ID value to get the \"true\" maximum.\n\t\tmid--\n\t\tmaxIDBinary = mid.Binary()\n\t}\n\n\tvar lastID seqid.ID\n\tvar printed int\n\tguestbook.ReverseIterate(\"\", maxIDBinary, func(key string, val interface{}) bool {\n\t\tsig := val.(Signature)\n\t\tmessage := strings.ReplaceAll(sig.Message, \"\\n\", \"\\n\u003e \")\n\t\tbld.WriteString(\"\u003e \" + message + \"\\n\u003e\\n\")\n\t\tidValue, ok := seqid.FromBinary(key)\n\t\tif !ok {\n\t\t\tpanic(\"invalid seqid id\")\n\t\t}\n\n\t\tbld.WriteString(\"\u003e _Written by \" + sig.Author.String() + \" at \" + sig.Time.Format(time.DateTime) + \"_ (#\" + idValue.String() + \")\\n\\n---\\n\\n\")\n\t\tlastID = idValue\n\n\t\tprinted++\n\t\t// stop after exceeding limit\n\t\treturn printed \u003e= maxPerPage\n\t})\n\n\tif printed == 0 {\n\t\tbld.WriteString(\"No messages!\")\n\t} else if printed \u003e= maxPerPage {\n\t\tbld.WriteString(\"\u003cp style='text-align:right'\u003e\u003ca href='./guestbook:\" + lastID.String() + \"'\u003eNext page\u003c/a\u003e\u003c/p\u003e\")\n\t}\n\n\treturn bld.String()\n}\n" + }, + { + "name": "guestbook_test.gno", + "body": "package guestbook\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\nfunc TestSign(t *testing.T) {\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user\"))\n\tSign(\"Hello!\")\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user2\"))\n\tSign(\"Hello2!\")\n\n\tres := Render(\"\")\n\tt.Log(res)\n\tif !strings.Contains(res, \"\u003e Hello!\\n\u003e\\n\u003e _Written by g1user \") {\n\t\tt.Error(\"does not contain first user's message\")\n\t}\n\tif !strings.Contains(res, \"\u003e Hello2!\\n\u003e\\n\u003e _Written by g1user2 \") {\n\t\tt.Error(\"does not contain second user's message\")\n\t}\n\tif guestbook.Size() != 2 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n}\n\nfunc TestSign_FromRealm(t *testing.T) {\n\tstd.TestSetRealm(std.NewCodeRealm(\"gno.land/r/demo/users\"))\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Fatal(\"not a string\", rec)\n\t\t} else if recString != errNotAUser {\n\t\t\tt.Fatal(\"invalid error\", recString)\n\t\t}\n\t}()\n\tSign(\"Hey!\")\n}\n\nfunc TestSign_Double(t *testing.T) {\n\t// Should not allow signing twice.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user\"))\n\tSign(\"Hello!\")\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"type assertion failed\", rec)\n\t\t} else if recString != errAlreadySigned {\n\t\t\tt.Error(\"invalid error message\", recString)\n\t\t}\n\t}()\n\n\tSign(\"Hello again!\")\n}\n\nfunc TestSign_InvalidMessage(t *testing.T) {\n\t// Should not allow control characters in message.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user\"))\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"type assertion failed\", rec)\n\t\t} else if recString != errInvalidCharacterInMessage {\n\t\t\tt.Error(\"invalid error message\", recString)\n\t\t}\n\t}()\n\tSign(\"\\x00Hello!\")\n}\n\nfunc TestAdminDelete(t *testing.T) {\n\tconst (\n\t\tuserAddr std.Address = \"g1user\"\n\t\tadminAddr std.Address = \"g1admin\"\n\t)\n\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\towner = ownable.NewWithAddress(adminAddr)\n\tsignatureID = 0\n\n\tstd.TestSetRealm(std.NewUserRealm(userAddr))\n\n\tconst bad = \"Very Bad Message! Nyeh heh heh!\"\n\tSign(bad)\n\n\tif rnd := Render(\"\"); !strings.Contains(rnd, bad) {\n\t\tt.Fatal(\"render does not contain bad message\", rnd)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(adminAddr))\n\tAdminDelete(signatureID.String())\n\n\tif rnd := Render(\"\"); strings.Contains(rnd, bad) {\n\t\tt.Error(\"render contains bad message\", rnd)\n\t}\n\tif guestbook.Size() != 0 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n\tif hasSigned.Size() != 1 {\n\t\tt.Error(\"invalid hasSigned size\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "home", + "path": "gno.land/r/morgan/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nconst staticHome = `# morgan's (gn)home\n\n- [📝 sign my guestbook](/r/morgan/guestbook)\n`\n\nfunc Render(path string) string {\n\treturn staticHome\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "registry", + "path": "gno.land/r/stefann/registry", + "files": [ + { + "name": "registry.gno", + "body": "package registry\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\nvar (\n\tmainAddr std.Address\n\tbackupAddr std.Address\n\towner *ownable.Ownable\n)\n\nfunc init() {\n\tmainAddr = \"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\"\n\tbackupAddr = \"g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8\"\n\n\towner = ownable.NewWithAddress(mainAddr)\n}\n\nfunc MainAddr() std.Address {\n\treturn mainAddr\n}\n\nfunc BackupAddr() std.Address {\n\treturn backupAddr\n}\n\nfunc SetMainAddr(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertCallerIsOwner()\n\n\tmainAddr = addr\n\treturn nil\n}\n\nfunc SetBackupAddr(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertCallerIsOwner()\n\n\tbackupAddr = addr\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "home", + "path": "gno.land/r/stefann/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/stefann/registry\"\n)\n\ntype City struct {\n\tName string\n\tURL string\n}\n\ntype Sponsor struct {\n\tAddress std.Address\n\tAmount std.Coins\n}\n\ntype Profile struct {\n\tpfp string\n\taboutMe []string\n}\n\ntype Travel struct {\n\tcities []City\n\tcurrentCityIndex int\n\tjarLink string\n}\n\ntype Sponsorship struct {\n\tmaxSponsors int\n\tsponsors *avl.Tree\n\tDonationsCount int\n\tsponsorsCount int\n}\n\nvar (\n\tprofile Profile\n\ttravel Travel\n\tsponsorship Sponsorship\n\towner *ownable.Ownable\n)\n\nfunc init() {\n\towner = ownable.NewWithAddress(registry.MainAddr())\n\n\tprofile = Profile{\n\t\tpfp: \"https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg\",\n\t\taboutMe: []string{\n\t\t\t`### About Me`,\n\t\t\t`Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`,\n\n\t\t\t`### Contributions`,\n\t\t\t`I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`,\n\t\t},\n\t}\n\n\ttravel = Travel{\n\t\tcities: []City{\n\t\t\t{Name: \"Venice\", URL: \"https://i.ibb.co/1mcZ7b1/venice.jpg\"},\n\t\t\t{Name: \"Tokyo\", URL: \"https://i.ibb.co/wNDJv3H/tokyo.jpg\"},\n\t\t\t{Name: \"São Paulo\", URL: \"https://i.ibb.co/yWMq2Sn/sao-paulo.jpg\"},\n\t\t\t{Name: \"Toronto\", URL: \"https://i.ibb.co/pb95HJB/toronto.jpg\"},\n\t\t\t{Name: \"Bangkok\", URL: \"https://i.ibb.co/pQy3w2g/bangkok.jpg\"},\n\t\t\t{Name: \"New York\", URL: \"https://i.ibb.co/6JWLm0h/new-york.jpg\"},\n\t\t\t{Name: \"Paris\", URL: \"https://i.ibb.co/q9vf6Hs/paris.jpg\"},\n\t\t\t{Name: \"Kandersteg\", URL: \"https://i.ibb.co/60DzywD/kandersteg.jpg\"},\n\t\t\t{Name: \"Rothenburg\", URL: \"https://i.ibb.co/cr8d2rQ/rothenburg.jpg\"},\n\t\t\t{Name: \"Capetown\", URL: \"https://i.ibb.co/bPGn0v3/capetown.jpg\"},\n\t\t\t{Name: \"Sydney\", URL: \"https://i.ibb.co/TBNzqfy/sydney.jpg\"},\n\t\t\t{Name: \"Oeschinen Lake\", URL: \"https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg\"},\n\t\t\t{Name: \"Barra Grande\", URL: \"https://i.ibb.co/z4RXKc1/barra-grande.jpg\"},\n\t\t\t{Name: \"London\", URL: \"https://i.ibb.co/CPGtvgr/london.jpg\"},\n\t\t},\n\t\tcurrentCityIndex: 0,\n\t\tjarLink: \"https://TODO\", // This value should be injected through UpdateJarLink after deployment.\n\t}\n\n\tsponsorship = Sponsorship{\n\t\tmaxSponsors: 5,\n\t\tsponsors: avl.NewTree(),\n\t\tDonationsCount: 0,\n\t\tsponsorsCount: 0,\n\t}\n}\n\nfunc UpdateCities(newCities []City) {\n\towner.AssertCallerIsOwner()\n\ttravel.cities = newCities\n}\n\nfunc AddCities(newCities ...City) {\n\towner.AssertCallerIsOwner()\n\n\ttravel.cities = append(travel.cities, newCities...)\n}\n\nfunc UpdateJarLink(newLink string) {\n\towner.AssertCallerIsOwner()\n\ttravel.jarLink = newLink\n}\n\nfunc UpdatePFP(url string) {\n\towner.AssertCallerIsOwner()\n\tprofile.pfp = url\n}\n\nfunc UpdateAboutMe(aboutMeStr string) {\n\towner.AssertCallerIsOwner()\n\tprofile.aboutMe = strings.Split(aboutMeStr, \"|\")\n}\n\nfunc AddAboutMeRows(newRows ...string) {\n\towner.AssertCallerIsOwner()\n\n\tprofile.aboutMe = append(profile.aboutMe, newRows...)\n}\n\nfunc UpdateMaxSponsors(newMax int) {\n\towner.AssertCallerIsOwner()\n\tif newMax \u003c= 0 {\n\t\tpanic(\"maxSponsors must be greater than zero\")\n\t}\n\tsponsorship.maxSponsors = newMax\n}\n\nfunc Donate() {\n\taddress := std.GetOrigCaller()\n\tamount := std.GetOrigSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\texistingAmount, exists := sponsorship.sponsors.Get(address.String())\n\tif exists {\n\t\tupdatedAmount := existingAmount.(std.Coins).Add(amount)\n\t\tsponsorship.sponsors.Set(address.String(), updatedAmount)\n\t} else {\n\t\tsponsorship.sponsors.Set(address.String(), amount)\n\t\tsponsorship.sponsorsCount++\n\t}\n\n\ttravel.currentCityIndex++\n\tsponsorship.DonationsCount++\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\townerAddr := registry.MainAddr()\n\tbanker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr()))\n}\n\ntype SponsorSlice []Sponsor\n\nfunc (s SponsorSlice) Len() int {\n\treturn len(s)\n}\n\nfunc (s SponsorSlice) Less(i, j int) bool {\n\treturn s[i].Amount.AmountOf(\"ugnot\") \u003e s[j].Amount.AmountOf(\"ugnot\")\n}\n\nfunc (s SponsorSlice) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc GetTopSponsors() []Sponsor {\n\tvar sponsorSlice SponsorSlice\n\n\tsponsorship.sponsors.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\taddr := std.Address(key)\n\t\tamount := value.(std.Coins)\n\t\tsponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount})\n\t\treturn false\n\t})\n\n\tsort.Sort(sponsorSlice)\n\treturn sponsorSlice\n}\n\nfunc GetTotalDonations() int {\n\ttotal := 0\n\tsponsorship.sponsors.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttotal += int(value.(std.Coins).AmountOf(\"ugnot\"))\n\t\treturn false\n\t})\n\treturn total\n}\n\nfunc Render(path string) string {\n\tout := ufmt.Sprintf(\"# Exploring %s!\\n\\n\", travel.cities[travel.currentCityIndex].Name)\n\n\tout += renderAboutMe()\n\tout += \"\\n\\n\"\n\tout += renderTips()\n\n\treturn out\n}\n\nfunc renderAboutMe() string {\n\tout := \"\u003cdiv class='rows-3'\u003e\"\n\n\tout += \"\u003cdiv style='position: relative; text-align: center;'\u003e\\n\\n\"\n\n\tout += ufmt.Sprintf(\"\u003cdiv style='background-image: url(%s); background-size: cover; background-position: center; width: 100%%; height: 600px; position: relative; border-radius: 15px; overflow: hidden;'\u003e\\n\\n\", travel.cities[travel.currentCityIndex%len(travel.cities)].URL)\n\n\tout += ufmt.Sprintf(\"\u003cimg src='%s' alt='my profile pic' style='width: 250px; height: auto; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 50%%; border: 3px solid #1e1e1e; position: absolute; top: 75%%; left: 50%%; transform: translate(-50%%, -50%%);'\u003e\\n\\n\", profile.pfp)\n\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tfor _, rows := range profile.aboutMe {\n\t\tout += \"\u003cdiv\u003e\\n\\n\"\n\t\tout += rows + \"\\n\\n\"\n\t\tout += \"\u003c/div\u003e\\n\\n\"\n\t}\n\n\tout += \"\u003c/div\u003e\u003c!-- /rows-3 --\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderTips() string {\n\tout := `\u003cdiv class=\"jumbotron\" style=\"display: flex; flex-direction: column; justify-content: flex-start; align-items: center; padding-top: 40px; padding-bottom: 50px; text-align: center;\"\u003e` + \"\\n\\n\"\n\n\tout += `\u003cdiv class=\"rows-2\" style=\"max-width: 500px; width: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;\"\u003e` + \"\\n\"\n\n\tout += `\u003ch1 style=\"margin-bottom: 50px;\"\u003eHelp Me Travel The World\u003c/h1\u003e` + \"\\n\\n\"\n\n\tout += renderTipsJar() + \"\\n\"\n\n\tout += ufmt.Sprintf(`\u003cstrong style=\"font-size: 1.2em;\"\u003eI am currently in %s, \u003cbr\u003e tip the jar to send me somewhere else!\u003c/strong\u003e`, travel.cities[travel.currentCityIndex].Name)\n\n\tout += `\u003cbr\u003e\u003cspan style=\"font-size: 1.2em; font-style: italic; margin-top: 10px; display: inline-block;\"\u003eClick the jar, tip in GNOT coins, and watch my background change as I head to a new adventure!\u003c/span\u003e\u003c/p\u003e` + \"\\n\\n\"\n\n\tout += renderSponsors()\n\n\tout += `\u003c/div\u003e\u003c!-- /rows-2 --\u003e` + \"\\n\\n\"\n\n\tout += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\n\treturn out\n}\n\nfunc formatAddress(address string) string {\n\tif len(address) \u003c= 8 {\n\t\treturn address\n\t}\n\treturn address[:4] + \"...\" + address[len(address)-4:]\n}\n\nfunc renderSponsors() string {\n\tout := `\u003ch3 style=\"margin-top: 5px; margin-bottom: 20px\"\u003eSponsor Leaderboard\u003c/h3\u003e` + \"\\n\"\n\n\tif sponsorship.sponsorsCount == 0 {\n\t\treturn out + `\u003cp style=\"text-align: center;\"\u003eNo sponsors yet. Be the first to tip the jar!\u003c/p\u003e` + \"\\n\"\n\t}\n\n\ttopSponsors := GetTopSponsors()\n\tnumSponsors := len(topSponsors)\n\tif numSponsors \u003e sponsorship.maxSponsors {\n\t\tnumSponsors = sponsorship.maxSponsors\n\t}\n\n\tout += `\u003cul style=\"list-style-type: none; padding: 0; border: 1px solid #ddd; border-radius: 8px; width: 100%; max-width: 300px; margin: 0 auto;\"\u003e` + \"\\n\"\n\n\tfor i := 0; i \u003c numSponsors; i++ {\n\t\tsponsor := topSponsors[i]\n\t\tisLastItem := (i == numSponsors-1)\n\n\t\tpadding := \"10px 5px\"\n\t\tborder := \"border-bottom: 1px solid #ddd;\"\n\n\t\tif isLastItem {\n\t\t\tpadding = \"8px 5px\"\n\t\t\tborder = \"\"\n\t\t}\n\n\t\tout += ufmt.Sprintf(\n\t\t\t`\u003cli style=\"padding: %s; %s text-align: left;\"\u003e\n\t\t\t\t\u003cstrong style=\"padding-left: 5px;\"\u003e%d. %s\u003c/strong\u003e\n\t\t\t\t\u003cspan style=\"float: right; padding-right: 5px;\"\u003e%s\u003c/span\u003e\n\t\t\t\u003c/li\u003e`,\n\t\t\tpadding, border, i+1, formatAddress(sponsor.Address.String()), sponsor.Amount.String(),\n\t\t)\n\t}\n\n\treturn out\n}\n\nfunc renderTipsJar() string {\n\tout := ufmt.Sprintf(`\u003ca href=\"%s\" target=\"_blank\" style=\"display: block; text-decoration: none;\"\u003e`, travel.jarLink) + \"\\n\"\n\n\tout += `\u003cimg src=\"https://i.ibb.co/4TH9zbw/tips-jar.png\" alt=\"Tips Jar\" style=\"width: 300px; height: auto; display: block; margin: 0 auto;\"\u003e` + \"\\n\"\n\n\tout += `\u003c/a\u003e` + \"\\n\"\n\n\treturn out\n}\n" + }, + { + "name": "home_test.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestUpdatePFP(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tprofile.pfp = \"\"\n\n\tUpdatePFP(\"https://example.com/pic.png\")\n\n\tif profile.pfp != \"https://example.com/pic.png\" {\n\t\tt.Fatalf(\"expected pfp to be https://example.com/pic.png, got %s\", profile.pfp)\n\t}\n}\n\nfunc TestUpdateAboutMe(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tprofile.aboutMe = []string{}\n\n\tUpdateAboutMe(\"This is my new bio.|I love coding!\")\n\n\texpected := []string{\"This is my new bio.\", \"I love coding!\"}\n\n\tif len(profile.aboutMe) != len(expected) {\n\t\tt.Fatalf(\"expected aboutMe to have length %d, got %d\", len(expected), len(profile.aboutMe))\n\t}\n\n\tfor i := range profile.aboutMe {\n\t\tif profile.aboutMe[i] != expected[i] {\n\t\t\tt.Fatalf(\"expected aboutMe[%d] to be %s, got %s\", i, expected[i], profile.aboutMe[i])\n\t\t}\n\t}\n}\n\nfunc TestUpdateCities(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\ttravel.cities = []City{}\n\n\tnewCities := []City{\n\t\t{Name: \"Berlin\", URL: \"https://example.com/berlin.jpg\"},\n\t\t{Name: \"Vienna\", URL: \"https://example.com/vienna.jpg\"},\n\t}\n\n\tUpdateCities(newCities)\n\n\tif len(travel.cities) != 2 {\n\t\tt.Fatalf(\"expected 2 cities, got %d\", len(travel.cities))\n\t}\n\n\tif travel.cities[0].Name != \"Berlin\" || travel.cities[1].Name != \"Vienna\" {\n\t\tt.Fatalf(\"expected cities to be updated to Berlin and Vienna, got %+v\", travel.cities)\n\t}\n}\n\nfunc TestUpdateJarLink(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\ttravel.jarLink = \"\"\n\n\tUpdateJarLink(\"https://example.com/jar\")\n\n\tif travel.jarLink != \"https://example.com/jar\" {\n\t\tt.Fatalf(\"expected jarLink to be https://example.com/jar, got %s\", travel.jarLink)\n\t}\n}\n\nfunc TestUpdateMaxSponsors(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tsponsorship.maxSponsors = 0\n\n\tUpdateMaxSponsors(10)\n\n\tif sponsorship.maxSponsors != 10 {\n\t\tt.Fatalf(\"expected maxSponsors to be 10, got %d\", sponsorship.maxSponsors)\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatalf(\"expected panic for setting maxSponsors to 0\")\n\t\t}\n\t}()\n\tUpdateMaxSponsors(0)\n}\n\nfunc TestAddCities(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\ttravel.cities = []City{}\n\n\tAddCities(City{Name: \"Berlin\", URL: \"https://example.com/berlin.jpg\"})\n\n\tif len(travel.cities) != 1 {\n\t\tt.Fatalf(\"expected 1 city, got %d\", len(travel.cities))\n\t}\n\tif travel.cities[0].Name != \"Berlin\" || travel.cities[0].URL != \"https://example.com/berlin.jpg\" {\n\t\tt.Fatalf(\"expected city to be Berlin, got %+v\", travel.cities[0])\n\t}\n\n\tAddCities(\n\t\tCity{Name: \"Paris\", URL: \"https://example.com/paris.jpg\"},\n\t\tCity{Name: \"Tokyo\", URL: \"https://example.com/tokyo.jpg\"},\n\t)\n\n\tif len(travel.cities) != 3 {\n\t\tt.Fatalf(\"expected 3 cities, got %d\", len(travel.cities))\n\t}\n\tif travel.cities[1].Name != \"Paris\" || travel.cities[2].Name != \"Tokyo\" {\n\t\tt.Fatalf(\"expected cities to be Paris and Tokyo, got %+v\", travel.cities[1:])\n\t}\n}\n\nfunc TestAddAboutMeRows(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tprofile.aboutMe = []string{}\n\n\tAddAboutMeRows(\"I love exploring new technologies!\")\n\n\tif len(profile.aboutMe) != 1 {\n\t\tt.Fatalf(\"expected 1 aboutMe row, got %d\", len(profile.aboutMe))\n\t}\n\tif profile.aboutMe[0] != \"I love exploring new technologies!\" {\n\t\tt.Fatalf(\"expected first aboutMe row to be 'I love exploring new technologies!', got %s\", profile.aboutMe[0])\n\t}\n\n\tAddAboutMeRows(\"Travel is my passion!\", \"Always learning.\")\n\n\tif len(profile.aboutMe) != 3 {\n\t\tt.Fatalf(\"expected 3 aboutMe rows, got %d\", len(profile.aboutMe))\n\t}\n\tif profile.aboutMe[1] != \"Travel is my passion!\" || profile.aboutMe[2] != \"Always learning.\" {\n\t\tt.Fatalf(\"expected aboutMe rows to be 'Travel is my passion!' and 'Always learning.', got %+v\", profile.aboutMe[1:])\n\t}\n}\n\nfunc TestDonate(t *testing.T) {\n\tvar user = testutils.TestAddress(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.DonationsCount = 0\n\tsponsorship.sponsorsCount = 0\n\ttravel.currentCityIndex = 0\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 500))\n\tstd.TestSetOrigSend(coinsSent, std.NewCoins())\n\tDonate()\n\n\texistingAmount, exists := sponsorship.sponsors.Get(string(user))\n\tif !exists {\n\t\tt.Fatalf(\"expected sponsor to be added, but it was not found\")\n\t}\n\n\tif existingAmount.(std.Coins).AmountOf(\"ugnot\") != 500 {\n\t\tt.Fatalf(\"expected donation amount to be 500ugnot, got %d\", existingAmount.(std.Coins).AmountOf(\"ugnot\"))\n\t}\n\n\tif sponsorship.DonationsCount != 1 {\n\t\tt.Fatalf(\"expected DonationsCount to be 1, got %d\", sponsorship.DonationsCount)\n\t}\n\n\tif sponsorship.sponsorsCount != 1 {\n\t\tt.Fatalf(\"expected sponsorsCount to be 1, got %d\", sponsorship.sponsorsCount)\n\t}\n\n\tif travel.currentCityIndex != 1 {\n\t\tt.Fatalf(\"expected currentCityIndex to be 1, got %d\", travel.currentCityIndex)\n\t}\n\n\tcoinsSent = std.NewCoins(std.NewCoin(\"ugnot\", 300))\n\tstd.TestSetOrigSend(coinsSent, std.NewCoins())\n\tDonate()\n\n\texistingAmount, exists = sponsorship.sponsors.Get(string(user))\n\tif !exists {\n\t\tt.Fatalf(\"expected sponsor to exist after second donation, but it was not found\")\n\t}\n\n\tif existingAmount.(std.Coins).AmountOf(\"ugnot\") != 800 {\n\t\tt.Fatalf(\"expected total donation amount to be 800ugnot, got %d\", existingAmount.(std.Coins).AmountOf(\"ugnot\"))\n\t}\n\n\tif sponsorship.DonationsCount != 2 {\n\t\tt.Fatalf(\"expected DonationsCount to be 2 after second donation, got %d\", sponsorship.DonationsCount)\n\t}\n\n\tif travel.currentCityIndex != 2 {\n\t\tt.Fatalf(\"expected currentCityIndex to be 2 after second donation, got %d\", travel.currentCityIndex)\n\t}\n}\n\nfunc TestGetTopSponsors(t *testing.T) {\n\tvar user = testutils.TestAddress(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.sponsorsCount = 0\n\n\tsponsorship.sponsors.Set(\"g1address1\", std.NewCoins(std.NewCoin(\"ugnot\", 300)))\n\tsponsorship.sponsors.Set(\"g1address2\", std.NewCoins(std.NewCoin(\"ugnot\", 500)))\n\tsponsorship.sponsors.Set(\"g1address3\", std.NewCoins(std.NewCoin(\"ugnot\", 200)))\n\tsponsorship.sponsorsCount = 3\n\n\ttopSponsors := GetTopSponsors()\n\n\tif len(topSponsors) != 3 {\n\t\tt.Fatalf(\"expected 3 sponsors, got %d\", len(topSponsors))\n\t}\n\n\tif topSponsors[0].Address.String() != \"g1address2\" || topSponsors[0].Amount.AmountOf(\"ugnot\") != 500 {\n\t\tt.Fatalf(\"expected top sponsor to be g1address2 with 500ugnot, got %s with %dugnot\", topSponsors[0].Address.String(), topSponsors[0].Amount.AmountOf(\"ugnot\"))\n\t}\n\n\tif topSponsors[1].Address.String() != \"g1address1\" || topSponsors[1].Amount.AmountOf(\"ugnot\") != 300 {\n\t\tt.Fatalf(\"expected second sponsor to be g1address1 with 300ugnot, got %s with %dugnot\", topSponsors[1].Address.String(), topSponsors[1].Amount.AmountOf(\"ugnot\"))\n\t}\n\n\tif topSponsors[2].Address.String() != \"g1address3\" || topSponsors[2].Amount.AmountOf(\"ugnot\") != 200 {\n\t\tt.Fatalf(\"expected third sponsor to be g1address3 with 200ugnot, got %s with %dugnot\", topSponsors[2].Address.String(), topSponsors[2].Amount.AmountOf(\"ugnot\"))\n\t}\n}\n\nfunc TestGetTotalDonations(t *testing.T) {\n\tvar user = testutils.TestAddress(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.sponsorsCount = 0\n\n\tsponsorship.sponsors.Set(\"g1address1\", std.NewCoins(std.NewCoin(\"ugnot\", 300)))\n\tsponsorship.sponsors.Set(\"g1address2\", std.NewCoins(std.NewCoin(\"ugnot\", 500)))\n\tsponsorship.sponsors.Set(\"g1address3\", std.NewCoins(std.NewCoin(\"ugnot\", 200)))\n\tsponsorship.sponsorsCount = 3\n\n\ttotalDonations := GetTotalDonations()\n\n\tif totalDonations != 1000 {\n\t\tt.Fatalf(\"expected total donations to be 1000ugnot, got %dugnot\", totalDonations)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\ttravel.currentCityIndex = 0\n\ttravel.cities = []City{\n\t\t{Name: \"Venice\", URL: \"https://example.com/venice.jpg\"},\n\t\t{Name: \"Paris\", URL: \"https://example.com/paris.jpg\"},\n\t}\n\n\toutput := Render(\"\")\n\n\texpectedCity := \"Venice\"\n\tif !strings.Contains(output, expectedCity) {\n\t\tt.Fatalf(\"expected output to contain city name '%s', got %s\", expectedCity, output)\n\t}\n\n\texpectedURL := \"https://example.com/venice.jpg\"\n\tif !strings.Contains(output, expectedURL) {\n\t\tt.Fatalf(\"expected output to contain city URL '%s', got %s\", expectedURL, output)\n\t}\n\n\ttravel.currentCityIndex = 1\n\toutput = Render(\"\")\n\n\texpectedCity = \"Paris\"\n\tif !strings.Contains(output, expectedCity) {\n\t\tt.Fatalf(\"expected output to contain city name '%s', got %s\", expectedCity, output)\n\t}\n\n\texpectedURL = \"https://example.com/paris.jpg\"\n\tif !strings.Contains(output, expectedURL) {\n\t\tt.Fatalf(\"expected output to contain city URL '%s', got %s\", expectedURL, output)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "rewards", + "path": "gno.land/r/sys/rewards", + "files": [ + { + "name": "rewards.gno", + "body": "// This package will be used to manage proof-of-contributions on the exposed smart-contract side.\npackage rewards\n\n// TODO: write specs.\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "users", + "path": "gno.land/r/sys/users", + "files": [ + { + "name": "verify.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\" // @moul\n\ntype VerifyNameFunc func(enabled bool, address std.Address, name string) bool\n\nvar (\n\towner = ownable.NewWithAddress(admin) // Package owner\n\tcheckFunc = VerifyNameByUser // Checking namespace callback\n\tenabled = true // For now this package is disabled by default\n)\n\nfunc IsEnabled() bool { return enabled }\n\n// This method ensures that the given address has ownership of the given name.\nfunc IsAuthorizedAddressForName(address std.Address, name string) bool {\n\treturn checkFunc(enabled, address, name)\n}\n\n// VerifyNameByUser checks from the `users` package that the user has correctly\n// registered the given name.\n// This function considers as valid an `address` that matches the `name`.\nfunc VerifyNameByUser(enable bool, address std.Address, name string) bool {\n\tif !enable {\n\t\treturn true\n\t}\n\n\t// Allow user with their own address as name\n\tif address.String() == name {\n\t\treturn true\n\t}\n\n\tif user := users.GetUserByName(name); user != nil {\n\t\treturn user.Address == address\n\t}\n\n\treturn false\n}\n\n// Admin calls\n\n// Enable this package.\nfunc AdminEnable() {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tenabled = true\n}\n\n// Disable this package.\nfunc AdminDisable() {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tenabled = false\n}\n\n// AdminUpdateVerifyCall updates the method that verifies the namespace.\nfunc AdminUpdateVerifyCall(check VerifyNameFunc) {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tcheckFunc = check\n}\n\n// AdminTransferOwnership transfers the ownership to a new owner.\nfunc AdminTransferOwnership(newOwner std.Address) error {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn owner.TransferOwnership(newOwner)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": null, + "signature": null + } + ], + "memo": "" + } + } + ] + } +} \ No newline at end of file diff --git a/misc/deployments/test5.gno.land/genesis_balances.txt b/misc/deployments/test5.gno.land/genesis_balances.txt new file mode 100644 index 00000000000..132f3b4369a --- /dev/null +++ b/misc/deployments/test5.gno.land/genesis_balances.txt @@ -0,0 +1,83 @@ +# Predeploy Accounts + +g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=131000000ugnot # Test1 (just enough for predeployment) + +# Faucet Accounts (Core) + +g13fzhe4655aqdfr3flydd3pt9s0f4a775g96wj7=9000000000000000000ugnot # Faucet #0 +g1mdy2f562he07a5txs8nvjelstur90e5sg5tkux=9000000000000000000ugnot # Faucet #1 +g1wmw2czwy260sydkupu53k6aeh6gxtf3e0egtku=9000000000000000000ugnot # Faucet #2 +g14vzc065ntj3rq3gfz9my3aja0yyezv7frmjsy3=9000000000000000000ugnot # Faucet #3 +g1pw4ju09ac9y0nj9lltglctk9zq7klk0tkttygk=9000000000000000000ugnot # Faucet #4 +g1dvkfj5q79r3fnepqa0u5ym9d5l3dw83z203j02=9000000000000000000ugnot # Faucet #5 +g1a6jf5g6gkhn5rxcvwxq5zjxgwaznjr9r8gehey=9000000000000000000ugnot # Faucet #6 +g1cx6s2rd4274vhvg509cwglw8senpq00ldqrntv=9000000000000000000ugnot # Faucet #7 +g1yllclm55ls04dtemcwqgd0nyvyem0s8v6arwzt=9000000000000000000ugnot # Faucet #8 +g1j40cmy9yefpwtesqzutc347d48uzk4428zu536=9000000000000000000ugnot # Faucet #9 + +# Faucet Accounts (DevX) + +g1q6jrp203fq0239pv38sdq3y3urvd6vt5azacpv=9000000000000000000ugnot # Faucet #10 +g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=9000000000000000000ugnot # Faucet #11 + +# Core Team + +g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj=9000000000000000000ugnot # Jae +g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=9000000000000000000ugnot # Manfred +g1e6gxg5tvc55mwsn7t7dymmlasratv7mkv0rap2=9000000000000000000ugnot # Milos +g1jazghxvvgz3egnr2fc8uf72z4g0l03596y9ls7=9000000000000000000ugnot # Nemanja +g1qhskthp2uycmg4zsdc9squ2jds7yv3t0qyrlnp=9000000000000000000ugnot # Petar +g18amm3fc00t43dcxsys6udug0czyvqt9e7p23rd=9000000000000000000ugnot # Marc +g1dfr24yhk5ztwtqn2a36m8f6ud8cx5hww4dkjfl=9000000000000000000ugnot # Antonio +g19p3yzr3cuhzqa02j0ce6kzvyjqfzwemw3vam0x=9000000000000000000ugnot # Guilhem +g1mx4pum9976th863jgry4sdjzfwu03qan5w2v9j=9000000000000000000ugnot # Ray +g127l4gkhk0emwsx5tmxe96sp86c05h8vg5tufzq=9000000000000000000ugnot # Maxwell +g1acn3xssksatydd0fcuslvgmjyw0fzkjdhusddg=9000000000000000000ugnot # Dylan +g1cpx59z5r8vzeww2fm4ezpz7yvjs7kptywkm864=9000000000000000000ugnot # Morgan +g1ker4vvggvsyatexxn3hkthp2hu80pkhrwmuczr=9000000000000000000ugnot # Sergio +g18x425qmujg99cfz3q97y4uep5pxjq3z8lmpt25=9000000000000000000ugnot # Antoine + +# DevRel + +g1778y2yphxs2wpuaflsy5y9qwcd4gttn4g5yjx5=9000000000000000000ugnot # Michelle +g1whzkakk4hzjkvy60d5pwfk484xu67ar2cl62h2=9000000000000000000ugnot # Kirk +g125em6arxsnj49vx35f0n0z34putv5ty3376fg5=9000000000000000000ugnot # Leon + +# DevX Team + +g16tfrrul20g4jzt3z303raqw8vs8s2pqqh5clwu=9000000000000000000ugnot # Ilker +g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun=9000000000000000000ugnot # Jerónimo +g15ruzptpql4dpuyzej0wkt5rq6r26kw4nxu9fwd=9000000000000000000ugnot # Denis +g1dnllrdzwfhxv3evyk09y48mgn5phfjvtyrlzm7=9000000000000000000ugnot # Danny +g197q5e9v00vuz256ly7fq7v3ekaun5cr7wmjgfh=9000000000000000000ugnot # Salvo +g1mpkp5lm8lwpm0pym4388836d009zfe4maxlqsq=9000000000000000000ugnot # Alexis + +# AiB + +g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr=9000000000000000000ugnot # Albert + +g1gu6wrz7xcavjtk2dudsfl586qrz5g4ahhhz2j3=9000000000000000000ugnot # aib-val-01 +g1x7rewh0w7u7yrmsmadq6w6t3jwh7ec6ql02klh=9000000000000000000ugnot # aib-val-02 +g1l8j7ts0gmghag7zmnatq5ta5xg83ylyxnmaxlh=9000000000000000000ugnot # aib-val-03 + +# Onbloc + +g1er355fkjksqpdtwmhf5penwa82p0rhqxkkyhk5=9000000000000000000ugnot +g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=9000000000000000000ugnot + +# Berty + +g1qynsu9dwj9lq0m5fkje7jh6qy3md80ztqnshhm=9000000000000000000ugnot + +# Dragos + +g16f5chytu99dmjqtekxf8qzg04vcv7dck6qny6d=9000000000000000000ugnot # Flippando faucet +g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3=9000000000000000000ugnot # ZenTasktic faucet + +# Teritori + +g1qrvwpcw0uxr22d8kgydfz3wp8rtl2h2l3lqmva=9000000000000000000ugnot # team address +g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a=9000000000000000000ugnot # norman +g1g69npft5fav254rvuay7xlmlvt7ddfucgvx8xf=9000000000000000000ugnot # gh0st +g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r=9000000000000000000ugnot # Mikael +g14vxq5e5pt5sev7rkz2ej438scmxtylnzv5vnkw=9000000000000000000ugnot # mikecito diff --git a/misc/deployments/test5.gno.land/genesis_txs.jsonl b/misc/deployments/test5.gno.land/genesis_txs.jsonl new file mode 100755 index 00000000000..7d03fddc523 --- /dev/null +++ b/misc/deployments/test5.gno.land/genesis_txs.jsonl @@ -0,0 +1,131 @@ +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"bank","path":"gno.land/p/demo/bank","files":[{"name":"types.gno","body":"// TODO: this is an example, and needs to be fixed up and tested.\n\npackage bank\n\n// NOTE: unexposed struct for security.\ntype order struct {\n\tfrom Address\n\tto Address\n\tamount Coins\n\tprocessed bool\n}\n\n// NOTE: unexposed methods for security.\nfunc (ch *order) string() string {\n\treturn \"TODO\"\n}\n\n// Wraps the internal *order for external use.\ntype Order struct {\n\t*order\n}\n\n// XXX only exposed for demonstration. TODO unexpose, make full demo.\nfunc NewOrder(from Address, to Address, amount Coins) Order {\n\treturn Order{\n\t\torder: \u0026order{\n\t\t\tfrom: from,\n\t\t\tto: to,\n\t\t\tamount: amount,\n\t\t},\n\t}\n}\n\n// Panics if error, or already processed.\nfunc (o Order) Execute() {\n\tif o.order.processed {\n\t\tpanic(\"order already processed\")\n\t}\n\to.order.processed = true\n\t// TODO implemement.\n}\n\nfunc (o Order) IsZero() bool {\n\treturn o.order == nil\n}\n\nfunc (o Order) From() Address {\n\treturn o.order.from\n}\n\nfunc (o Order) To() Address {\n\treturn o.order.to\n}\n\nfunc (o Order) Amount() Coins {\n\treturn o.order.amount\n}\n\nfunc (o Order) Processed() bool {\n\treturn o.order.processed\n}\n\n//----------------------------------------\n// Escrow\n\ntype EscrowTerms struct {\n\tPartyA Address\n\tPartyB Address\n\tAmountA Coins\n\tAmountB Coins\n}\n\ntype EscrowContract struct {\n\tEscrowTerms\n\tOrderA Order\n\tOrderB Order\n}\n\nfunc CreateEscrow(terms EscrowTerms) *EscrowContract {\n\treturn \u0026EscrowContract{\n\t\tEscrowTerms: terms,\n\t}\n}\n\nfunc (esc *EscrowContract) SetOrderA(order Order) {\n\tif !esc.OrderA.IsZero() {\n\t\tpanic(\"order-a already set\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.From() {\n\t\tpanic(\"invalid order-a:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.To() {\n\t\tpanic(\"invalid order-a:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountA.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-a amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) SetOrderB(order Order) {\n\tif !esc.OrderB.IsZero() {\n\t\tpanic(\"order-b already set\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.From() {\n\t\tpanic(\"invalid order-b:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.To() {\n\t\tpanic(\"invalid order-b:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountB.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-b amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) Execute() {\n\tif esc.OrderA.IsZero() {\n\t\tpanic(\"order-a not yet set\")\n\t}\n\tif esc.OrderB.IsZero() {\n\t\tpanic(\"order-b not yet set\")\n\t}\n\t// NOTE: succeeds atomically.\n\tesc.OrderA.Execute()\n\tesc.OrderB.Execute()\n}\n\n//----------------------------------------\n// TODO: actually implement these in std package.\n\ntype (\n\tAddress string\n\tCoins []Coin\n\tCoin struct {\n\t\tDenom bool\n\t\tAmount int64\n\t}\n)\n\nfunc (a Coins) Equal(b Coins) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"avl","path":"gno.land/p/demo/avl","files":[{"name":"node.gno","body":"package avl\n\n//----------------------------------------\n// Node\n\n// Node represents a node in an AVL tree.\ntype Node struct {\n\tkey string // key is the unique identifier for the node.\n\tvalue interface{} // value is the data stored in the node.\n\theight int8 // height is the height of the node in the tree.\n\tsize int // size is the number of nodes in the subtree rooted at this node.\n\tleftNode *Node // leftNode is the left child of the node.\n\trightNode *Node // rightNode is the right child of the node.\n}\n\n// NewNode creates a new node with the given key and value.\nfunc NewNode(key string, value interface{}) *Node {\n\treturn \u0026Node{\n\t\tkey: key,\n\t\tvalue: value,\n\t\theight: 0,\n\t\tsize: 1,\n\t}\n}\n\n// Size returns the size of the subtree rooted at the node.\nfunc (node *Node) Size() int {\n\tif node == nil {\n\t\treturn 0\n\t}\n\treturn node.size\n}\n\n// IsLeaf checks if the node is a leaf node (has no children).\nfunc (node *Node) IsLeaf() bool {\n\treturn node.height == 0\n}\n\n// Key returns the key of the node.\nfunc (node *Node) Key() string {\n\treturn node.key\n}\n\n// Value returns the value of the node.\nfunc (node *Node) Value() interface{} {\n\treturn node.value\n}\n\n// _copy creates a copy of the node (excluding value).\nfunc (node *Node) _copy() *Node {\n\tif node.height == 0 {\n\t\tpanic(\"Why are you copying a value node?\")\n\t}\n\treturn \u0026Node{\n\t\tkey: node.key,\n\t\theight: node.height,\n\t\tsize: node.size,\n\t\tleftNode: node.leftNode,\n\t\trightNode: node.rightNode,\n\t}\n}\n\n// Has checks if a node with the given key exists in the subtree rooted at the node.\nfunc (node *Node) Has(key string) (has bool) {\n\tif node == nil {\n\t\treturn false\n\t}\n\tif node.key == key {\n\t\treturn true\n\t}\n\tif node.height == 0 {\n\t\treturn false\n\t}\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Has(key)\n\t}\n\treturn node.getRightNode().Has(key)\n}\n\n// Get searches for a node with the given key in the subtree rooted at the node\n// and returns its index, value, and whether it exists.\nfunc (node *Node) Get(key string) (index int, value interface{}, exists bool) {\n\tif node == nil {\n\t\treturn 0, nil, false\n\t}\n\n\tif node.height == 0 {\n\t\tif node.key == key {\n\t\t\treturn 0, node.value, true\n\t\t}\n\t\tif node.key \u003c key {\n\t\t\treturn 1, nil, false\n\t\t}\n\t\treturn 0, nil, false\n\t}\n\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Get(key)\n\t}\n\n\trightNode := node.getRightNode()\n\tindex, value, exists = rightNode.Get(key)\n\tindex += node.size - rightNode.size\n\treturn index, value, exists\n}\n\n// GetByIndex retrieves the key-value pair of the node at the given index\n// in the subtree rooted at the node.\nfunc (node *Node) GetByIndex(index int) (key string, value interface{}) {\n\tif node.height == 0 {\n\t\tif index == 0 {\n\t\t\treturn node.key, node.value\n\t\t}\n\t\tpanic(\"GetByIndex asked for invalid index\")\n\t}\n\t// TODO: could improve this by storing the sizes\n\tleftNode := node.getLeftNode()\n\tif index \u003c leftNode.size {\n\t\treturn leftNode.GetByIndex(index)\n\t}\n\treturn node.getRightNode().GetByIndex(index - leftNode.size)\n}\n\n// Set inserts a new node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\n//\n// XXX consider a better way to do this... perhaps split Node from Node.\nfunc (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) {\n\tif node == nil {\n\t\treturn NewNode(key, value), false\n\t}\n\n\tif node.height == 0 {\n\t\treturn node.setLeaf(key, value)\n\t}\n\n\tnode = node._copy()\n\tif key \u003c node.key {\n\t\tnode.leftNode, updated = node.getLeftNode().Set(key, value)\n\t} else {\n\t\tnode.rightNode, updated = node.getRightNode().Set(key, value)\n\t}\n\n\tif updated {\n\t\treturn node, updated\n\t}\n\n\tnode.calcHeightAndSize()\n\treturn node.balance(), updated\n}\n\n// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\nfunc (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) {\n\tif key == node.key {\n\t\treturn NewNode(key, value), true\n\t}\n\n\tif key \u003c node.key {\n\t\treturn \u0026Node{\n\t\t\tkey: node.key,\n\t\t\theight: 1,\n\t\t\tsize: 2,\n\t\t\tleftNode: NewNode(key, value),\n\t\t\trightNode: node,\n\t\t}, false\n\t}\n\n\treturn \u0026Node{\n\t\tkey: key,\n\t\theight: 1,\n\t\tsize: 2,\n\t\tleftNode: node,\n\t\trightNode: NewNode(key, value),\n\t}, false\n}\n\n// Remove deletes the node with the given key from the subtree rooted at the node.\n// returns the new root of the subtree, the new leftmost leaf key (if changed),\n// the removed value and the removal was successful.\nfunc (node *Node) Remove(key string) (\n\tnewNode *Node, newKey string, value interface{}, removed bool,\n) {\n\tif node == nil {\n\t\treturn nil, \"\", nil, false\n\t}\n\tif node.height == 0 {\n\t\tif key == node.key {\n\t\t\treturn nil, \"\", node.value, true\n\t\t}\n\t\treturn node, \"\", nil, false\n\t}\n\tif key \u003c node.key {\n\t\tvar newLeftNode *Node\n\t\tnewLeftNode, newKey, value, removed = node.getLeftNode().Remove(key)\n\t\tif !removed {\n\t\t\treturn node, \"\", value, false\n\t\t}\n\t\tif newLeftNode == nil { // left node held value, was removed\n\t\t\treturn node.rightNode, node.key, value, true\n\t\t}\n\t\tnode = node._copy()\n\t\tnode.leftNode = newLeftNode\n\t\tnode.calcHeightAndSize()\n\t\tnode = node.balance()\n\t\treturn node, newKey, value, true\n\t}\n\n\tvar newRightNode *Node\n\tnewRightNode, newKey, value, removed = node.getRightNode().Remove(key)\n\tif !removed {\n\t\treturn node, \"\", value, false\n\t}\n\tif newRightNode == nil { // right node held value, was removed\n\t\treturn node.leftNode, \"\", value, true\n\t}\n\tnode = node._copy()\n\tnode.rightNode = newRightNode\n\tif newKey != \"\" {\n\t\tnode.key = newKey\n\t}\n\tnode.calcHeightAndSize()\n\tnode = node.balance()\n\treturn node, \"\", value, true\n}\n\n// getLeftNode returns the left child of the node.\nfunc (node *Node) getLeftNode() *Node {\n\treturn node.leftNode\n}\n\n// getRightNode returns the right child of the node.\nfunc (node *Node) getRightNode() *Node {\n\treturn node.rightNode\n}\n\n// rotateRight performs a right rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateRight() *Node {\n\tnode = node._copy()\n\tl := node.getLeftNode()\n\t_l := l._copy()\n\n\t_lrCached := _l.rightNode\n\t_l.rightNode = node\n\tnode.leftNode = _lrCached\n\n\tnode.calcHeightAndSize()\n\t_l.calcHeightAndSize()\n\n\treturn _l\n}\n\n// rotateLeft performs a left rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateLeft() *Node {\n\tnode = node._copy()\n\tr := node.getRightNode()\n\t_r := r._copy()\n\n\t_rlCached := _r.leftNode\n\t_r.leftNode = node\n\tnode.rightNode = _rlCached\n\n\tnode.calcHeightAndSize()\n\t_r.calcHeightAndSize()\n\n\treturn _r\n}\n\n// calcHeightAndSize updates the height and size of the node based on its children.\n// NOTE: mutates height and size\nfunc (node *Node) calcHeightAndSize() {\n\tnode.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1\n\tnode.size = node.getLeftNode().size + node.getRightNode().size\n}\n\n// calcBalance calculates the balance factor of the node.\nfunc (node *Node) calcBalance() int {\n\treturn int(node.getLeftNode().height) - int(node.getRightNode().height)\n}\n\n// balance balances the subtree rooted at the node and returns the new root.\n// NOTE: assumes that node can be modified\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) balance() (newSelf *Node) {\n\tbalance := node.calcBalance()\n\tif balance \u003e= -1 {\n\t\treturn node\n\t}\n\tif balance \u003e 1 {\n\t\tif node.getLeftNode().calcBalance() \u003e= 0 {\n\t\t\t// Left Left Case\n\t\t\treturn node.rotateRight()\n\t\t}\n\t\t// Left Right Case\n\t\tleft := node.getLeftNode()\n\t\tnode.leftNode = left.rotateLeft()\n\t\treturn node.rotateRight()\n\t}\n\n\tif node.getRightNode().calcBalance() \u003c= 0 {\n\t\t// Right Right Case\n\t\treturn node.rotateLeft()\n\t}\n\n\t// Right Left Case\n\tright := node.getRightNode()\n\tnode.rightNode = right.rotateRight()\n\treturn node.rotateLeft()\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) Iterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, true, true, cb)\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, false, true, cb)\n}\n\n// TraverseInRange traverses all nodes, including inner nodes.\n// Start is inclusive and end is exclusive when ascending,\n// Start and end are inclusive when descending.\n// Empty start and empty end denote no start and no end.\n// If leavesOnly is true, only visit leaf nodes.\n// NOTE: To simulate an exclusive reverse traversal,\n// just append 0x00 to start.\nfunc (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\tafterStart := (start == \"\" || start \u003c node.key)\n\tstartOrAfter := (start == \"\" || start \u003c= node.key)\n\tbeforeEnd := false\n\tif ascending {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c end)\n\t} else {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c= end)\n\t}\n\n\t// Run callback per inner/leaf node.\n\tstop := false\n\tif (!node.IsLeaf() \u0026\u0026 !leavesOnly) ||\n\t\t(node.IsLeaf() \u0026\u0026 startOrAfter \u0026\u0026 beforeEnd) {\n\t\tstop = cb(node)\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t}\n\tif node.IsLeaf() {\n\t\treturn stop\n\t}\n\n\tif ascending {\n\t\t// check lower nodes, then higher\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t} else {\n\t\t// check the higher nodes first\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t}\n\n\treturn stop\n}\n\n// TraverseByOffset traverses all nodes, including inner nodes.\n// A limit of math.MaxInt means no limit.\nfunc (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\n\t// fast paths. these happen only if TraverseByOffset is called directly on a leaf.\n\tif limit \u003c= 0 || offset \u003e= node.size {\n\t\treturn false\n\t}\n\tif node.IsLeaf() {\n\t\tif offset \u003e 0 {\n\t\t\treturn false\n\t\t}\n\t\treturn cb(node)\n\t}\n\n\t// go to the actual recursive function.\n\treturn node.traverseByOffset(offset, limit, descending, leavesOnly, cb)\n}\n\n// TraverseByOffset traverses the subtree rooted at the node by offset and limit,\n// in either ascending or descending order, and applies the callback function to each traversed node.\n// If leavesOnly is true, only leaf nodes are visited.\nfunc (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\t// caller guarantees: offset \u003c node.size; limit \u003e 0.\n\tif !leavesOnly {\n\t\tif cb(node) {\n\t\t\treturn true\n\t\t}\n\t}\n\tfirst, second := node.getLeftNode(), node.getRightNode()\n\tif descending {\n\t\tfirst, second = second, first\n\t}\n\tif first.IsLeaf() {\n\t\t// either run or skip, based on offset\n\t\tif offset \u003e 0 {\n\t\t\toffset--\n\t\t} else {\n\t\t\tcb(first)\n\t\t\tlimit--\n\t\t\tif limit \u003c= 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// possible cases:\n\t\t// 1 the offset given skips the first node entirely\n\t\t// 2 the offset skips none or part of the first node, but the limit requires some of the second node.\n\t\t// 3 the offset skips none or part of the first node, and the limit stops our search on the first node.\n\t\tif offset \u003e= first.size {\n\t\t\toffset -= first.size // 1\n\t\t} else {\n\t\t\tif first.traverseByOffset(offset, limit, descending, leavesOnly, cb) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// number of leaves which could actually be called from inside\n\t\t\tdelta := first.size - offset\n\t\t\toffset = 0\n\t\t\tif delta \u003e= limit {\n\t\t\t\treturn true // 3\n\t\t\t}\n\t\t\tlimit -= delta // 2\n\t\t}\n\t}\n\n\t// because of the caller guarantees and the way we handle the first node,\n\t// at this point we know that limit \u003e 0 and there must be some values in\n\t// this second node that we include.\n\n\t// =\u003e if the second node is a leaf, it has to be included.\n\tif second.IsLeaf() {\n\t\treturn cb(second)\n\t}\n\t// =\u003e if it is not a leaf, it will still be enough to recursively call this\n\t// function with the updated offset and limit\n\treturn second.traverseByOffset(offset, limit, descending, leavesOnly, cb)\n}\n\n// Only used in testing...\nfunc (node *Node) lmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getLeftNode().lmd()\n}\n\n// Only used in testing...\nfunc (node *Node) rmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getRightNode().rmd()\n}\n\nfunc maxInt8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n"},{"name":"node_test.gno","body":"package avl\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestTraverseByOffset(t *testing.T) {\n\tconst testStrings = `Alfa\nAlfred\nAlpha\nAlphabet\nBeta\nBeth\nBook\nBrowser`\n\ttt := []struct {\n\t\tname string\n\t\tdesc bool\n\t}{\n\t\t{\"ascending\", false},\n\t\t{\"descending\", true},\n\t}\n\n\tfor _, tt := range tt {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsl := strings.Split(testStrings, \"\\n\")\n\n\t\t\t// sort a first time in the order opposite to how we'll be traversing\n\t\t\t// the tree, to ensure that we are not just iterating through with\n\t\t\t// insertion order.\n\t\t\tsort.Strings(sl)\n\t\t\tif !tt.desc {\n\t\t\t\treverseSlice(sl)\n\t\t\t}\n\n\t\t\tr := NewNode(sl[0], nil)\n\t\t\tfor _, v := range sl[1:] {\n\t\t\t\tr, _ = r.Set(v, nil)\n\t\t\t}\n\n\t\t\t// then sort sl in the order we'll be traversing it, so that we can\n\t\t\t// compare the result with sl.\n\t\t\treverseSlice(sl)\n\n\t\t\tvar result []string\n\t\t\tfor i := 0; i \u003c len(sl); i++ {\n\t\t\t\tr.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif !slicesEqual(sl, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", sl, result)\n\t\t\t}\n\n\t\t\tfor l := 2; l \u003c= len(sl); l++ {\n\t\t\t\t// \"slices\"\n\t\t\t\tfor i := 0; i \u003c= len(sl); i++ {\n\t\t\t\t\tmax := i + l\n\t\t\t\t\tif max \u003e len(sl) {\n\t\t\t\t\t\tmax = len(sl)\n\t\t\t\t\t}\n\t\t\t\t\texp := sl[i:max]\n\t\t\t\t\tactual := []string{}\n\n\t\t\t\t\tr.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool {\n\t\t\t\t\t\tactual = append(actual, tr.Key())\n\t\t\t\t\t\treturn false\n\t\t\t\t\t})\n\t\t\t\t\tif !slicesEqual(exp, actual) {\n\t\t\t\t\t\tt.Errorf(\"want %v got %v\", exp, actual)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\thasKey string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"has key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"has key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"A\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"B\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tresult := tree.Has(tt.hasKey)\n\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tgetKey string\n\t\texpectIdx int\n\t\texpectVal interface{}\n\t\texpectExists bool\n\t}{\n\t\t{\n\t\t\t\"get existing key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (smaller)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"@\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (larger)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t5,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get from empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tidx, val, exists := tree.Get(tt.getKey)\n\n\t\t\tif idx != tt.expectIdx {\n\t\t\t\tt.Errorf(\"Expected index %d, got %d\", tt.expectIdx, idx)\n\t\t\t}\n\n\t\t\tif val != tt.expectVal {\n\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t}\n\n\t\t\tif exists != tt.expectExists {\n\t\t\t\tt.Errorf(\"Expected exists %t, got %t\", tt.expectExists, exists)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tidx int\n\t\texpectKey string\n\t\texpectVal interface{}\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\t\"get by valid index\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t2,\n\t\t\t\"C\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (smallest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t0,\n\t\t\t\"A\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (largest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t4,\n\t\t\t\"E\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (negative)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t-1,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (out of range)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t5,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tif tt.expectPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"Expected a panic but didn't get one\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tkey, val := tree.GetByIndex(tt.idx)\n\n\t\t\tif !tt.expectPanic {\n\t\t\t\tif key != tt.expectKey {\n\t\t\t\t\tt.Errorf(\"Expected key %s, got %s\", tt.expectKey, key)\n\t\t\t\t}\n\n\t\t\t\tif val != tt.expectVal {\n\t\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tremoveKey string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"remove leaf node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"B\",\n\t\t\t[]string{\"A\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with one child\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"A\",\n\t\t\t[]string{\"B\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with two children\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove root node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove non-existent key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree, _, _, _ = tree.Remove(tt.removeKey)\n\n\t\t\tresult := make([]string, 0)\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraverse(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"empty tree\",\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"single node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t[]string{\"A\"},\n\t\t},\n\t\t{\n\t\t\t\"small tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"large tree\",\n\t\t\t[]string{\"H\", \"D\", \"L\", \"B\", \"F\", \"J\", \"N\", \"A\", \"C\", \"E\", \"G\", \"I\", \"K\", \"M\", \"O\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tt.Run(\"iterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"ReverseIterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.ReverseIterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, len(tt.expected))\n\t\t\t\tcopy(expected, tt.expected)\n\t\t\t\tfor i, j := 0, len(expected)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\t\t\texpected[i], expected[j] = expected[j], expected[i]\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"TraverseInRange\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\tstart, end := \"C\", \"M\"\n\t\t\t\ttree.TraverseInRange(start, end, true, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, 0)\n\t\t\t\tfor _, key := range tt.expected {\n\t\t\t\t\tif key \u003e= start \u0026\u0026 key \u003c end {\n\t\t\t\t\t\texpected = append(expected, key)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestRotateWhenHeightDiffers(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation when left subtree is higher\",\n\t\t\t[]string{\"E\", \"C\", \"A\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation when right subtree is higher\",\n\t\t\t[]string{\"A\", \"C\", \"E\", \"D\", \"F\"},\n\t\t\t[]string{\"A\", \"C\", \"D\", \"E\", \"F\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"E\", \"A\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"A\", \"E\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\t// perform rotation or balance\n\t\t\ttree = tree.balance()\n\n\t\t\t// check tree structure\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRotateAndBalance(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation\",\n\t\t\t[]string{\"E\", \"D\", \"C\", \"B\", \"A\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"C\", \"A\", \"E\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"C\", \"E\", \"A\", \"D\", \"B\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree = tree.balance()\n\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc slicesEqual(w1, w2 []string) bool {\n\tif len(w1) != len(w2) {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(w1); i++ {\n\t\tif w1[0] != w2[0] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc maxint8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc reverseSlice(ss []string) {\n\tfor i := 0; i \u003c len(ss)/2; i++ {\n\t\tj := len(ss) - 1 - i\n\t\tss[i], ss[j] = ss[j], ss[i]\n\t}\n}\n"},{"name":"tree.gno","body":"package avl\n\ntype IterCbFn func(key string, value interface{}) bool\n\n//----------------------------------------\n// Tree\n\n// The zero struct can be used as an empty tree.\ntype Tree struct {\n\tnode *Node\n}\n\n// NewTree creates a new empty AVL tree.\nfunc NewTree() *Tree {\n\treturn \u0026Tree{\n\t\tnode: nil,\n\t}\n}\n\n// Size returns the number of key-value pair in the tree.\nfunc (tree *Tree) Size() int {\n\treturn tree.node.Size()\n}\n\n// Has checks whether a key exists in the tree.\n// It returns true if the key exists, otherwise false.\nfunc (tree *Tree) Has(key string) (has bool) {\n\treturn tree.node.Has(key)\n}\n\n// Get retrieves the value associated with the given key.\n// It returns the value and a boolean indicating whether the key exists.\nfunc (tree *Tree) Get(key string) (value interface{}, exists bool) {\n\t_, value, exists = tree.node.Get(key)\n\treturn\n}\n\n// GetByIndex retrieves the key-value pair at the specified index in the tree.\n// It returns the key and value at the given index.\nfunc (tree *Tree) GetByIndex(index int) (key string, value interface{}) {\n\treturn tree.node.GetByIndex(index)\n}\n\n// Set inserts a key-value pair into the tree.\n// If the key already exists, the value will be updated.\n// It returns a boolean indicating whether the key was newly inserted or updated.\nfunc (tree *Tree) Set(key string, value interface{}) (updated bool) {\n\tnewnode, updated := tree.node.Set(key, value)\n\ttree.node = newnode\n\treturn updated\n}\n\n// Remove removes a key-value pair from the tree.\n// It returns the removed value and a boolean indicating whether the key was found and removed.\nfunc (tree *Tree) Remove(key string) (value interface{}, removed bool) {\n\tnewnode, _, value, removed := tree.node.Remove(key)\n\ttree.node = newnode\n\treturn value, removed\n}\n\n// Iterate performs an in-order traversal of the tree within the specified key range.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) Iterate(start, end string, cb IterCbFn) bool {\n\treturn tree.node.TraverseInRange(start, end, true, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool {\n\treturn tree.node.TraverseInRange(start, end, false, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// IterateByOffset performs an in-order traversal of the tree starting from the specified offset.\n// It calls the provided callback function for each key-value pair encountered, up to the specified count.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool {\n\treturn tree.node.TraverseByOffset(offset, count, true, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset.\n// It calls the provided callback function for each key-value pair encountered, up to the specified count.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool {\n\treturn tree.node.TraverseByOffset(offset, count, false, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n"},{"name":"tree_test.gno","body":"package avl\n\nimport \"testing\"\n\nfunc TestNewTree(t *testing.T) {\n\ttree := NewTree()\n\tif tree.node != nil {\n\t\tt.Error(\"Expected tree.node to be nil\")\n\t}\n}\n\nfunc TestTreeSize(t *testing.T) {\n\ttree := NewTree()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tif tree.Size() != 2 {\n\t\tt.Error(\"Expected tree size to be 2\")\n\t}\n}\n\nfunc TestTreeHas(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tif !tree.Has(\"key1\") {\n\t\tt.Error(\"Expected tree to have key1\")\n\t}\n\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected tree to not have key2\")\n\t}\n}\n\nfunc TestTreeGet(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, exists := tree.Get(\"key1\")\n\tif !exists || value != \"value1\" {\n\t\tt.Error(\"Expected Get to return value1 and true\")\n\t}\n\n\t_, exists = tree.Get(\"key2\")\n\tif exists {\n\t\tt.Error(\"Expected Get to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeGetByIndex(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\n\tkey, value := tree.GetByIndex(0)\n\tif key != \"key1\" || value != \"value1\" {\n\t\tt.Error(\"Expected GetByIndex(0) to return key1 and value1\")\n\t}\n\n\tkey, value = tree.GetByIndex(1)\n\tif key != \"key2\" || value != \"value2\" {\n\t\tt.Error(\"Expected GetByIndex(1) to return key2 and value2\")\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected GetByIndex to panic for out-of-range index\")\n\t\t}\n\t}()\n\ttree.GetByIndex(2)\n}\n\nfunc TestTreeRemove(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, removed := tree.Remove(\"key1\")\n\tif !removed || value != \"value1\" || tree.Size() != 0 {\n\t\tt.Error(\"Expected Remove to remove key-value pair\")\n\t}\n\n\t_, removed = tree.Remove(\"key2\")\n\tif removed {\n\t\tt.Error(\"Expected Remove to return false for non-existent key\")\n\t}\n}\n\nfunc TestTreeIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key1\", \"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterate(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key3\", \"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.IterateByOffset(1, 2, func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key3\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n\nfunc TestTreeReverseIterateByOffset(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\ttree.Set(\"key3\", \"value3\")\n\n\tvar keys []string\n\ttree.ReverseIterateByOffset(1, 2, func(key string, value interface{}) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\n\texpectedKeys := []string{\"key2\", \"key1\"}\n\tif !slicesEqual(keys, expectedKeys) {\n\t\tt.Errorf(\"Expected keys %v, got %v\", expectedKeys, keys)\n\t}\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar node *avl.Node\n\nfunc init() {\n\tnode = avl.NewNode(\"key0\", \"value0\")\n\t// node, _ = node.Set(\"key0\", \"value0\")\n}\n\nfunc main() {\n\tvar updated bool\n\tnode, updated = node.Set(\"key1\", \"value1\")\n\t// println(node, updated)\n\tprintln(updated, node.Size())\n}\n\n// Output:\n// false 2\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\",\n// \"ModTime\": \"7\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"627e8e517e7ae5db0f3b753e2a32b607989198b6\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:5\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b28057ab7be6383785c0a5503e8a531bdbc21851\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:7]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"6da365f0d6cacbcdf53cd5a4b125803cddce08c2\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"f216afe7b5a17f4ebdbb98dceccedbc22e237596\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ff1a50d8489090af37a2c7766d659f0d717939b5\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={\n// \"Blank\": {},\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"5\",\n// \"RefCount\": \"2\"\n// },\n// \"Parent\": null,\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"0\",\n// \"File\": \"\",\n// \"Line\": \"0\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Values\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ae86874f9b47fa5e64c30b3e92e9d07f2ec967a4\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"init.1\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"10\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"main\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"15\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// }\n// ]\n// }\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar node *avl.Node\n\nfunc init() {\n\tnode = avl.NewNode(\"key0\", \"value0\")\n\tnode, _ = node.Set(\"key1\", \"value1\")\n}\n\nfunc main() {\n\tvar updated bool\n\tnode, updated = node.Set(\"key2\", \"value2\")\n\t// println(node, updated)\n\tprintln(updated, node.Size())\n}\n\n// Output:\n// false 3\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:14]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"143aebc820da33550f7338723fb1e2eec575b196\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:13]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"2f3adc5d0f2a3fe0331cfa93572a7abdde14c9aa\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"2e733a8e9e74fe14f0a5d10fb0f6728fa53d052d\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:12]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"fe20a19f956511f274dc77854e9e5468387260f4\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:11]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AwAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c89a71bdf045e8bde2059dc9d33839f916e02e5d\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"90fa67f8c47db4b9b2a60425dff08d5a3385100f\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:10]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"83e42caaf53070dd95b5f859053eb51ed900bbda\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={\n// \"Blank\": {},\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"9\",\n// \"RefCount\": \"2\"\n// },\n// \"Parent\": null,\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"0\",\n// \"File\": \"\",\n// \"Line\": \"0\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Values\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"1faa9fa4ba1935121a6d3f0a623772e9d4499b0a\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"init.1\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"10\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// },\n// \"V\": {\n// \"@type\": \"/gno.FuncValue\",\n// \"Closure\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\"\n// },\n// \"FileName\": \"main.gno\",\n// \"IsMethod\": false,\n// \"Name\": \"main\",\n// \"NativeName\": \"\",\n// \"NativePkg\": \"\",\n// \"PkgPath\": \"gno.land/r/test\",\n// \"Source\": {\n// \"@type\": \"/gno.RefNode\",\n// \"BlockNode\": null,\n// \"Location\": {\n// \"Column\": \"1\",\n// \"File\": \"main.gno\",\n// \"Line\": \"15\",\n// \"PkgPath\": \"gno.land/r/test\"\n// }\n// },\n// \"Type\": {\n// \"@type\": \"/gno.FuncType\",\n// \"Params\": [],\n// \"Results\": []\n// }\n// }\n// }\n// ]\n// }\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar tree avl.Tree\n\nfunc init() {\n\ttree.Set(\"key0\", \"value0\")\n\ttree.Set(\"key1\", \"value1\")\n}\n\nfunc main() {\n\tvar updated bool\n\tupdated = tree.Set(\"key2\", \"value2\")\n\tprintln(updated, tree.Size())\n}\n\n// Output:\n// false 3\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:16]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:16\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"db333c89cd6773709e031f1f4e4ed4d3fed66c11\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:16\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:14]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"849a50d6c78d65742752e3c89ad8dd556e2e63cb\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b4fc2fdd2d0fe936c87ed2ace97136cffeed207f\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:13]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"a1160b0060ad752dbfe5fe436f7734bb19136150\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:12]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AwAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"fd95e08763159ac529e26986d652e752e78b6325\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"3ecdcf148fe2f9e97b72a3bedf303b2ba56d4f4b\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:11]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"63126557dba88f8556f7a0ccbbfc1d218ae7a302\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:3]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"d31c7e797793e03ffe0bbcb72f963264f8300d22\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\",\n// \"ModTime\": \"10\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// }\n// }\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"testutils","path":"gno.land/p/demo/testutils","files":[{"name":"access.gno","body":"package testutils\n\n// for testing access. see tests/files/access*.go\n\n// NOTE: non-package variables cannot be overridden, except during init().\nvar (\n\tTestVar1 int\n\ttestVar2 int\n)\n\nfunc init() {\n\tTestVar1 = 123\n\ttestVar2 = 456\n}\n\ntype TestAccessStruct struct {\n\tPublicField string\n\tprivateField string\n}\n\nfunc (tas TestAccessStruct) PublicMethod() string {\n\treturn tas.PublicField + \"/\" + tas.privateField\n}\n\nfunc (tas TestAccessStruct) privateMethod() string {\n\treturn tas.PublicField + \"/\" + tas.privateField\n}\n\nfunc NewTestAccessStruct(pub, priv string) TestAccessStruct {\n\treturn TestAccessStruct{\n\t\tPublicField: pub,\n\t\tprivateField: priv,\n\t}\n}\n\n// see access6.g0 etc.\ntype PrivateInterface interface {\n\tprivateMethod() string\n}\n\nfunc PrintPrivateInterface(pi PrivateInterface) {\n\tprintln(\"testutils.PrintPrivateInterface\", pi.privateMethod())\n}\n"},{"name":"crypto.gno","body":"package testutils\n\nimport \"std\"\n\nfunc TestAddress(name string) std.Address {\n\tif len(name) \u003e std.RawAddressSize {\n\t\tpanic(\"address name cannot be greater than std.AddressSize bytes\")\n\t}\n\taddr := std.RawAddress{}\n\t// TODO: use strings.RepeatString or similar.\n\t// NOTE: I miss python's \"\".Join().\n\tblanks := \"____________________\"\n\tcopy(addr[:], []byte(blanks))\n\tcopy(addr[:], []byte(name))\n\treturn std.Address(std.EncodeBech32(\"g\", addr))\n}\n"},{"name":"misc.gno","body":"package testutils\n\n// For testing std.GetCallerAt().\nfunc WrapCall(fn func()) {\n\tfn()\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"diff","path":"gno.land/p/demo/diff","files":[{"name":"diff.gno","body":"// The diff package implements the Myers diff algorithm to compute the edit distance\n// and generate a minimal edit script between two strings.\n//\n// Edit distance, also known as Levenshtein distance, is a measure of the similarity\n// between two strings. It is defined as the minimum number of single-character edits (insertions,\n// deletions, or substitutions) required to change one string into the other.\npackage diff\n\nimport (\n\t\"strings\"\n)\n\n// EditType represents the type of edit operation in a diff.\ntype EditType uint8\n\nconst (\n\t// EditKeep indicates that a character is unchanged in both strings.\n\tEditKeep EditType = iota\n\n\t// EditInsert indicates that a character was inserted in the new string.\n\tEditInsert\n\n\t// EditDelete indicates that a character was deleted from the old string.\n\tEditDelete\n)\n\n// Edit represent a single edit operation in a diff.\ntype Edit struct {\n\t// Type is the kind of edit operation.\n\tType EditType\n\n\t// Char is the character involved in the edit operation.\n\tChar rune\n}\n\n// MyersDiff computes the difference between two strings using Myers' diff algorithm.\n// It returns a slice of Edit operations that transform the old string into the new string.\n// This implementation finds the shortest edit script (SES) that represents the minimal\n// set of operations to transform one string into the other.\n//\n// The function handles both ASCII and non-ASCII characters correctly.\n//\n// Time complexity: O((N+M)D), where N and M are the lengths of the input strings,\n// and D is the size of the minimum edit script.\n//\n// Space complexity: O((N+M)D)\n//\n// In the worst case, where the strings are completely different, D can be as large as N+M,\n// leading to a time and space complexity of O((N+M)^2). However, for strings with many\n// common substrings, the performance is much better, often closer to O(N+M).\n//\n// Parameters:\n// - old: the original string.\n// - new: the modified string.\n//\n// Returns:\n// - A slice of Edit operations representing the minimum difference between the two strings.\nfunc MyersDiff(old, new string) []Edit {\n\toldRunes, newRunes := []rune(old), []rune(new)\n\tn, m := len(oldRunes), len(newRunes)\n\n\tif n == 0 \u0026\u0026 m == 0 {\n\t\treturn []Edit{}\n\t}\n\n\t// old is empty\n\tif n == 0 {\n\t\tedits := make([]Edit, m)\n\t\tfor i, r := range newRunes {\n\t\t\tedits[i] = Edit{Type: EditInsert, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tif m == 0 {\n\t\tedits := make([]Edit, n)\n\t\tfor i, r := range oldRunes {\n\t\t\tedits[i] = Edit{Type: EditDelete, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tmax := n + m\n\tv := make([]int, 2*max+1)\n\tvar trace [][]int\nsearch:\n\tfor d := 0; d \u003c= max; d++ {\n\t\t// iterate through diagonals\n\t\tfor k := -d; k \u003c= d; k += 2 {\n\t\t\tvar x int\n\t\t\tif k == -d || (k != d \u0026\u0026 v[max+k-1] \u003c v[max+k+1]) {\n\t\t\t\tx = v[max+k+1] // move down\n\t\t\t} else {\n\t\t\t\tx = v[max+k-1] + 1 // move right\n\t\t\t}\n\t\t\ty := x - k\n\n\t\t\t// extend the path as far as possible with matching characters\n\t\t\tfor x \u003c n \u0026\u0026 y \u003c m \u0026\u0026 oldRunes[x] == newRunes[y] {\n\t\t\t\tx++\n\t\t\t\ty++\n\t\t\t}\n\n\t\t\tv[max+k] = x\n\n\t\t\t// check if we've reached the end of both strings\n\t\t\tif x == n \u0026\u0026 y == m {\n\t\t\t\ttrace = append(trace, append([]int(nil), v...))\n\t\t\t\tbreak search\n\t\t\t}\n\t\t}\n\t\ttrace = append(trace, append([]int(nil), v...))\n\t}\n\n\t// backtrack to construct the edit script\n\tedits := make([]Edit, 0, n+m)\n\tx, y := n, m\n\tfor d := len(trace) - 1; d \u003e= 0; d-- {\n\t\tvPrev := trace[d]\n\t\tk := x - y\n\t\tvar prevK int\n\t\tif k == -d || (k != d \u0026\u0026 vPrev[max+k-1] \u003c vPrev[max+k+1]) {\n\t\t\tprevK = k + 1\n\t\t} else {\n\t\t\tprevK = k - 1\n\t\t}\n\t\tprevX := vPrev[max+prevK]\n\t\tprevY := prevX - prevK\n\n\t\t// add keep edits for matching characters\n\t\tfor x \u003e prevX \u0026\u0026 y \u003e prevY {\n\t\t\tif x \u003e 0 \u0026\u0026 y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditKeep, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t\ty--\n\t\t}\n\t\tif y \u003e prevY {\n\t\t\tif y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditInsert, Char: newRunes[y-1]}}, edits...)\n\t\t\t}\n\t\t\ty--\n\t\t} else if x \u003e prevX {\n\t\t\tif x \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditDelete, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t}\n\t}\n\n\treturn edits\n}\n\n// Format converts a slice of Edit operations into a human-readable string representation.\n// It groups consecutive edits of the same type and formats them as follows:\n// - Unchanged characters are left as-is\n// - Inserted characters are wrapped in [+...]\n// - Deleted characters are wrapped in [-...]\n//\n// This function is useful for visualizing the differences between two strings\n// in a compact and intuitive format.\n//\n// Parameters:\n// - edits: A slice of Edit operations, typically produced by MyersDiff\n//\n// Returns:\n// - A formatted string representing the diff\n//\n// Example output:\n//\n//\tFor the diff between \"abcd\" and \"acbd\", the output might be:\n//\t\"a[-b]c[+b]d\"\n//\n// Note:\n//\n//\tThe function assumes that the input slice of edits is in the correct order.\n//\tAn empty input slice will result in an empty string.\nfunc Format(edits []Edit) string {\n\tif len(edits) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar (\n\t\tresult strings.Builder\n\t\tcurrentType EditType\n\t\tcurrentChars strings.Builder\n\t)\n\n\tflushCurrent := func() {\n\t\tif currentChars.Len() \u003e 0 {\n\t\t\tswitch currentType {\n\t\t\tcase EditKeep:\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\tcase EditInsert:\n\t\t\t\tresult.WriteString(\"[+\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\tcase EditDelete:\n\t\t\t\tresult.WriteString(\"[-\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\t}\n\t\t\tcurrentChars.Reset()\n\t\t}\n\t}\n\n\tfor _, edit := range edits {\n\t\tif edit.Type != currentType {\n\t\t\tflushCurrent()\n\t\t\tcurrentType = edit.Type\n\t\t}\n\t\tcurrentChars.WriteRune(edit.Char)\n\t}\n\tflushCurrent()\n\n\treturn result.String()\n}\n"},{"name":"diff_test.gno","body":"package diff\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestMyersDiff(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\told string\n\t\tnew string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"No difference\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"abc\",\n\t\t},\n\t\t{\n\t\t\tname: \"Simple insertion\",\n\t\t\told: \"ac\",\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"a[+b]c\",\n\t\t},\n\t\t{\n\t\t\tname: \"Simple deletion\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"ac\",\n\t\t\texpected: \"a[-b]c\",\n\t\t},\n\t\t{\n\t\t\tname: \"Simple substitution\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"abd\",\n\t\t\texpected: \"ab[-c][+d]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple changes\",\n\t\t\told: \"The quick brown fox jumps over the lazy dog\",\n\t\t\tnew: \"The quick brown cat jumps over the lazy dog\",\n\t\t\texpected: \"The quick brown [-fox][+cat] jumps over the lazy dog\",\n\t\t},\n\t\t{\n\t\t\tname: \"Prefix and suffix\",\n\t\t\told: \"Hello, world!\",\n\t\t\tnew: \"Hello, beautiful world!\",\n\t\t\texpected: \"Hello, [+beautiful ]world!\",\n\t\t},\n\t\t{\n\t\t\tname: \"Complete change\",\n\t\t\told: \"abcdef\",\n\t\t\tnew: \"ghijkl\",\n\t\t\texpected: \"[-abcdef][+ghijkl]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Empty strings\",\n\t\t\told: \"\",\n\t\t\tnew: \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Old empty\",\n\t\t\told: \"\",\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"[+abc]\",\n\t\t},\n\t\t{\n\t\t\tname: \"New empty\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"\",\n\t\t\texpected: \"[-abc]\",\n\t\t},\n\t\t{\n\t\t\tname: \"non-ascii (Korean characters)\",\n\t\t\told: \"ASCII 문자가 아닌 것도 되나?\",\n\t\t\tnew: \"ASCII 문자가 아닌 것도 됨.\",\n\t\t\texpected: \"ASCII 문자가 아닌 것도 [-되나?][+됨.]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Emoji diff\",\n\t\t\told: \"Hello 👋 World 🌍\",\n\t\t\tnew: \"Hello 👋 Beautiful 🌸 World 🌍\",\n\t\t\texpected: \"Hello 👋 [+Beautiful 🌸 ]World 🌍\",\n\t\t},\n\t\t{\n\t\t\tname: \"Mixed multibyte and ASCII\",\n\t\t\told: \"こんにちは World\",\n\t\t\tnew: \"こんばんは World\",\n\t\t\texpected: \"こん[-にち][+ばん]は World\",\n\t\t},\n\t\t{\n\t\t\tname: \"Chinese characters\",\n\t\t\told: \"我喜欢编程\",\n\t\t\tnew: \"我喜欢看书和编程\",\n\t\t\texpected: \"我喜欢[+看书和]编程\",\n\t\t},\n\t\t{\n\t\t\tname: \"Combining characters\",\n\t\t\told: \"e\\u0301\", // é (e + ´)\n\t\t\tnew: \"e\\u0300\", // è (e + `)\n\t\t\texpected: \"e[-\\u0301][+\\u0300]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Right-to-Left languages\",\n\t\t\told: \"שלום\",\n\t\t\tnew: \"שלום עולם\",\n\t\t\texpected: \"שלום[+ עולם]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Normalization NFC and NFD\",\n\t\t\told: \"e\\u0301\", // NFD (decomposed)\n\t\t\tnew: \"\\u00e9\", // NFC (precomposed)\n\t\t\texpected: \"[-e\\u0301][+\\u00e9]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Case sensitivity\",\n\t\t\told: \"abc\",\n\t\t\tnew: \"Abc\",\n\t\t\texpected: \"[-a][+A]bc\",\n\t\t},\n\t\t{\n\t\t\tname: \"Surrogate pairs\",\n\t\t\told: \"Hello 🌍\",\n\t\t\tnew: \"Hello 🌎\",\n\t\t\texpected: \"Hello [-🌍][+🌎]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Control characters\",\n\t\t\told: \"Line1\\nLine2\",\n\t\t\tnew: \"Line1\\r\\nLine2\",\n\t\t\texpected: \"Line1[+\\r]\\nLine2\",\n\t\t},\n\t\t{\n\t\t\tname: \"Mixed scripts\",\n\t\t\told: \"Hello नमस्ते こんにちは\",\n\t\t\tnew: \"Hello สวัสดี こんにちは\",\n\t\t\texpected: \"Hello [-नमस्ते][+สวัสดี] こんにちは\",\n\t\t},\n\t\t{\n\t\t\tname: \"Unicode normalization\",\n\t\t\told: \"é\", // U+00E9 (precomposed)\n\t\t\tnew: \"e\\u0301\", // U+0065 U+0301 (decomposed)\n\t\t\texpected: \"[-é][+e\\u0301]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Directional marks\",\n\t\t\told: \"Hello\\u200Eworld\", // LTR mark\n\t\t\tnew: \"Hello\\u200Fworld\", // RTL mark\n\t\t\texpected: \"Hello[-\\u200E][+\\u200F]world\",\n\t\t},\n\t\t{\n\t\t\tname: \"Zero-width characters\",\n\t\t\told: \"ab\\u200Bc\", // Zero-width space\n\t\t\tnew: \"abc\",\n\t\t\texpected: \"ab[-\\u200B]c\",\n\t\t},\n\t\t{\n\t\t\tname: \"Worst-case scenario (completely different strings)\",\n\t\t\told: strings.Repeat(\"a\", 1000),\n\t\t\tnew: strings.Repeat(\"b\", 1000),\n\t\t\texpected: \"[-\" + strings.Repeat(\"a\", 1000) + \"][+\" + strings.Repeat(\"b\", 1000) + \"]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Very long strings\",\n\t\t\told: strings.Repeat(\"a\", 10000) + \"b\" + strings.Repeat(\"a\", 10000),\n\t\t\tnew: strings.Repeat(\"a\", 10000) + \"c\" + strings.Repeat(\"a\", 10000),\n\t\t\texpected: strings.Repeat(\"a\", 10000) + \"[-b][+c]\" + strings.Repeat(\"a\", 10000),\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdiff := MyersDiff(tc.old, tc.new)\n\t\t\tresult := Format(diff)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected: %s, got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"uassert","path":"gno.land/p/demo/uassert","files":[{"name":"doc.gno","body":"package uassert // import \"gno.land/p/demo/uassert\"\n"},{"name":"helpers.gno","body":"package uassert\n\nimport \"strings\"\n\nfunc fail(t TestingT, customMsgs []string, failureMessage string, args ...interface{}) bool {\n\tcustomMsg := \"\"\n\tif len(customMsgs) \u003e 0 {\n\t\tcustomMsg = strings.Join(customMsgs, \" \")\n\t}\n\tif customMsg != \"\" {\n\t\tfailureMessage += \" - \" + customMsg\n\t}\n\tt.Errorf(failureMessage, args...)\n\treturn false\n}\n\nfunc autofail(t TestingT, success bool, customMsgs []string, failureMessage string, args ...interface{}) bool {\n\tif success {\n\t\treturn true\n\t}\n\treturn fail(t, customMsgs, failureMessage, args...)\n}\n\nfunc checkDidPanic(f func()) (didPanic bool, message string) {\n\tdidPanic = true\n\tdefer func() {\n\t\tr := recover()\n\n\t\tif r == nil {\n\t\t\tmessage = \"nil\"\n\t\t\treturn\n\t\t}\n\n\t\terr, ok := r.(error)\n\t\tif ok {\n\t\t\tmessage = err.Error()\n\t\t\treturn\n\t\t}\n\n\t\terrStr, ok := r.(string)\n\t\tif ok {\n\t\t\tmessage = errStr\n\t\t\treturn\n\t\t}\n\n\t\tmessage = \"recover: unsupported type\"\n\t}()\n\tf()\n\tdidPanic = false\n\treturn\n}\n"},{"name":"mock_test.gno","body":"package uassert\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype mockTestingT struct {\n\tfmt string\n\targs []interface{}\n}\n\n// --- interface mock\n\nvar _ TestingT = (*mockTestingT)(nil)\n\nfunc (mockT *mockTestingT) Helper() { /* noop */ }\nfunc (mockT *mockTestingT) Skip(args ...interface{}) { /* not implmented */ }\nfunc (mockT *mockTestingT) Fail() { /* not implmented */ }\nfunc (mockT *mockTestingT) FailNow() { /* not implmented */ }\nfunc (mockT *mockTestingT) Logf(fmt string, args ...interface{}) { /* noop */ }\n\nfunc (mockT *mockTestingT) Fatalf(fmt string, args ...interface{}) {\n\tmockT.fmt = \"fatal: \" + fmt\n\tmockT.args = args\n}\n\nfunc (mockT *mockTestingT) Errorf(fmt string, args ...interface{}) {\n\tmockT.fmt = \"error: \" + fmt\n\tmockT.args = args\n}\n\n// --- helpers\n\nfunc (mockT *mockTestingT) actualString() string {\n\tres := fmt.Sprintf(mockT.fmt, mockT.args...)\n\tmockT.reset()\n\treturn res\n}\n\nfunc (mockT *mockTestingT) reset() {\n\tmockT.fmt = \"\"\n\tmockT.args = nil\n}\n\nfunc (mockT *mockTestingT) equals(t *testing.T, expected string) {\n\tactual := mockT.actualString()\n\n\tif expected != actual {\n\t\tt.Errorf(\"mockT differs:\\n- expected: %s\\n- actual: %s\\n\", expected, actual)\n\t}\n}\n\nfunc (mockT *mockTestingT) empty(t *testing.T) {\n\tif mockT.fmt != \"\" || mockT.args != nil {\n\t\tactual := mockT.actualString()\n\t\tt.Errorf(\"mockT should be empty, got %s\", actual)\n\t}\n}\n"},{"name":"types.gno","body":"package uassert\n\ntype TestingT interface {\n\tHelper()\n\tSkip(args ...interface{})\n\tFatalf(fmt string, args ...interface{})\n\tErrorf(fmt string, args ...interface{})\n\tLogf(fmt string, args ...interface{})\n\tFail()\n\tFailNow()\n}\n"},{"name":"uassert.gno","body":"// uassert is an adapted lighter version of https://github.com/stretchr/testify/assert.\npackage uassert\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/diff\"\n)\n\n// NoError asserts that a function returned no error (i.e. `nil`).\nfunc NoError(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err != nil {\n\t\treturn fail(t, msgs, \"unexpected error: %s\", err.Error())\n\t}\n\treturn true\n}\n\n// Error asserts that a function returned an error (i.e. not `nil`).\nfunc Error(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err == nil {\n\t\treturn fail(t, msgs, \"an error is expected but got nil\")\n\t}\n\treturn true\n}\n\n// ErrorContains asserts that a function returned an error (i.e. not `nil`)\n// and that the error contains the specified substring.\nfunc ErrorContains(t TestingT, err error, contains string, msgs ...string) bool {\n\tt.Helper()\n\n\tif !Error(t, err, msgs...) {\n\t\treturn false\n\t}\n\n\tactual := err.Error()\n\tif !strings.Contains(actual, contains) {\n\t\treturn fail(t, msgs, \"error %q does not contain %q\", actual, contains)\n\t}\n\n\treturn true\n}\n\n// True asserts that the specified value is true.\nfunc True(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif !value {\n\t\treturn fail(t, msgs, \"should be true\")\n\t}\n\treturn true\n}\n\n// False asserts that the specified value is false.\nfunc False(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif value {\n\t\treturn fail(t, msgs, \"should be false\")\n\t}\n\treturn true\n}\n\n// ErrorIs asserts the given error matches the target error\nfunc ErrorIs(t TestingT, err, target error, msgs ...string) bool {\n\tt.Helper()\n\n\tif err == nil || target == nil {\n\t\treturn err == target\n\t}\n\n\t// XXX: if errors.Is(err, target) return true\n\n\tif err.Error() != target.Error() {\n\t\treturn fail(t, msgs, \"error mismatch, expected %s, got %s\", target.Error(), err.Error())\n\t}\n\n\treturn true\n}\n\n// PanicsWithMessage asserts that the code inside the specified func panics,\n// and that the recovered panic value satisfies the given message\nfunc PanicsWithMessage(t TestingT, msg string, f func(), msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\n\tif !didPanic {\n\t\treturn fail(t, msgs, \"func should panic\\n\\tPanic value:\\t%v\", panicValue)\n\t}\n\n\tif panicValue != msg {\n\t\treturn fail(t, msgs, \"func should panic with message:\\t%s\\n\\tPanic value:\\t%s\", msg, panicValue)\n\t}\n\treturn true\n}\n\n// NotPanics asserts that the code inside the specified func does NOT panic.\nfunc NotPanics(t TestingT, f func(), msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\n\n\tif didPanic {\n\t\treturn fail(t, msgs, \"func should not panic\\n\\tPanic value:\\t%s\", panicValue)\n\t}\n\treturn true\n}\n\n// Equal asserts that two objects are equal.\nfunc Equal(t TestingT, expected, actual interface{}, msgs ...string) bool {\n\tt.Helper()\n\n\tif expected == nil || actual == nil {\n\t\treturn expected == actual\n\t}\n\n\t// XXX: errors\n\t// XXX: slices\n\t// XXX: pointers\n\n\tequal := false\n\tok_ := false\n\tes, as := \"unsupported type\", \"unsupported type\"\n\n\tswitch ev := expected.(type) {\n\tcase string:\n\t\tif av, ok := actual.(string); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = ev, av\n\t\t\tif !equal {\n\t\t\t\tdif := diff.MyersDiff(ev, av)\n\t\t\t\treturn fail(t, msgs, \"uassert.Equal: strings are different\\n\\tDiff: %s\", diff.Format(dif))\n\t\t\t}\n\t\t}\n\tcase std.Address:\n\t\tif av, ok := actual.(std.Address); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = string(ev), string(av)\n\t\t}\n\tcase int:\n\t\tif av, ok := actual.(int); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(ev), strconv.Itoa(av)\n\t\t}\n\tcase int8:\n\t\tif av, ok := actual.(int8); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int16:\n\t\tif av, ok := actual.(int16); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int32:\n\t\tif av, ok := actual.(int32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int64:\n\t\tif av, ok := actual.(int64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase uint:\n\t\tif av, ok := actual.(uint); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint8:\n\t\tif av, ok := actual.(uint8); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint16:\n\t\tif av, ok := actual.(uint16); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint32:\n\t\tif av, ok := actual.(uint32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint64:\n\t\tif av, ok := actual.(uint64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)\n\t\t}\n\tcase bool:\n\t\tif av, ok := actual.(bool); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tif ev {\n\t\t\t\tes, as = \"true\", \"false\"\n\t\t\t} else {\n\t\t\t\tes, as = \"false\", \"true\"\n\t\t\t}\n\t\t}\n\tcase float32:\n\t\tif av, ok := actual.(float32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t}\n\tcase float64:\n\t\tif av, ok := actual.(float64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t}\n\tdefault:\n\t\treturn fail(t, msgs, \"uassert.Equal: unsupported type\")\n\t}\n\n\t/*\n\t\t// XXX: implement stringer and other well known similar interfaces\n\t\ttype stringer interface{ String() string }\n\t\tif ev, ok := expected.(stringer); ok {\n\t\t\tif av, ok := actual.(stringer); ok {\n\t\t\t\tequal = ev.String() == av.String()\n\t\t\t\tok_ = true\n\t\t\t}\n\t\t}\n\t*/\n\n\tif !ok_ {\n\t\treturn fail(t, msgs, \"uassert.Equal: different types\") // XXX: display the types\n\t}\n\tif !equal {\n\t\treturn fail(t, msgs, \"uassert.Equal: same type but different value\\n\\texpected: %s\\n\\tactual: %s\", es, as)\n\t}\n\n\treturn true\n}\n\n// NotEqual asserts that two objects are not equal.\nfunc NotEqual(t TestingT, expected, actual interface{}, msgs ...string) bool {\n\tt.Helper()\n\n\tif expected == nil || actual == nil {\n\t\treturn expected != actual\n\t}\n\n\t// XXX: errors\n\t// XXX: slices\n\t// XXX: pointers\n\n\tnotEqual := false\n\tok_ := false\n\tes, as := \"unsupported type\", \"unsupported type\"\n\n\tswitch ev := expected.(type) {\n\tcase string:\n\t\tif av, ok := actual.(string); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = ev, av\n\t\t}\n\tcase std.Address:\n\t\tif av, ok := actual.(std.Address); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = string(ev), string(av)\n\t\t}\n\tcase int:\n\t\tif av, ok := actual.(int); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(ev), strconv.Itoa(av)\n\t\t}\n\tcase int8:\n\t\tif av, ok := actual.(int8); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int16:\n\t\tif av, ok := actual.(int16); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int32:\n\t\tif av, ok := actual.(int32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int64:\n\t\tif av, ok := actual.(int64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase uint:\n\t\tif av, ok := actual.(uint); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint8:\n\t\tif av, ok := actual.(uint8); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint16:\n\t\tif av, ok := actual.(uint16); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint32:\n\t\tif av, ok := actual.(uint32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint64:\n\t\tif av, ok := actual.(uint64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)\n\t\t}\n\tcase bool:\n\t\tif av, ok := actual.(bool); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tif ev {\n\t\t\t\tes, as = \"true\", \"false\"\n\t\t\t} else {\n\t\t\t\tes, as = \"false\", \"true\"\n\t\t\t}\n\t\t}\n\tcase float32:\n\t\tif av, ok := actual.(float32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t}\n\tcase float64:\n\t\tif av, ok := actual.(float64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t}\n\tdefault:\n\t\treturn fail(t, msgs, \"uassert.NotEqual: unsupported type\")\n\t}\n\n\t/*\n\t\t// XXX: implement stringer and other well known similar interfaces\n\t\ttype stringer interface{ String() string }\n\t\tif ev, ok := expected.(stringer); ok {\n\t\t\tif av, ok := actual.(stringer); ok {\n\t\t\t\tnotEqual = ev.String() != av.String()\n\t\t\t\tok_ = true\n\t\t\t}\n\t\t}\n\t*/\n\n\tif !ok_ {\n\t\treturn fail(t, msgs, \"uassert.NotEqual: different types\") // XXX: display the types\n\t}\n\tif !notEqual {\n\t\treturn fail(t, msgs, \"uassert.NotEqual: same type and same value\\n\\texpected: %s\\n\\tactual: %s\", es, as)\n\t}\n\n\treturn true\n}\n\nfunc isNumberEmpty(n interface{}) (isNumber, isEmpty bool) {\n\tswitch n := n.(type) {\n\t// NOTE: the cases are split individually, so that n becomes of the\n\t// asserted type; the type of '0' was correctly inferred and converted\n\t// to the corresponding type, int, int8, etc.\n\tcase int:\n\t\treturn true, n == 0\n\tcase int8:\n\t\treturn true, n == 0\n\tcase int16:\n\t\treturn true, n == 0\n\tcase int32:\n\t\treturn true, n == 0\n\tcase int64:\n\t\treturn true, n == 0\n\tcase uint:\n\t\treturn true, n == 0\n\tcase uint8:\n\t\treturn true, n == 0\n\tcase uint16:\n\t\treturn true, n == 0\n\tcase uint32:\n\t\treturn true, n == 0\n\tcase uint64:\n\t\treturn true, n == 0\n\tcase float32:\n\t\treturn true, n == 0\n\tcase float64:\n\t\treturn true, n == 0\n\t}\n\treturn false, false\n}\nfunc Empty(t TestingT, obj interface{}, msgs ...string) bool {\n\tt.Helper()\n\n\tisNumber, isEmpty := isNumberEmpty(obj)\n\tif isNumber {\n\t\tif !isEmpty {\n\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty number: %d\", obj)\n\t\t}\n\t} else {\n\t\tswitch val := obj.(type) {\n\t\tcase string:\n\t\t\tif val != \"\" {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty string: %s\", val)\n\t\t\t}\n\t\tcase std.Address:\n\t\t\tvar zeroAddr std.Address\n\t\t\tif val != zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty std.Address: %s\", string(val))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fail(t, msgs, \"uassert.Empty: unsupported type\")\n\t\t}\n\t}\n\treturn true\n}\n\nfunc NotEmpty(t TestingT, obj interface{}, msgs ...string) bool {\n\tt.Helper()\n\tisNumber, isEmpty := isNumberEmpty(obj)\n\tif isNumber {\n\t\tif isEmpty {\n\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty number: %d\", obj)\n\t\t}\n\t} else {\n\t\tswitch val := obj.(type) {\n\t\tcase string:\n\t\t\tif val == \"\" {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty string: %s\", val)\n\t\t\t}\n\t\tcase std.Address:\n\t\t\tvar zeroAddr std.Address\n\t\t\tif val == zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty std.Address: %s\", string(val))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: unsupported type\")\n\t\t}\n\t}\n\treturn true\n}\n"},{"name":"uassert_test.gno","body":"package uassert\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"std\"\n\t\"testing\"\n)\n\nvar _ TestingT = (*testing.T)(nil)\n\nfunc TestMock(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tmockT.empty(t)\n\tNoError(mockT, errors.New(\"foo\"))\n\tmockT.equals(t, \"error: unexpected error: foo\")\n\tNoError(mockT, errors.New(\"foo\"), \"custom message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n\tNoError(mockT, errors.New(\"foo\"), \"custom\", \"message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n}\n\nfunc TestNoError(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tTrue(t, NoError(mockT, nil))\n\tmockT.empty(t)\n\tFalse(t, NoError(mockT, errors.New(\"foo bar\")))\n\tmockT.equals(t, \"error: unexpected error: foo bar\")\n}\n\nfunc TestError(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tTrue(t, Error(mockT, errors.New(\"foo bar\")))\n\tmockT.empty(t)\n\tFalse(t, Error(mockT, nil))\n\tmockT.equals(t, \"error: an error is expected but got nil\")\n}\n\nfunc TestErrorContains(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\t// nil error\n\tvar err error\n\tFalse(t, ErrorContains(mockT, err, \"\"), \"ErrorContains should return false for nil arg\")\n}\n\nfunc TestTrue(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !True(mockT, true) {\n\t\tt.Error(\"True should return true\")\n\t}\n\tmockT.empty(t)\n\tif True(mockT, false) {\n\t\tt.Error(\"True should return false\")\n\t}\n\tmockT.equals(t, \"error: should be true\")\n}\n\nfunc TestFalse(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !False(mockT, false) {\n\t\tt.Error(\"False should return true\")\n\t}\n\tmockT.empty(t)\n\tif False(mockT, true) {\n\t\tt.Error(\"False should return false\")\n\t}\n\tmockT.equals(t, \"error: should be false\")\n}\n\nfunc TestPanicsWithMessage(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !PanicsWithMessage(mockT, \"panic\", func() {\n\t\tpanic(errors.New(\"panic\"))\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif PanicsWithMessage(mockT, \"Panic!\", func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic\\n\\tPanic value:\\tnil\")\n\n\tif PanicsWithMessage(mockT, \"at the disco\", func() {\n\t\tpanic(errors.New(\"panic\"))\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message:\\tat the disco\\n\\tPanic value:\\tpanic\")\n\n\tif PanicsWithMessage(mockT, \"Panic!\", func() {\n\t\tpanic(\"panic\")\n\t}) {\n\t\tt.Error(\"PanicsWithMessage should return false\")\n\t}\n\tmockT.equals(t, \"error: func should panic with message:\\tPanic!\\n\\tPanic value:\\tpanic\")\n}\n\nfunc TestNotPanics(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !NotPanics(mockT, func() {\n\t\t// noop\n\t}) {\n\t\tt.Error(\"NotPanics should return true\")\n\t}\n\tmockT.empty(t)\n\n\tif NotPanics(mockT, func() {\n\t\tpanic(\"Panic!\")\n\t}) {\n\t\tt.Error(\"NotPanics should return false\")\n\t}\n}\n\nfunc TestEqual(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\texpected interface{}\n\t\tactual interface{}\n\t\tresult bool\n\t\tremark string\n\t}{\n\t\t// expected to be equal\n\t\t{\"Hello World\", \"Hello World\", true, \"\"},\n\t\t{123, 123, true, \"\"},\n\t\t{123.5, 123.5, true, \"\"},\n\t\t{nil, nil, true, \"\"},\n\t\t{int32(123), int32(123), true, \"\"},\n\t\t{uint64(123), uint64(123), true, \"\"},\n\t\t{std.Address(\"g12345\"), std.Address(\"g12345\"), true, \"\"},\n\t\t// XXX: continue\n\n\t\t// not expected to be equal\n\t\t{\"Hello World\", 42, false, \"\"},\n\t\t{41, 42, false, \"\"},\n\t\t{10, uint(10), false, \"\"},\n\t\t// XXX: continue\n\n\t\t// expected to raise errors\n\t\t// XXX: todo\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"Equal(%v, %v)\", c.expected, c.actual)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := Equal(mockT, c.expected, c.actual)\n\n\t\t\tif res != c.result {\n\t\t\t\tt.Errorf(\"%s should return %v: %s - %s\", name, c.result, c.remark, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotEqual(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\texpected interface{}\n\t\tactual interface{}\n\t\tresult bool\n\t\tremark string\n\t}{\n\t\t// expected to be not equal\n\t\t{\"Hello World\", \"Hello\", true, \"\"},\n\t\t{123, 124, true, \"\"},\n\t\t{123.5, 123.6, true, \"\"},\n\t\t{nil, 123, true, \"\"},\n\t\t{int32(123), int32(124), true, \"\"},\n\t\t{uint64(123), uint64(124), true, \"\"},\n\t\t{std.Address(\"g12345\"), std.Address(\"g67890\"), true, \"\"},\n\t\t// XXX: continue\n\n\t\t// not expected to be not equal\n\t\t{\"Hello World\", \"Hello World\", false, \"\"},\n\t\t{123, 123, false, \"\"},\n\t\t{123.5, 123.5, false, \"\"},\n\t\t{nil, nil, false, \"\"},\n\t\t{int32(123), int32(123), false, \"\"},\n\t\t{uint64(123), uint64(123), false, \"\"},\n\t\t{std.Address(\"g12345\"), std.Address(\"g12345\"), false, \"\"},\n\t\t// XXX: continue\n\n\t\t// expected to raise errors\n\t\t// XXX: todo\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"NotEqual(%v, %v)\", c.expected, c.actual)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := NotEqual(mockT, c.expected, c.actual)\n\n\t\t\tif res != c.result {\n\t\t\t\tt.Errorf(\"%s should return %v: %s - %s\", name, c.result, c.remark, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype myStruct struct {\n\tS string\n\tI int\n}\n\nfunc TestEmpty(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\tobj interface{}\n\t\texpectedEmpty bool\n\t}{\n\t\t// expected to be empty\n\t\t{\"\", true},\n\t\t{0, true},\n\t\t{int(0), true},\n\t\t{int32(0), true},\n\t\t{int64(0), true},\n\t\t{uint(0), true},\n\t\t// XXX: continue\n\n\t\t// not expected to be empty\n\t\t{\"Hello World\", false},\n\t\t{1, false},\n\t\t{int32(1), false},\n\t\t{uint64(1), false},\n\t\t{std.Address(\"g12345\"), false},\n\n\t\t// unsupported\n\t\t{nil, false},\n\t\t{myStruct{}, false},\n\t\t{\u0026myStruct{}, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"Empty(%v)\", c.obj)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := Empty(mockT, c.obj)\n\n\t\t\tif res != c.expectedEmpty {\n\t\t\t\tt.Errorf(\"%s should return %v: %s\", name, c.expectedEmpty, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEqualWithStringDiff(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\texpected string\n\t\tactual string\n\t\tshouldPass bool\n\t\texpectedMsg string\n\t}{\n\t\t{\n\t\t\tname: \"Identical strings\",\n\t\t\texpected: \"Hello, world!\",\n\t\t\tactual: \"Hello, world!\",\n\t\t\tshouldPass: true,\n\t\t\texpectedMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - simple\",\n\t\t\texpected: \"Hello, world!\",\n\t\t\tactual: \"Hello, World!\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: Hello, [-w][+W]orld!\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - complex\",\n\t\t\texpected: \"The quick brown fox jumps over the lazy dog\",\n\t\t\tactual: \"The quick brown cat jumps over the lazy dog\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: The quick brown [-fox][+cat] jumps over the lazy dog\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - prefix\",\n\t\t\texpected: \"prefix_string\",\n\t\t\tactual: \"string\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [-prefix_]string\",\n\t\t},\n\t\t{\n\t\t\tname: \"Different strings - suffix\",\n\t\t\texpected: \"string\",\n\t\t\tactual: \"string_suffix\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: string[+_suffix]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Empty string vs non-empty string\",\n\t\t\texpected: \"\",\n\t\t\tactual: \"non-empty\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [+non-empty]\",\n\t\t},\n\t\t{\n\t\t\tname: \"Non-empty string vs empty string\",\n\t\t\texpected: \"non-empty\",\n\t\t\tactual: \"\",\n\t\t\tshouldPass: false,\n\t\t\texpectedMsg: \"error: uassert.Equal: strings are different\\n\\tDiff: [-non-empty]\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmockT := \u0026mockTestingT{}\n\t\t\tresult := Equal(mockT, tc.expected, tc.actual)\n\n\t\t\tif result != tc.shouldPass {\n\t\t\t\tt.Errorf(\"Expected Equal to return %v, but got %v\", tc.shouldPass, result)\n\t\t\t}\n\n\t\t\tif tc.shouldPass {\n\t\t\t\tmockT.empty(t)\n\t\t\t} else {\n\t\t\t\tmockT.equals(t, tc.expectedMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotEmpty(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tcases := []struct {\n\t\tobj interface{}\n\t\texpectedNotEmpty bool\n\t}{\n\t\t// expected to be empty\n\t\t{\"\", false},\n\t\t{0, false},\n\t\t{int(0), false},\n\t\t{int32(0), false},\n\t\t{int64(0), false},\n\t\t{uint(0), false},\n\t\t{std.Address(\"\"), false},\n\n\t\t// not expected to be empty\n\t\t{\"Hello World\", true},\n\t\t{1, true},\n\t\t{int32(1), true},\n\t\t{uint64(1), true},\n\t\t{std.Address(\"g12345\"), true},\n\n\t\t// unsupported\n\t\t{nil, false},\n\t\t{myStruct{}, false},\n\t\t{\u0026myStruct{}, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tname := fmt.Sprintf(\"NotEmpty(%v)\", c.obj)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tres := NotEmpty(mockT, c.obj)\n\n\t\t\tif res != c.expectedNotEmpty {\n\t\t\t\tt.Errorf(\"%s should return %v: %s\", name, c.expectedNotEmpty, mockT.actualString())\n\t\t\t}\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ufmt","path":"gno.land/p/demo/ufmt","files":[{"name":"ufmt.gno","body":"// Package ufmt provides utility functions for formatting strings, similarly\n// to the Go package \"fmt\", of which only a subset is currently supported\n// (hence the name µfmt - micro fmt).\npackage ufmt\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Println formats using the default formats for its operands and writes to standard output.\n// Println writes the given arguments to standard output with spaces between arguments\n// and a newline at the end.\nfunc Println(args ...interface{}) {\n\tvar strs []string\n\tfor _, arg := range args {\n\t\tswitch v := arg.(type) {\n\t\tcase string:\n\t\t\tstrs = append(strs, v)\n\t\tcase (interface{ String() string }):\n\t\t\tstrs = append(strs, v.String())\n\t\tcase error:\n\t\t\tstrs = append(strs, v.Error())\n\t\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t\tstrs = append(strs, Sprintf(\"%d\", v))\n\t\tcase bool:\n\t\t\tif v {\n\t\t\t\tstrs = append(strs, \"true\")\n\t\t\t} else {\n\t\t\t\tstrs = append(strs, \"false\")\n\t\t\t}\n\t\tcase nil:\n\t\t\tstrs = append(strs, \"\u003cnil\u003e\")\n\t\tdefault:\n\t\t\tstrs = append(strs, \"(unhandled)\")\n\t\t}\n\t}\n\n\t// TODO: remove println after gno supports os.Stdout\n\tprintln(strings.Join(strs, \" \"))\n}\n\n// Sprintf offers similar functionality to Go's fmt.Sprintf, or the sprintf\n// equivalent available in many languages, including C/C++.\n// The number of args passed must exactly match the arguments consumed by the format.\n// A limited number of formatting verbs and features are currently supported,\n// hence the name ufmt (µfmt, micro-fmt).\n//\n// The currently formatted verbs are the following:\n//\n//\t%s: places a string value directly.\n//\t If the value implements the interface interface{ String() string },\n//\t the String() method is called to retrieve the value. Same about Error()\n//\t string.\n//\t%c: formats the character represented by Unicode code point\n//\t%d: formats an integer value using package \"strconv\".\n//\t Currently supports only uint, uint64, int, int64.\n//\t%t: formats a boolean value to \"true\" or \"false\".\n//\t%x: formats an integer value as a hexadecimal string.\n//\t Currently supports only uint8, []uint8, [32]uint8.\n//\t%c: formats a rune value as a string.\n//\t Currently supports only rune, int.\n//\t%q: formats a string value as a quoted string.\n//\t%T: formats the type of the value.\n//\t%%: outputs a literal %. Does not consume an argument.\nfunc Sprintf(format string, args ...interface{}) string {\n\t// we use runes to handle multi-byte characters\n\tsTor := []rune(format)\n\tend := len(sTor)\n\targNum := 0\n\targLen := len(args)\n\tbuf := \"\"\n\n\tfor i := 0; i \u003c end; {\n\t\tisLast := i == end-1\n\t\tc := string(sTor[i])\n\n\t\tif isLast || c != \"%\" {\n\t\t\t// we don't check for invalid format like a one ending with \"%\"\n\t\t\tbuf += string(c)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tverb := string(sTor[i+1])\n\t\tif verb == \"%\" {\n\t\t\tbuf += \"%\"\n\t\t\ti += 2\n\t\t\tcontinue\n\t\t}\n\n\t\tif argNum \u003e argLen {\n\t\t\tpanic(\"invalid number of arguments to ufmt.Sprintf\")\n\t\t}\n\t\targ := args[argNum]\n\t\targNum++\n\n\t\tswitch verb {\n\t\tcase \"s\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase interface{ String() string }:\n\t\t\t\tbuf += v.String()\n\t\t\tcase error:\n\t\t\t\tbuf += v.Error()\n\t\t\tcase string:\n\t\t\t\tbuf += v\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"c\":\n\t\t\tswitch v := arg.(type) {\n\t\t\t// rune is int32. Exclude overflowing numeric types and dups (byte, int32):\n\t\t\tcase rune:\n\t\t\t\tbuf += string(v)\n\t\t\tcase int:\n\t\t\t\tbuf += string(v)\n\t\t\tcase int8:\n\t\t\t\tbuf += string(v)\n\t\t\tcase int16:\n\t\t\t\tbuf += string(v)\n\t\t\tcase uint:\n\t\t\t\tbuf += string(v)\n\t\t\tcase uint8:\n\t\t\t\tbuf += string(v)\n\t\t\tcase uint16:\n\t\t\t\tbuf += string(v)\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"d\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase int:\n\t\t\t\tbuf += strconv.Itoa(v)\n\t\t\tcase int8:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase int16:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase int32:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase int64:\n\t\t\t\tbuf += strconv.Itoa(int(v))\n\t\t\tcase uint:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint8:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint16:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint32:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 10)\n\t\t\tcase uint64:\n\t\t\t\tbuf += strconv.FormatUint(v, 10)\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"t\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase bool:\n\t\t\t\tif v {\n\t\t\t\t\tbuf += \"true\"\n\t\t\t\t} else {\n\t\t\t\t\tbuf += \"false\"\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbuf += fallback(verb, v)\n\t\t\t}\n\t\tcase \"x\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase uint8:\n\t\t\t\tbuf += strconv.FormatUint(uint64(v), 16)\n\t\t\tdefault:\n\t\t\t\tbuf += \"(unhandled)\"\n\t\t\t}\n\t\tcase \"q\":\n\t\t\tswitch v := arg.(type) {\n\t\t\tcase string:\n\t\t\t\tbuf += strconv.Quote(v)\n\t\t\tdefault:\n\t\t\t\tbuf += \"(unhandled)\"\n\t\t\t}\n\t\tcase \"T\":\n\t\t\tswitch arg.(type) {\n\t\t\tcase bool:\n\t\t\t\tbuf += \"bool\"\n\t\t\tcase int:\n\t\t\t\tbuf += \"int\"\n\t\t\tcase int8:\n\t\t\t\tbuf += \"int8\"\n\t\t\tcase int16:\n\t\t\t\tbuf += \"int16\"\n\t\t\tcase int32:\n\t\t\t\tbuf += \"int32\"\n\t\t\tcase int64:\n\t\t\t\tbuf += \"int64\"\n\t\t\tcase uint:\n\t\t\t\tbuf += \"uint\"\n\t\t\tcase uint8:\n\t\t\t\tbuf += \"uint8\"\n\t\t\tcase uint16:\n\t\t\t\tbuf += \"uint16\"\n\t\t\tcase uint32:\n\t\t\t\tbuf += \"uint32\"\n\t\t\tcase uint64:\n\t\t\t\tbuf += \"uint64\"\n\t\t\tcase string:\n\t\t\t\tbuf += \"string\"\n\t\t\tcase []byte:\n\t\t\t\tbuf += \"[]byte\"\n\t\t\tcase []rune:\n\t\t\t\tbuf += \"[]rune\"\n\t\t\tdefault:\n\t\t\t\tbuf += \"unknown\"\n\t\t\t}\n\t\t// % handled before, as it does not consume an argument\n\t\tdefault:\n\t\t\tbuf += \"(unhandled verb: %\" + verb + \")\"\n\t\t}\n\n\t\ti += 2\n\t}\n\tif argNum \u003c argLen {\n\t\tpanic(\"too many arguments to ufmt.Sprintf\")\n\t}\n\treturn buf\n}\n\n// This function is used to mimic Go's fmt.Sprintf\n// specific behaviour of showing verb/type mismatches,\n// where for example:\n//\n//\tfmt.Sprintf(\"%d\", \"foo\") gives \"%!d(string=foo)\"\n//\n// Here:\n//\n//\tfallback(\"s\", 8) -\u003e \"%!s(int=8)\"\n//\tfallback(\"d\", nil) -\u003e \"%!d(\u003cnil\u003e)\", and so on.\nfunc fallback(verb string, arg interface{}) string {\n\tvar s string\n\tswitch v := arg.(type) {\n\tcase string:\n\t\ts = \"string=\" + v\n\tcase (interface{ String() string }):\n\t\ts = \"string=\" + v.String()\n\tcase error:\n\t\t// note: also \"string=\" in Go fmt\n\t\ts = \"string=\" + v.Error()\n\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t// note: rune, byte would be dups, being aliases\n\t\tif typename, e := typeToString(v); e != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t} else {\n\t\t\ts = typename + \"=\" + Sprintf(\"%d\", v)\n\t\t}\n\tcase bool:\n\t\tif v {\n\t\t\ts = \"bool=true\"\n\t\t} else {\n\t\t\ts = \"bool=false\"\n\t\t}\n\tcase nil:\n\t\ts = \"\u003cnil\u003e\"\n\tdefault:\n\t\ts = \"(unhandled)\"\n\t}\n\treturn \"%!\" + verb + \"(\" + s + \")\"\n}\n\n// Get the name of the type of `v` as a string.\n// The recognized type of v is currently limited to native non-composite types.\n// An error is returned otherwise.\nfunc typeToString(v interface{}) (string, error) {\n\tswitch v.(type) {\n\tcase string:\n\t\treturn \"string\", nil\n\tcase int:\n\t\treturn \"int\", nil\n\tcase int8:\n\t\treturn \"int8\", nil\n\tcase int16:\n\t\treturn \"int16\", nil\n\tcase int32:\n\t\treturn \"int32\", nil\n\tcase int64:\n\t\treturn \"int64\", nil\n\tcase uint:\n\t\treturn \"uint\", nil\n\tcase uint8:\n\t\treturn \"uint8\", nil\n\tcase uint16:\n\t\treturn \"uint16\", nil\n\tcase uint32:\n\t\treturn \"uint32\", nil\n\tcase uint64:\n\t\treturn \"uint64\", nil\n\tcase float32:\n\t\treturn \"float32\", nil\n\tcase float64:\n\t\treturn \"float64\", nil\n\tcase bool:\n\t\treturn \"bool\", nil\n\tdefault:\n\t\treturn \"\", errors.New(\"(unsupported type)\")\n\t}\n}\n\n// errMsg implements the error interface.\ntype errMsg struct {\n\tmsg string\n}\n\n// Error defines the requirements of the error interface.\n// It functions similarly to Go's errors.New()\nfunc (e *errMsg) Error() string {\n\treturn e.msg\n}\n\n// Errorf is a function that mirrors the functionality of fmt.Errorf.\n//\n// It takes a format string and arguments to create a formatted string,\n// then sets this string as the 'msg' field of an errMsg struct and returns a pointer to this struct.\n//\n// This function operates in a similar manner to Go's fmt.Errorf,\n// providing a way to create formatted error messages.\n//\n// The currently formatted verbs are the following:\n//\n//\t%s: places a string value directly.\n//\t If the value implements the interface interface{ String() string },\n//\t the String() method is called to retrieve the value. Same for error.\n//\t%c: formats the character represented by Unicode code point\n//\t%d: formats an integer value using package \"strconv\".\n//\t Currently supports only uint, uint64, int, int64.\n//\t%t: formats a boolean value to \"true\" or \"false\".\n//\t%%: outputs a literal %. Does not consume an argument.\nfunc Errorf(format string, args ...interface{}) error {\n\treturn \u0026errMsg{Sprintf(format, args...)}\n}\n"},{"name":"ufmt_test.gno","body":"package ufmt\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype stringer struct{}\n\nfunc (stringer) String() string {\n\treturn \"I'm a stringer\"\n}\n\nfunc TestSprintf(t *testing.T) {\n\ttru := true\n\tcases := []struct {\n\t\tformat string\n\t\tvalues []interface{}\n\t\texpectedOutput string\n\t}{\n\t\t{\"hello %s!\", []interface{}{\"planet\"}, \"hello planet!\"},\n\t\t{\"hi %%%s!\", []interface{}{\"worl%d\"}, \"hi %worl%d!\"},\n\t\t{\"%s %c %d %t\", []interface{}{\"foo\", 'α', 421, true}, \"foo α 421 true\"},\n\t\t{\"string [%s]\", []interface{}{\"foo\"}, \"string [foo]\"},\n\t\t{\"int [%d]\", []interface{}{int(42)}, \"int [42]\"},\n\t\t{\"int8 [%d]\", []interface{}{int8(8)}, \"int8 [8]\"},\n\t\t{\"int16 [%d]\", []interface{}{int16(16)}, \"int16 [16]\"},\n\t\t{\"int32 [%d]\", []interface{}{int32(32)}, \"int32 [32]\"},\n\t\t{\"int64 [%d]\", []interface{}{int64(64)}, \"int64 [64]\"},\n\t\t{\"uint [%d]\", []interface{}{uint(42)}, \"uint [42]\"},\n\t\t{\"uint8 [%d]\", []interface{}{uint8(8)}, \"uint8 [8]\"},\n\t\t{\"uint16 [%d]\", []interface{}{uint16(16)}, \"uint16 [16]\"},\n\t\t{\"uint32 [%d]\", []interface{}{uint32(32)}, \"uint32 [32]\"},\n\t\t{\"uint64 [%d]\", []interface{}{uint64(64)}, \"uint64 [64]\"},\n\t\t{\"bool [%t]\", []interface{}{true}, \"bool [true]\"},\n\t\t{\"bool [%t]\", []interface{}{false}, \"bool [false]\"},\n\t\t{\"no args\", nil, \"no args\"},\n\t\t{\"finish with %\", nil, \"finish with %\"},\n\t\t{\"stringer [%s]\", []interface{}{stringer{}}, \"stringer [I'm a stringer]\"},\n\t\t{\"â\", nil, \"â\"},\n\t\t{\"Hello, World! 😊\", nil, \"Hello, World! 😊\"},\n\t\t{\"unicode formatting: %s\", []interface{}{\"😊\"}, \"unicode formatting: 😊\"},\n\t\t{\"invalid hex [%x]\", []interface{}{\"invalid\"}, \"invalid hex [(unhandled)]\"},\n\t\t{\"rune as character [%c]\", []interface{}{rune('A')}, \"rune as character [A]\"},\n\t\t{\"int as character [%c]\", []interface{}{int('B')}, \"int as character [B]\"},\n\t\t{\"quoted string [%q]\", []interface{}{\"hello\"}, \"quoted string [\\\"hello\\\"]\"},\n\t\t{\"quoted string with escape [%q]\", []interface{}{\"\\thello\\nworld\\\\\"}, \"quoted string with escape [\\\"\\\\thello\\\\nworld\\\\\\\\\\\"]\"},\n\t\t{\"invalid quoted string [%q]\", []interface{}{123}, \"invalid quoted string [(unhandled)]\"},\n\t\t{\"type of bool [%T]\", []interface{}{true}, \"type of bool [bool]\"},\n\t\t{\"type of int [%T]\", []interface{}{123}, \"type of int [int]\"},\n\t\t{\"type of string [%T]\", []interface{}{\"hello\"}, \"type of string [string]\"},\n\t\t{\"type of []byte [%T]\", []interface{}{[]byte{1, 2, 3}}, \"type of []byte [[]byte]\"},\n\t\t{\"type of []rune [%T]\", []interface{}{[]rune{'a', 'b', 'c'}}, \"type of []rune [[]rune]\"},\n\t\t{\"type of unknown [%T]\", []interface{}{struct{}{}}, \"type of unknown [unknown]\"},\n\t\t// mismatch printing\n\t\t{\"%s\", []interface{}{nil}, \"%!s(\u003cnil\u003e)\"},\n\t\t{\"%s\", []interface{}{421}, \"%!s(int=421)\"},\n\t\t{\"%s\", []interface{}{\"z\"}, \"z\"},\n\t\t{\"%s\", []interface{}{tru}, \"%!s(bool=true)\"},\n\t\t{\"%s\", []interface{}{'z'}, \"%!s(int32=122)\"},\n\n\t\t{\"%c\", []interface{}{nil}, \"%!c(\u003cnil\u003e)\"},\n\t\t{\"%c\", []interface{}{421}, \"ƥ\"},\n\t\t{\"%c\", []interface{}{\"z\"}, \"%!c(string=z)\"},\n\t\t{\"%c\", []interface{}{tru}, \"%!c(bool=true)\"},\n\t\t{\"%c\", []interface{}{'z'}, \"z\"},\n\n\t\t{\"%d\", []interface{}{nil}, \"%!d(\u003cnil\u003e)\"},\n\t\t{\"%d\", []interface{}{421}, \"421\"},\n\t\t{\"%d\", []interface{}{\"z\"}, \"%!d(string=z)\"},\n\t\t{\"%d\", []interface{}{tru}, \"%!d(bool=true)\"},\n\t\t{\"%d\", []interface{}{'z'}, \"122\"},\n\n\t\t{\"%t\", []interface{}{nil}, \"%!t(\u003cnil\u003e)\"},\n\t\t{\"%t\", []interface{}{421}, \"%!t(int=421)\"},\n\t\t{\"%t\", []interface{}{\"z\"}, \"%!t(string=z)\"},\n\t\t{\"%t\", []interface{}{tru}, \"true\"},\n\t\t{\"%t\", []interface{}{'z'}, \"%!t(int32=122)\"},\n\t}\n\n\tfor _, tc := range cases {\n\t\tname := fmt.Sprintf(tc.format, tc.values...)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Sprintf(tc.format, tc.values...)\n\t\t\tif got != tc.expectedOutput {\n\t\t\t\tt.Errorf(\"got %q, want %q.\", got, tc.expectedOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestErrorf(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tformat string\n\t\targs []interface{}\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"simple string\",\n\t\t\tformat: \"error: %s\",\n\t\t\targs: []interface{}{\"something went wrong\"},\n\t\t\texpected: \"error: something went wrong\",\n\t\t},\n\t\t{\n\t\t\tname: \"integer value\",\n\t\t\tformat: \"value: %d\",\n\t\t\targs: []interface{}{42},\n\t\t\texpected: \"value: 42\",\n\t\t},\n\t\t{\n\t\t\tname: \"boolean value\",\n\t\t\tformat: \"success: %t\",\n\t\t\targs: []interface{}{true},\n\t\t\texpected: \"success: true\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple values\",\n\t\t\tformat: \"error %d: %s (success=%t)\",\n\t\t\targs: []interface{}{123, \"failure occurred\", false},\n\t\t\texpected: \"error 123: failure occurred (success=false)\",\n\t\t},\n\t\t{\n\t\t\tname: \"literal percent\",\n\t\t\tformat: \"literal %%\",\n\t\t\targs: []interface{}{},\n\t\t\texpected: \"literal %\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := Errorf(tt.format, tt.args...)\n\t\t\tif err.Error() != tt.expected {\n\t\t\t\tt.Errorf(\"Errorf(%q, %v) = %q, expected %q\", tt.format, tt.args, err.Error(), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrintErrors(t *testing.T) {\n\tgot := Sprintf(\"error: %s\", errors.New(\"can I be printed?\"))\n\texpectedOutput := \"error: can I be printed?\"\n\tif got != expectedOutput {\n\t\tt.Errorf(\"got %q, want %q.\", got, expectedOutput)\n\t}\n}\n\n// NOTE: Currently, there is no way to get the output of Println without using os.Stdout,\n// so we can only test that it doesn't panic and print arguments well.\nfunc TestPrintln(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\targs []interface{}\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Empty args\",\n\t\t\targs: []interface{}{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"String args\",\n\t\t\targs: []interface{}{\"Hello\", \"World\"},\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname: \"Integer args\",\n\t\t\targs: []interface{}{1, 2, 3},\n\t\t\texpected: \"1 2 3\",\n\t\t},\n\t\t{\n\t\t\tname: \"Mixed args\",\n\t\t\targs: []interface{}{\"Hello\", 42, true, false, \"World\"},\n\t\t\texpected: \"Hello 42 true false World\",\n\t\t},\n\t\t{\n\t\t\tname: \"Unhandled type\",\n\t\t\targs: []interface{}{\"Hello\", 3.14, []int{1, 2, 3}},\n\t\t\texpected: \"Hello (unhandled) (unhandled)\",\n\t\t},\n\t}\n\n\t// TODO: replace os.Stdout with a buffer to capture the output and test it.\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tPrintln(tt.args...)\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"acl","path":"gno.land/p/demo/acl","files":[{"name":"acl.gno","body":"package acl\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nfunc New() *Directory {\n\treturn \u0026Directory{\n\t\tuserGroups: avl.Tree{},\n\t\tpermBuckets: avl.Tree{},\n\t}\n}\n\ntype Directory struct {\n\tpermBuckets avl.Tree // identifier -\u003e perms\n\tuserGroups avl.Tree // std.Address -\u003e []string\n}\n\nfunc (d *Directory) HasPerm(addr std.Address, verb, resource string) bool {\n\t// FIXME: consider memoize.\n\n\t// user perms\n\tif d.getBucketPerms(\"u:\"+addr.String()).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// everyone's perms.\n\tif d.getBucketPerms(\"g:\"+Everyone).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// user groups' perms.\n\tgroups, ok := d.userGroups.Get(addr.String())\n\tif ok {\n\t\tfor _, group := range groups.([]string) {\n\t\t\tif d.getBucketPerms(\"g:\"+group).hasPerm(verb, resource) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (d *Directory) getBucketPerms(bucket string) perms {\n\tres, ok := d.permBuckets.Get(bucket)\n\tif ok {\n\t\treturn res.(perms)\n\t}\n\treturn perms{}\n}\n\nfunc (d *Directory) HasRole(addr std.Address, role string) bool {\n\treturn d.HasPerm(addr, \"role\", role)\n}\n\nfunc (d *Directory) AddUserPerm(addr std.Address, verb, resource string) {\n\tbucket := \"u:\" + addr.String()\n\tp := perm{\n\t\tverbs: []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) AddGroupPerm(name string, verb, resource string) {\n\tbucket := \"g:\" + name\n\tp := perm{\n\t\tverbs: []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) addPermToBucket(bucket string, p perm) {\n\tvar ps perms\n\n\texisting, ok := d.permBuckets.Get(bucket)\n\tif ok {\n\t\tps = existing.(perms)\n\t}\n\tps = append(ps, p)\n\n\td.permBuckets.Set(bucket, ps)\n}\n\nfunc (d *Directory) AddUserToGroup(user std.Address, group string) {\n\texisting, ok := d.userGroups.Get(user.String())\n\tvar groups []string\n\tif ok {\n\t\tgroups = existing.([]string)\n\t}\n\tgroups = append(groups, group)\n\td.userGroups.Set(user.String(), groups)\n}\n\n// TODO: helpers to remove permissions.\n// TODO: helpers to adds multiple permissions at once -\u003e {verbs: []string{\"read\",\"write\"}}.\n// TODO: helpers to delete users from gorups.\n// TODO: helpers to quickly reset states.\n"},{"name":"acl_test.gno","body":"package acl\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Test(t *testing.T) {\n\tadm := testutils.TestAddress(\"admin\")\n\tmod := testutils.TestAddress(\"mod\")\n\tusr := testutils.TestAddress(\"user\")\n\tcst := testutils.TestAddress(\"custom\")\n\n\tdir := New()\n\n\t// by default, no one has perm.\n\tshouldNotHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldNotHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding all the rights to admin.\n\tdir.AddUserPerm(adm, \".*\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding custom regexp rule for user \"cst\".\n\tdir.AddUserPerm(cst, \"write\", \"r/demo/boards:gnolang/.*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding a group perm for a new group.\n\t// no changes expected.\n\tdir.AddGroupPerm(\"mods\", \"role\", \"moderator\")\n\tdir.AddGroupPerm(\"mods\", \"write\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// assigning the user \"mod\" to the \"mods\" group.\n\tdir.AddUserToGroup(mod, \"mods\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding \"read\" permission for everyone.\n\tdir.AddGroupPerm(Everyone, \"read\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\") // new\n}\n\nfunc shouldHasRole(t *testing.T, dir *Directory, addr std.Address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has role %s\", addr.String(), role))\n}\n\nfunc shouldNotHasRole(t *testing.T, dir *Directory, addr std.Address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has role %s\", addr.String(), role))\n}\n\nfunc shouldHasPerm(t *testing.T, dir *Directory, addr std.Address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has perm for %s - %s\", addr.String(), verb, resource))\n}\n\nfunc shouldNotHasPerm(t *testing.T, dir *Directory, addr std.Address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has perm for %s - %s\", addr.String(), verb, resource))\n}\n"},{"name":"const.gno","body":"package acl\n\nconst Everyone string = \"everyone\"\n"},{"name":"perm.gno","body":"package acl\n\nimport \"regexp\"\n\ntype perm struct {\n\tverbs []string\n\tresources []string\n}\n\nfunc (perm perm) hasPerm(verb, resource string) bool {\n\t// check verb\n\tverbOK := false\n\tfor _, pattern := range perm.verbs {\n\t\tif match(pattern, verb) {\n\t\t\tverbOK = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !verbOK {\n\t\treturn false\n\t}\n\n\t// check resource\n\tfor _, pattern := range perm.resources {\n\t\tif match(pattern, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc match(pattern, target string) bool {\n\tif pattern == \".*\" {\n\t\treturn true\n\t}\n\n\tif pattern == target {\n\t\treturn true\n\t}\n\n\t// regexp handling\n\tmatch, _ := regexp.MatchString(pattern, target)\n\treturn match\n}\n"},{"name":"perms.gno","body":"package acl\n\ntype perms []perm\n\nfunc (perms perms) hasPerm(verb, resource string) bool {\n\tfor _, perm := range perms {\n\t\tif perm.hasPerm(verb, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"urequire","path":"gno.land/p/demo/urequire","files":[{"name":"urequire.gno","body":"// urequire is a sister package for uassert.\n// XXX: codegen the package.\npackage urequire\n\nimport \"gno.land/p/demo/uassert\"\n\n// type TestingT = uassert.TestingT // XXX: bug, should work\n\nfunc NoError(t uassert.TestingT, err error, msgs ...string) {\n\tt.Helper()\n\tif uassert.NoError(t, err, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc Error(t uassert.TestingT, err error, msgs ...string) {\n\tt.Helper()\n\tif uassert.Error(t, err, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc ErrorContains(t uassert.TestingT, err error, contains string, msgs ...string) {\n\tt.Helper()\n\tif uassert.ErrorContains(t, err, contains, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc True(t uassert.TestingT, value bool, msgs ...string) {\n\tt.Helper()\n\tif uassert.True(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc False(t uassert.TestingT, value bool, msgs ...string) {\n\tt.Helper()\n\tif uassert.False(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc ErrorIs(t uassert.TestingT, err, target error, msgs ...string) {\n\tt.Helper()\n\tif uassert.ErrorIs(t, err, target, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc PanicsWithMessage(t uassert.TestingT, msg string, f func(), msgs ...string) {\n\tt.Helper()\n\tif uassert.PanicsWithMessage(t, msg, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotPanics(t uassert.TestingT, f func(), msgs ...string) {\n\tt.Helper()\n\tif uassert.NotPanics(t, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc Equal(t uassert.TestingT, expected, actual interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.Equal(t, expected, actual, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotEqual(t uassert.TestingT, expected, actual interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotEqual(t, expected, actual, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc Empty(t uassert.TestingT, obj interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.Empty(t, obj, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotEmpty(t uassert.TestingT, obj interface{}, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotEmpty(t, obj, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n"},{"name":"urequire_test.gno","body":"package urequire\n\nimport \"testing\"\n\nfunc TestPackage(t *testing.T) {\n\tEqual(t, 42, 42)\n\t// XXX: find a way to unit test this package\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"pager","path":"gno.land/p/demo/avl/pager","files":[{"name":"pager.gno","body":"package pager\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Pager is a struct that holds the AVL tree and pagination parameters.\ntype Pager struct {\n\tTree *avl.Tree\n\tPageQueryParam string\n\tSizeQueryParam string\n\tDefaultPageSize int\n}\n\n// Page represents a single page of results.\ntype Page struct {\n\tItems []Item\n\tPageNumber int\n\tPageSize int\n\tTotalItems int\n\tTotalPages int\n\tHasPrev bool\n\tHasNext bool\n\tPager *Pager // Reference to the parent Pager\n}\n\n// Item represents a key-value pair in the AVL tree.\ntype Item struct {\n\tKey string\n\tValue interface{}\n}\n\n// NewPager creates a new Pager with default values.\nfunc NewPager(tree *avl.Tree, defaultPageSize int) *Pager {\n\treturn \u0026Pager{\n\t\tTree: tree,\n\t\tPageQueryParam: \"page\",\n\t\tSizeQueryParam: \"size\",\n\t\tDefaultPageSize: defaultPageSize,\n\t}\n}\n\n// GetPage retrieves a page of results from the AVL tree.\nfunc (p *Pager) GetPage(pageNumber int) *Page {\n\treturn p.GetPageWithSize(pageNumber, p.DefaultPageSize)\n}\n\nfunc (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {\n\ttotalItems := p.Tree.Size()\n\ttotalPages := int(math.Ceil(float64(totalItems) / float64(pageSize)))\n\n\tpage := \u0026Page{\n\t\tTotalItems: totalItems,\n\t\tTotalPages: totalPages,\n\t\tPageSize: pageSize,\n\t\tPager: p,\n\t}\n\n\t// pages without content\n\tif pageSize \u003c 1 {\n\t\treturn page\n\t}\n\n\t// page number provided is not available\n\tif pageNumber \u003c 1 {\n\t\tpage.HasNext = totalPages \u003e 0\n\t\treturn page\n\t}\n\n\t// page number provided is outside the range of total pages\n\tif pageNumber \u003e totalPages {\n\t\tpage.PageNumber = pageNumber\n\t\tpage.HasPrev = pageNumber \u003e 0\n\t\treturn page\n\t}\n\n\tstartIndex := (pageNumber - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\tif endIndex \u003e totalItems {\n\t\tendIndex = totalItems\n\t}\n\n\titems := []Item{}\n\tp.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool {\n\t\titems = append(items, Item{Key: key, Value: value})\n\t\treturn false\n\t})\n\n\tpage.Items = items\n\tpage.PageNumber = pageNumber\n\tpage.HasPrev = pageNumber \u003e 1\n\tpage.HasNext = pageNumber \u003c totalPages\n\treturn page\n}\n\nfunc (p *Pager) MustGetPageByPath(rawURL string) *Page {\n\tpage, err := p.GetPageByPath(rawURL)\n\tif err != nil {\n\t\tpanic(\"invalid path\")\n\t}\n\treturn page\n}\n\n// GetPageByPath retrieves a page of results based on the query parameters in the URL path.\nfunc (p *Pager) GetPageByPath(rawURL string) (*Page, error) {\n\tpageNumber, pageSize, err := p.ParseQuery(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.GetPageWithSize(pageNumber, pageSize), nil\n}\n\n// UI generates the Markdown UI for the page selector.\nfunc (p *Page) Selector() string {\n\tpageNumber := p.PageNumber\n\tpageNumber = max(pageNumber, 1)\n\n\tif p.TotalPages \u003c= 1 {\n\t\treturn \"\"\n\t}\n\n\tmd := \"\"\n\n\tif p.HasPrev {\n\t\t// Always show the first page link\n\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", 1, p.Pager.PageQueryParam, 1)\n\n\t\t// Before\n\t\tif p.PageNumber \u003e 4 {\n\t\t\tmd += \"… | \"\n\t\t}\n\n\t\tif p.PageNumber \u003e 3 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2)\n\t\t}\n\n\t\tif p.PageNumber \u003e 2 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1)\n\t\t}\n\t}\n\n\tif p.PageNumber \u003e 0 \u0026\u0026 p.PageNumber \u003c= p.TotalPages {\n\t\t// Current page\n\t\tmd += ufmt.Sprintf(\"**%d**\", p.PageNumber)\n\t} else {\n\t\tmd += ufmt.Sprintf(\"_%d_\", p.PageNumber)\n\t}\n\n\tif p.HasNext {\n\t\tmd += \" | \"\n\n\t\tif p.PageNumber \u003c p.TotalPages-1 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-2 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d) | \", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-3 {\n\t\t\tmd += \"… | \"\n\t\t}\n\n\t\t// Always show the last page link\n\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d)\", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages)\n\t}\n\n\treturn md\n}\n\n// ParseQuery parses the URL to extract the page number and page size.\nfunc (p *Pager) ParseQuery(rawURL string) (int, int, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn 1, p.DefaultPageSize, err\n\t}\n\n\tquery := u.Query()\n\tpageNumber := 1\n\tpageSize := p.DefaultPageSize\n\n\tif p.PageQueryParam != \"\" {\n\t\tif pageStr := query.Get(p.PageQueryParam); pageStr != \"\" {\n\t\t\tpageNumber, err = strconv.Atoi(pageStr)\n\t\t\tif err != nil || pageNumber \u003c 1 {\n\t\t\t\tpageNumber = 1\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.SizeQueryParam != \"\" {\n\t\tif sizeStr := query.Get(p.SizeQueryParam); sizeStr != \"\" {\n\t\t\tpageSize, err = strconv.Atoi(sizeStr)\n\t\t\tif err != nil || pageSize \u003c 1 {\n\t\t\t\tpageSize = p.DefaultPageSize\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pageNumber, pageSize, nil\n}\n\nfunc max(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n"},{"name":"pager_test.gno","body":"package pager\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestPager_GetPage(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize int\n\t\texpected []Item\n\t}{\n\t\t{1, 2, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}}},\n\t\t{2, 2, []Item{{Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}}},\n\t\t{3, 2, []Item{{Key: \"e\", Value: 5}}},\n\t\t{1, 3, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}}},\n\t\t{2, 3, []Item{{Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t{1, 5, []Item{{Key: \"a\", Value: 1}, {Key: \"b\", Value: 2}, {Key: \"c\", Value: 3}, {Key: \"d\", Value: 4}, {Key: \"e\", Value: 5}}},\n\t\t{2, 5, []Item{}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tuassert.Equal(t, len(tt.expected), len(page.Items))\n\n\t\tfor i, item := range page.Items {\n\t\t\tuassert.Equal(t, tt.expected[i].Key, item.Key)\n\t\t\tuassert.Equal(t, tt.expected[i].Value, item.Value)\n\t\t}\n\t}\n}\n\nfunc TestPager_GetPageByPath(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 50; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL string\n\t\texpectedPage int\n\t\texpectedSize int\n\t}{\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=1\", 1, 10},\n\t\t{\"/r/foo:bar/baz?size=10\u0026page=2\", 2, 10},\n\t\t{\"/r/foo:bar/baz?page=3\", 3, pager.DefaultPageSize},\n\t\t{\"/r/foo:bar/baz?size=20\", 1, 20},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, err := pager.GetPageByPath(tt.rawURL)\n\t\turequire.NoError(t, err, ufmt.Sprintf(\"GetPageByPath(%s) returned error: %v\", tt.rawURL, err))\n\n\t\tuassert.Equal(t, tt.expectedPage, page.PageNumber)\n\t\tuassert.Equal(t, tt.expectedSize, page.PageSize)\n\t}\n}\n\nfunc TestPage_Selector(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize int\n\t\texpected string\n\t}{\n\t\t{1, 2, \"**1** | [2](?page=2) | [3](?page=3)\"},\n\t\t{2, 2, \"[1](?page=1) | **2** | [3](?page=3)\"},\n\t\t{3, 2, \"[1](?page=1) | [2](?page=2) | **3**\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Selector()\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_UI_WithManyPages(t *testing.T) {\n\t// Create a new AVL tree and populate it with many key-value pairs.\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 100; i++ {\n\t\ttree.Set(ufmt.Sprintf(\"key%d\", i), i)\n\t}\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases for a large number of pages.\n\ttests := []struct {\n\t\tpageNumber int\n\t\tpageSize int\n\t\texpected string\n\t}{\n\t\t// XXX: -1\n\t\t// XXX: 0\n\t\t{1, 10, \"**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)\"},\n\t\t{2, 10, \"[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)\"},\n\t\t{3, 10, \"[1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | … | [10](?page=10)\"},\n\t\t{4, 10, \"[1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) | … | [10](?page=10)\"},\n\t\t{5, 10, \"[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)\"},\n\t\t{6, 10, \"[1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** | [7](?page=7) | [8](?page=8) | … | [10](?page=10)\"},\n\t\t{7, 10, \"[1](?page=1) | … | [5](?page=5) | [6](?page=6) | **7** | [8](?page=8) | [9](?page=9) | [10](?page=10)\"},\n\t\t{8, 10, \"[1](?page=1) | … | [6](?page=6) | [7](?page=7) | **8** | [9](?page=9) | [10](?page=10)\"},\n\t\t{9, 10, \"[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)\"},\n\t\t{10, 10, \"[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**\"},\n\t\t// XXX: 11\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage := pager.GetPageWithSize(tt.pageNumber, tt.pageSize)\n\n\t\tui := page.Selector()\n\t\tuassert.Equal(t, tt.expected, ui)\n\t}\n}\n\nfunc TestPager_ParseQuery(t *testing.T) {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\ttree := avl.NewTree()\n\ttree.Set(\"a\", 1)\n\ttree.Set(\"b\", 2)\n\ttree.Set(\"c\", 3)\n\ttree.Set(\"d\", 4)\n\ttree.Set(\"e\", 5)\n\n\t// Create a new pager.\n\tpager := NewPager(tree, 10)\n\n\t// Define test cases.\n\ttests := []struct {\n\t\trawURL string\n\t\texpectedPage int\n\t\texpectedSize int\n\t\texpectedError bool\n\t}{\n\t\t{\"/r/foo:bar/baz?size=2\u0026page=1\", 1, 2, false},\n\t\t{\"/r/foo:bar/baz?size=3\u0026page=2\", 2, 3, false},\n\t\t{\"/r/foo:bar/baz?size=5\u0026page=3\", 3, 5, false},\n\t\t{\"/r/foo:bar/baz?page=2\", 2, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=3\", 1, 3, false},\n\t\t{\"/r/foo:bar/baz\", 1, pager.DefaultPageSize, false},\n\t\t{\"/r/foo:bar/baz?size=0\u0026page=0\", 1, pager.DefaultPageSize, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpage, size, err := pager.ParseQuery(tt.rawURL)\n\t\tif tt.expectedError {\n\t\t\tuassert.Error(t, err, ufmt.Sprintf(\"ParseQuery(%s) expected error but got none\", tt.rawURL))\n\t\t} else {\n\t\t\turequire.NoError(t, err, ufmt.Sprintf(\"ParseQuery(%s) returned error: %v\", tt.rawURL, err))\n\t\t\tuassert.Equal(t, tt.expectedPage, page, ufmt.Sprintf(\"ParseQuery(%s) returned page %d, expected %d\", tt.rawURL, page, tt.expectedPage))\n\t\t\tuassert.Equal(t, tt.expectedSize, size, ufmt.Sprintf(\"ParseQuery(%s) returned size %d, expected %d\", tt.rawURL, size, tt.expectedSize))\n\t\t}\n\t}\n}\n"},{"name":"z_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n\t// Create a new AVL tree and populate it with some key-value pairs.\n\tvar id seqid.ID\n\ttree := avl.NewTree()\n\tfor i := 0; i \u003c 42; i++ {\n\t\ttree.Set(id.Next().String(), i)\n\t}\n\n\t// Create a new pager.\n\tpager := pager.NewPager(tree, 7)\n\n\tfor pn := -1; pn \u003c 8; pn++ {\n\t\tpage := pager.GetPage(pn)\n\n\t\tprintln(ufmt.Sprintf(\"## Page %d of %d\", page.PageNumber, page.TotalPages))\n\t\tfor idx, item := range page.Items {\n\t\t\tprintln(ufmt.Sprintf(\"- idx=%d key=%s value=%d\", idx, item.Key, item.Value))\n\t\t}\n\t\tprintln(page.Selector())\n\t\tprintln()\n\t}\n}\n\n// Output:\n// ## Page 0 of 6\n// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n//\n// ## Page 0 of 6\n// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6)\n//\n// ## Page 1 of 6\n// - idx=0 key=0000001 value=0\n// - idx=1 key=0000002 value=1\n// - idx=2 key=0000003 value=2\n// - idx=3 key=0000004 value=3\n// - idx=4 key=0000005 value=4\n// - idx=5 key=0000006 value=5\n// - idx=6 key=0000007 value=6\n// **1** | [2](?page=2) | [3](?page=3) | … | [6](?page=6)\n//\n// ## Page 2 of 6\n// - idx=0 key=0000008 value=7\n// - idx=1 key=0000009 value=8\n// - idx=2 key=000000a value=9\n// - idx=3 key=000000b value=10\n// - idx=4 key=000000c value=11\n// - idx=5 key=000000d value=12\n// - idx=6 key=000000e value=13\n// [1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [6](?page=6)\n//\n// ## Page 3 of 6\n// - idx=0 key=000000f value=14\n// - idx=1 key=000000g value=15\n// - idx=2 key=000000h value=16\n// - idx=3 key=000000j value=17\n// - idx=4 key=000000k value=18\n// - idx=5 key=000000m value=19\n// - idx=6 key=000000n value=20\n// [1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | [6](?page=6)\n//\n// ## Page 4 of 6\n// - idx=0 key=000000p value=21\n// - idx=1 key=000000q value=22\n// - idx=2 key=000000r value=23\n// - idx=3 key=000000s value=24\n// - idx=4 key=000000t value=25\n// - idx=5 key=000000v value=26\n// - idx=6 key=000000w value=27\n// [1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6)\n//\n// ## Page 5 of 6\n// - idx=0 key=000000x value=28\n// - idx=1 key=000000y value=29\n// - idx=2 key=000000z value=30\n// - idx=3 key=0000010 value=31\n// - idx=4 key=0000011 value=32\n// - idx=5 key=0000012 value=33\n// - idx=6 key=0000013 value=34\n// [1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6)\n//\n// ## Page 6 of 6\n// - idx=0 key=0000014 value=35\n// - idx=1 key=0000015 value=36\n// - idx=2 key=0000016 value=37\n// - idx=3 key=0000017 value=38\n// - idx=4 key=0000018 value=39\n// - idx=5 key=0000019 value=40\n// - idx=6 key=000001a value=41\n// [1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6**\n//\n// ## Page 7 of 6\n// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"avlhelpers","path":"gno.land/p/demo/avlhelpers","files":[{"name":"avlhelpers.gno","body":"package avlhelpers\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// Iterate the keys in-order starting from the given prefix.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc IterateByteStringKeysByPrefix(tree avl.Tree, prefix string, cb avl.IterCbFn) {\n\tend := \"\"\n\tn := len(prefix)\n\t// To make the end of the search, increment the final character ASCII by one.\n\tfor n \u003e 0 {\n\t\tif ascii := int(prefix[n-1]); ascii \u003c 0xff {\n\t\t\tend = prefix[0:n-1] + string(ascii+1)\n\t\t\tbreak\n\t\t}\n\n\t\t// The last character is 0xff. Try the previous character.\n\t\tn--\n\t}\n\n\ttree.Iterate(prefix, end, cb)\n}\n\n// Get a list of keys starting from the given prefix. Limit the\n// number of results to maxResults.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc ListByteStringKeysByPrefix(tree avl.Tree, prefix string, maxResults int) []string {\n\tresult := []string{}\n\tIterateByteStringKeysByPrefix(tree, prefix, func(key string, value interface{}) bool {\n\t\tresult = append(result, key)\n\t\tif len(result) \u003e= maxResults {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn result\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"encoding/hex\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avlhelpers\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n\ttree := avl.Tree{}\n\n\t{\n\t\t// Empty tree.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t}\n\n\ttree.Set(\"alice\", \"\")\n\ttree.Set(\"andy\", \"\")\n\ttree.Set(\"bob\", \"\")\n\n\t{\n\t\t// Match only alice.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"al\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\t{\n\t\t// Match alice and andy.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t\tprintln(\"match: \" + matches[1])\n\t}\n\n\t{\n\t\t// Match alice and andy limited to 1.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\", 1)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\ttree = avl.Tree{}\n\ttree.Set(\"a\\xff\", \"\")\n\ttree.Set(\"a\\xff\\xff\", \"\")\n\ttree.Set(\"b\", \"\")\n\ttree.Set(\"\\xff\\xff\\x00\", \"\")\n\n\t{\n\t\t// Match only \"a\\xff\\xff\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n\n\t{\n\t\t// Match \"a\\xff\" and \"a\\xff\\xff\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[1]))))\n\t}\n\n\t{\n\t\t// Edge case: Match only \"\\xff\\xff\\x00\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n}\n\n// Output:\n// # matches: 0\n// # matches: 1\n// match: alice\n// # matches: 2\n// match: alice\n// match: andy\n// # matches: 1\n// match: alice\n// # matches: 1\n// match: 61ffff\n// # matches: 2\n// match: 61ff\n// match: 61ffff\n// # matches: 1\n// match: ffff00\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"bf","path":"gno.land/p/demo/bf","files":[{"name":"bf.gno","body":"package bf\n\nimport (\n\t\"strings\"\n)\n\nconst maxlen = 30000\n\nfunc Execute(code string) string {\n\tvar (\n\t\tmemory = make([]byte, maxlen) // memory tape\n\t\tpointer = 0 // initial memory pointer\n\t\tbuf strings.Builder\n\t)\n\n\t// Loop through each character in the code\n\tfor i := 0; i \u003c len(code); i++ {\n\t\tswitch code[i] {\n\t\tcase '\u003e':\n\t\t\t// Increment memory pointer\n\t\t\tpointer++\n\t\t\tif pointer \u003e= maxlen {\n\t\t\t\tpointer = 0\n\t\t\t}\n\t\tcase '\u003c':\n\t\t\t// Decrement memory pointer\n\t\t\tpointer--\n\t\t\tif pointer \u003c 0 {\n\t\t\t\tpointer = maxlen - 1\n\t\t\t}\n\t\tcase '+':\n\t\t\t// Increment the byte at the memory pointer\n\t\t\tmemory[pointer]++\n\t\tcase '-':\n\t\t\t// Decrement the byte at the memory pointer\n\t\t\tmemory[pointer]--\n\t\tcase '.':\n\t\t\t// Output the byte at the memory pointer\n\t\t\tbuf.WriteByte(memory[pointer])\n\t\tcase ',':\n\t\t\t// Input a byte and store it in the memory\n\t\t\tpanic(\"unsupported\")\n\t\t\t// fmt.Scan(\u0026memory[pointer])\n\t\tcase '[':\n\t\t\t// Jump forward past the matching ']' if the byte at the memory pointer is zero\n\t\t\tif memory[pointer] == 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti++\n\t\t\t\t\tif code[i] == '[' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == ']' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase ']':\n\t\t\t// Jump backward to the matching '[' if the byte at the memory pointer is nonzero\n\t\t\tif memory[pointer] != 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti--\n\t\t\t\t\tif code[i] == ']' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == '[' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti-- // Move back one more to compensate for the upcoming increment in the loop\n\t\t\t}\n\t\t}\n\t}\n\treturn buf.String()\n}\n"},{"name":"bf_test.gno","body":"package bf\n\nimport \"testing\"\n\nfunc TestExecuteBrainfuck(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tcode string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"hello\",\n\t\t\tcode: \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\",\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname: \"increment\",\n\t\t\tcode: \"+++++ +++++ [ \u003e +++++ ++ \u003c - ] \u003e +++++ .\",\n\t\t\texpected: \"K\",\n\t\t},\n\t\t// Add more test cases as needed\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := Execute(tc.code)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected output: %s, but got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"doc.gno","body":"// Package bf implements a minimalist Brainfuck virtual machine in Gno.\n//\n// Brainfuck is an esoteric programming language known for its simplicity and minimalistic design.\n// It operates on an array of memory cells, with a memory pointer that can move left or right.\n// The language consists of eight commands: \u003e \u003c + - . , [ ].\n//\n// Usage:\n// To execute Brainfuck code, use the Execute function and provide the code as a string.\n//\n//\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n//\toutput := bf.Execute(code)\n//\n// Note:\n// This implementation is a minimalist version and may not handle all edge cases or advanced features of the Brainfuck language.\n//\n// Reference:\n// For more information on Brainfuck, refer to the Wikipedia page: https://en.wikipedia.org/wiki/Brainfuck\npackage bf // import \"gno.land/p/demo/bf\"\n"},{"name":"run.gno","body":"package bf\n\n// for `gno run`\nfunc main() {\n\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n\t// TODO: code = os.Args...\n\tExecute(code)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"mux","path":"gno.land/p/demo/mux","files":[{"name":"doc.gno","body":"// Package mux provides a simple routing and rendering library for handling dynamic path-based requests in Gno contracts.\n//\n// The `mux` package aims to offer similar functionality to `http.ServeMux` in Go, but for Gno's Render() requests.\n// It allows you to define routes with dynamic parts and associate them with corresponding handler functions for rendering outputs.\n//\n// Usage:\n// 1. Create a new Router instance using `NewRouter()` to handle routing and rendering logic.\n// 2. Register routes and their associated handler functions using the `Handle(route, handler)` method.\n// 3. Implement the rendering logic within the handler functions, utilizing the `Request` and `ResponseWriter` types.\n// 4. Use the `Render(path)` method to process a given path and execute the corresponding handler function to obtain the rendered output.\n//\n// Route Patterns:\n// Routes can include dynamic parts enclosed in braces, such as \"users/{id}\" or \"hello/{name}\". The `Request` object's `GetVar(key)`\n// method allows you to extract the value of a specific variable from the path based on routing rules.\n//\n// Example:\n//\n//\trouter := mux.NewRouter()\n//\n//\t// Define a route with a variable and associated handler function\n//\trouter.HandleFunc(\"hello/{name}\", func(res *mux.ResponseWriter, req *mux.Request) {\n//\t\tname := req.GetVar(\"name\")\n//\t\tif name != \"\" {\n//\t\t\tres.Write(\"Hello, \" + name + \"!\")\n//\t\t} else {\n//\t\t\tres.Write(\"Hello, world!\")\n//\t\t}\n//\t})\n//\n//\t// Render the output for the \"/hello/Alice\" path\n//\toutput := router.Render(\"hello/Alice\")\n//\t// Output: \"Hello, Alice!\"\n//\n// Note: The `mux` package provides a basic routing and rendering mechanism for simple use cases. For more advanced routing features,\n// consider using more specialized libraries or frameworks.\npackage mux\n"},{"name":"handler.gno","body":"package mux\n\ntype Handler struct {\n\tPattern string\n\tFn HandlerFunc\n}\n\ntype HandlerFunc func(*ResponseWriter, *Request)\n\n// TODO: type ErrHandlerFunc func(*ResponseWriter, *Request) error\n// TODO: NotFoundHandler\n// TODO: AutomaticIndex\n"},{"name":"helpers.gno","body":"package mux\n\nfunc defaultNotFoundHandler(res *ResponseWriter, req *Request) {\n\tres.Write(\"404\")\n}\n"},{"name":"request.gno","body":"package mux\n\nimport \"strings\"\n\n// Request represents an incoming request.\ntype Request struct {\n\tPath string\n\tHandlerPath string\n}\n\n// GetVar retrieves a variable from the path based on routing rules.\nfunc (r *Request) GetVar(key string) string {\n\tvar (\n\t\thandlerParts = strings.Split(r.HandlerPath, \"/\")\n\t\treqParts = strings.Split(r.Path, \"/\")\n\t)\n\n\tfor i := 0; i \u003c len(handlerParts); i++ {\n\t\thandlerPart := handlerParts[i]\n\t\tswitch {\n\t\tcase handlerPart == \"*\":\n\t\t\t// XXX: implement a/b/*/d/e\n\t\t\tpanic(\"not implemented\")\n\t\tcase strings.HasPrefix(handlerPart, \"{\") \u0026\u0026 strings.HasSuffix(handlerPart, \"}\"):\n\t\t\tparameter := handlerPart[1 : len(handlerPart)-1]\n\t\t\tif parameter == key {\n\t\t\t\treturn reqParts[i]\n\t\t\t}\n\t\tdefault:\n\t\t\t// continue\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"},{"name":"request_test.gno","body":"package mux\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestRequest_GetVar(t *testing.T) {\n\tcases := []struct {\n\t\thandlerPath string\n\t\treqPath string\n\t\tgetVarKey string\n\t\texpectedOutput string\n\t}{\n\t\t{\"users/{id}\", \"users/123\", \"id\", \"123\"},\n\t\t{\"users/123\", \"users/123\", \"id\", \"\"},\n\t\t{\"users/{id}\", \"users/123\", \"nonexistent\", \"\"},\n\t\t{\"a/{b}/c/{d}\", \"a/42/c/1337\", \"b\", \"42\"},\n\t\t{\"a/{b}/c/{d}\", \"a/42/c/1337\", \"d\", \"1337\"},\n\t\t{\"{a}\", \"foo\", \"a\", \"foo\"},\n\t\t// TODO: wildcards: a/*/c\n\t\t// TODO: multiple patterns per slashes: a/{b}-{c}/d\n\t}\n\n\tfor _, tt := range cases {\n\t\tname := fmt.Sprintf(\"%s-%s\", tt.handlerPath, tt.reqPath)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\treq := \u0026Request{\n\t\t\t\tHandlerPath: tt.handlerPath,\n\t\t\t\tPath: tt.reqPath,\n\t\t\t}\n\n\t\t\toutput := req.GetVar(tt.getVarKey)\n\t\t\tif output != tt.expectedOutput {\n\t\t\t\tt.Errorf(\"Expected '%q, but got %q\", tt.expectedOutput, output)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"response.gno","body":"package mux\n\nimport \"strings\"\n\n// ResponseWriter represents the response writer.\ntype ResponseWriter struct {\n\toutput strings.Builder\n}\n\n// Write appends data to the response output.\nfunc (rw *ResponseWriter) Write(data string) {\n\trw.output.WriteString(data)\n}\n\n// Output returns the final response output.\nfunc (rw *ResponseWriter) Output() string {\n\treturn rw.output.String()\n}\n\n// TODO: func (rw *ResponseWriter) Header()...\n"},{"name":"router.gno","body":"package mux\n\nimport \"strings\"\n\n// Router handles the routing and rendering logic.\ntype Router struct {\n\troutes []Handler\n\tNotFoundHandler HandlerFunc\n}\n\n// NewRouter creates a new Router instance.\nfunc NewRouter() *Router {\n\treturn \u0026Router{\n\t\troutes: make([]Handler, 0),\n\t\tNotFoundHandler: defaultNotFoundHandler,\n\t}\n}\n\n// Render renders the output for the given path using the registered route handler.\nfunc (r *Router) Render(reqPath string) string {\n\treqParts := strings.Split(reqPath, \"/\")\n\n\tfor _, route := range r.routes {\n\t\tpatParts := strings.Split(route.Pattern, \"/\")\n\n\t\tif len(patParts) != len(reqParts) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatch := true\n\t\tfor i := 0; i \u003c len(patParts); i++ {\n\t\t\tpatPart := patParts[i]\n\t\t\treqPart := reqParts[i]\n\n\t\t\tif patPart == \"*\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(patPart, \"{\") \u0026\u0026 strings.HasSuffix(patPart, \"}\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif patPart != reqPart {\n\t\t\t\tmatch = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif match {\n\t\t\treq := \u0026Request{\n\t\t\t\tPath: reqPath,\n\t\t\t\tHandlerPath: route.Pattern,\n\t\t\t}\n\t\t\tres := \u0026ResponseWriter{}\n\t\t\troute.Fn(res, req)\n\t\t\treturn res.Output()\n\t\t}\n\t}\n\n\t// not found\n\treq := \u0026Request{Path: reqPath}\n\tres := \u0026ResponseWriter{}\n\tr.NotFoundHandler(res, req)\n\treturn res.Output()\n}\n\n// Handle registers a route and its handler function.\nfunc (r *Router) HandleFunc(pattern string, fn HandlerFunc) {\n\troute := Handler{Pattern: pattern, Fn: fn}\n\tr.routes = append(r.routes, route)\n}\n"},{"name":"router_test.gno","body":"package mux\n\nimport \"testing\"\n\nfunc TestRouter_Render(t *testing.T) {\n\t// Define handlers and route configuration\n\trouter := NewRouter()\n\trouter.HandleFunc(\"hello/{name}\", func(res *ResponseWriter, req *Request) {\n\t\tname := req.GetVar(\"name\")\n\t\tif name != \"\" {\n\t\t\tres.Write(\"Hello, \" + name + \"!\")\n\t\t} else {\n\t\t\tres.Write(\"Hello, world!\")\n\t\t}\n\t})\n\trouter.HandleFunc(\"hi\", func(res *ResponseWriter, req *Request) {\n\t\tres.Write(\"Hi, earth!\")\n\t})\n\n\tcases := []struct {\n\t\tpath string\n\t\texpectedOutput string\n\t}{\n\t\t{\"hello/Alice\", \"Hello, Alice!\"},\n\t\t{\"hi\", \"Hi, earth!\"},\n\t\t{\"hello/Bob\", \"Hello, Bob!\"},\n\t\t// TODO: {\"hello\", \"Hello, world!\"},\n\t\t// TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc\n\t}\n\tfor _, tt := range cases {\n\t\tt.Run(tt.path, func(t *testing.T) {\n\t\t\toutput := router.Render(tt.path)\n\t\t\tif output != tt.expectedOutput {\n\t\t\t\tt.Errorf(\"Expected output %q, but got %q\", tt.expectedOutput, output)\n\t\t\t}\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"blog","path":"gno.land/p/demo/blog","files":[{"name":"blog.gno","body":"package blog\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Blog struct {\n\tTitle string\n\tPrefix string // i.e. r/gnoland/blog:\n\tPosts avl.Tree // slug -\u003e *Post\n\tPostsPublished avl.Tree // published-date -\u003e *Post\n\tPostsAlphabetical avl.Tree // title -\u003e *Post\n\tNoBreadcrumb bool\n}\n\nfunc (b Blog) RenderLastPostsWidget(limit int) string {\n\tif b.PostsPublished.Size() == 0 {\n\t\treturn \"No posts.\"\n\t}\n\n\toutput := \"\"\n\ti := 0\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tp := value.(*Post)\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", p.Title, p.URL())\n\t\ti++\n\t\treturn i \u003e= limit\n\t})\n\treturn output\n}\n\nfunc (b Blog) RenderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tif !b.NoBreadcrumb {\n\t\tres.Write(breadcrumb([]string{b.Title}))\n\t}\n\n\tif b.Posts.Size() == 0 {\n\t\tres.Write(\"No posts.\")\n\t\treturn\n\t}\n\n\tres.Write(\"\u003cdiv class='columns-3'\u003e\")\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpost := value.(*Post)\n\t\tres.Write(post.RenderListItem())\n\t\treturn false\n\t})\n\tres.Write(\"\u003c/div\u003e\")\n\n\t// FIXME: tag list/cloud.\n}\n\nfunc (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) {\n\tslug := req.GetVar(\"slug\")\n\n\tpost, found := b.Posts.Get(slug)\n\tif !found {\n\t\tres.Write(\"404\")\n\t\treturn\n\t}\n\tp := post.(*Post)\n\n\tres.Write(\"\u003cmain class='gno-tmpl-page'\u003e\" + \"\\n\\n\")\n\n\tres.Write(\"# \" + p.Title + \"\\n\\n\")\n\tres.Write(p.Body + \"\\n\\n\")\n\tres.Write(\"---\\n\\n\")\n\n\tres.Write(p.RenderTagList() + \"\\n\\n\")\n\tres.Write(p.RenderAuthorList() + \"\\n\\n\")\n\tres.Write(p.RenderPublishData() + \"\\n\\n\")\n\n\tres.Write(\"---\\n\")\n\tres.Write(\"\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\\n\\n\")\n\n\t// comments\n\tp.Comments.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tcomment := value.(*Comment)\n\t\tres.Write(comment.RenderListItem())\n\t\treturn false\n\t})\n\n\tres.Write(\"\u003c/details\u003e\\n\")\n\tres.Write(\"\u003c/main\u003e\")\n}\n\nfunc (b Blog) RenderTag(res *mux.ResponseWriter, req *mux.Request) {\n\tslug := req.GetVar(\"slug\")\n\n\tif slug == \"\" {\n\t\tres.Write(\"404\")\n\t\treturn\n\t}\n\n\tif !b.NoBreadcrumb {\n\t\tbreadStr := breadcrumb([]string{\n\t\t\tufmt.Sprintf(\"[%s](%s)\", b.Title, b.Prefix),\n\t\t\t\"t\",\n\t\t\tslug,\n\t\t})\n\t\tres.Write(breadStr)\n\t}\n\n\tnb := 0\n\tb.Posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpost := value.(*Post)\n\t\tif !post.HasTag(slug) {\n\t\t\treturn false\n\t\t}\n\t\tres.Write(post.RenderListItem())\n\t\tnb++\n\t\treturn false\n\t})\n\tif nb == 0 {\n\t\tres.Write(\"No posts.\")\n\t}\n}\n\nfunc (b Blog) Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", b.RenderHome)\n\trouter.HandleFunc(\"p/{slug}\", b.RenderPost)\n\trouter.HandleFunc(\"t/{slug}\", b.RenderTag)\n\treturn router.Render(path)\n}\n\nfunc (b *Blog) NewPost(publisher std.Address, slug, title, body, pubDate string, authors, tags []string) error {\n\tif _, found := b.Posts.Get(slug); found {\n\t\treturn ErrPostSlugExists\n\t}\n\n\tvar parsedTime time.Time\n\tvar err error\n\tif pubDate != \"\" {\n\t\tparsedTime, err = time.Parse(time.RFC3339, pubDate)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// If no publication date was passed in by caller, take current block time\n\t\tparsedTime = time.Now()\n\t}\n\n\tpost := \u0026Post{\n\t\tPublisher: publisher,\n\t\tAuthors: authors,\n\t\tSlug: slug,\n\t\tTitle: title,\n\t\tBody: body,\n\t\tTags: tags,\n\t\tCreatedAt: parsedTime,\n\t}\n\n\treturn b.prepareAndSetPost(post, false)\n}\n\nfunc (b *Blog) prepareAndSetPost(post *Post, edit bool) error {\n\tpost.Title = strings.TrimSpace(post.Title)\n\tpost.Body = strings.TrimSpace(post.Body)\n\n\tif post.Title == \"\" {\n\t\treturn ErrPostTitleMissing\n\t}\n\tif post.Body == \"\" {\n\t\treturn ErrPostBodyMissing\n\t}\n\tif post.Slug == \"\" {\n\t\treturn ErrPostSlugMissing\n\t}\n\n\tpost.Blog = b\n\tpost.UpdatedAt = time.Now()\n\n\ttrimmedTitleKey := getTitleKey(post.Title)\n\tpubDateKey := getPublishedKey(post.CreatedAt)\n\n\tif !edit {\n\t\t// Cannot have two posts with same title key\n\t\tif _, found := b.PostsAlphabetical.Get(trimmedTitleKey); found {\n\t\t\treturn ErrPostTitleExists\n\t\t}\n\t\t// Cannot have two posts with *exact* same timestamp\n\t\tif _, found := b.PostsPublished.Get(pubDateKey); found {\n\t\t\treturn ErrPostPubDateExists\n\t\t}\n\t}\n\n\t// Store post under keys\n\tb.PostsAlphabetical.Set(trimmedTitleKey, post)\n\tb.PostsPublished.Set(pubDateKey, post)\n\tb.Posts.Set(post.Slug, post)\n\n\treturn nil\n}\n\nfunc (b *Blog) RemovePost(slug string) {\n\tp, exists := b.Posts.Get(slug)\n\tif !exists {\n\t\tpanic(\"post with specified slug doesn't exist\")\n\t}\n\n\tpost := p.(*Post)\n\n\ttitleKey := getTitleKey(post.Title)\n\tpublishedKey := getPublishedKey(post.CreatedAt)\n\n\t_, _ = b.Posts.Remove(slug)\n\t_, _ = b.PostsAlphabetical.Remove(titleKey)\n\t_, _ = b.PostsPublished.Remove(publishedKey)\n}\n\nfunc (b *Blog) GetPost(slug string) *Post {\n\tpost, found := b.Posts.Get(slug)\n\tif !found {\n\t\treturn nil\n\t}\n\treturn post.(*Post)\n}\n\ntype Post struct {\n\tBlog *Blog\n\tSlug string // FIXME: save space?\n\tTitle string\n\tBody string\n\tCreatedAt time.Time\n\tUpdatedAt time.Time\n\tComments avl.Tree\n\tAuthors []string\n\tPublisher std.Address\n\tTags []string\n\tCommentIndex int\n}\n\nfunc (p *Post) Update(title, body, publicationDate string, authors, tags []string) error {\n\tp.Title = title\n\tp.Body = body\n\tp.Tags = tags\n\tp.Authors = authors\n\n\tparsedTime, err := time.Parse(time.RFC3339, publicationDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.CreatedAt = parsedTime\n\treturn p.Blog.prepareAndSetPost(p, true)\n}\n\nfunc (p *Post) AddComment(author std.Address, comment string) error {\n\tif p == nil {\n\t\treturn ErrNoSuchPost\n\t}\n\tp.CommentIndex++\n\tcommentKey := strconv.Itoa(p.CommentIndex)\n\tcomment = strings.TrimSpace(comment)\n\tp.Comments.Set(commentKey, \u0026Comment{\n\t\tPost: p,\n\t\tCreatedAt: time.Now(),\n\t\tAuthor: author,\n\t\tComment: comment,\n\t})\n\n\treturn nil\n}\n\nfunc (p *Post) DeleteComment(index int) error {\n\tif p == nil {\n\t\treturn ErrNoSuchPost\n\t}\n\tcommentKey := strconv.Itoa(index)\n\tp.Comments.Remove(commentKey)\n\treturn nil\n}\n\nfunc (p *Post) HasTag(tag string) bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\tfor _, t := range p.Tags {\n\t\tif t == tag {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (p *Post) RenderListItem() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\toutput := \"\u003cdiv\u003e\\n\\n\"\n\toutput += ufmt.Sprintf(\"### [%s](%s)\\n\", p.Title, p.URL())\n\t// output += ufmt.Sprintf(\"**[Learn More](%s)**\\n\\n\", p.URL())\n\n\toutput += \" \" + p.CreatedAt.Format(\"02 Jan 2006\")\n\t// output += p.Summary() + \"\\n\\n\"\n\t// output += p.RenderTagList() + \"\\n\\n\"\n\toutput += \"\\n\"\n\toutput += \"\u003c/div\u003e\"\n\treturn output\n}\n\n// Render post tags\nfunc (p *Post) RenderTagList() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\tif len(p.Tags) == 0 {\n\t\treturn \"\"\n\t}\n\n\toutput := \"Tags: \"\n\tfor idx, tag := range p.Tags {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" \"\n\t\t}\n\t\ttagURL := p.Blog.Prefix + \"t/\" + tag\n\t\toutput += ufmt.Sprintf(\"[#%s](%s)\", tag, tagURL)\n\n\t}\n\treturn output\n}\n\n// Render authors if there are any\nfunc (p *Post) RenderAuthorList() string {\n\tout := \"Written\"\n\tif len(p.Authors) != 0 {\n\t\tout += \" by \"\n\n\t\tfor idx, author := range p.Authors {\n\t\t\tout += author\n\t\t\tif idx \u003c len(p.Authors)-1 {\n\t\t\t\tout += \", \"\n\t\t\t}\n\t\t}\n\t}\n\tout += \" on \" + p.CreatedAt.Format(\"02 Jan 2006\")\n\n\treturn out\n}\n\nfunc (p *Post) RenderPublishData() string {\n\tout := \"Published \"\n\tif p.Publisher != \"\" {\n\t\tout += \"by \" + p.Publisher.String() + \" \"\n\t}\n\tout += \"to \" + p.Blog.Title\n\n\treturn out\n}\n\nfunc (p *Post) URL() string {\n\tif p == nil {\n\t\treturn p.Blog.Prefix + \"404\"\n\t}\n\treturn p.Blog.Prefix + \"p/\" + p.Slug\n}\n\nfunc (p *Post) Summary() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\n\t// FIXME: better summary.\n\tlines := strings.Split(p.Body, \"\\n\")\n\tif len(lines) \u003c= 3 {\n\t\treturn p.Body\n\t}\n\treturn strings.Join(lines[0:3], \"\\n\") + \"...\"\n}\n\ntype Comment struct {\n\tPost *Post\n\tCreatedAt time.Time\n\tAuthor std.Address\n\tComment string\n}\n\nfunc (c Comment) RenderListItem() string {\n\toutput := \"\u003ch5\u003e\"\n\toutput += c.Comment + \"\\n\\n\"\n\toutput += \"\u003c/h5\u003e\"\n\n\toutput += \"\u003ch6\u003e\"\n\toutput += ufmt.Sprintf(\"by %s on %s\", c.Author, c.CreatedAt.Format(time.RFC822))\n\toutput += \"\u003c/h6\u003e\\n\\n\"\n\n\toutput += \"---\\n\\n\"\n\n\treturn output\n}\n"},{"name":"blog_test.gno","body":"package blog\n\n// TODO: add generic tests here.\n// right now, you can checkout r/gnoland/blog/*_test.gno.\n"},{"name":"errors.gno","body":"package blog\n\nimport \"errors\"\n\nvar (\n\tErrPostTitleMissing = errors.New(\"post title is missing\")\n\tErrPostSlugMissing = errors.New(\"post slug is missing\")\n\tErrPostBodyMissing = errors.New(\"post body is missing\")\n\tErrPostSlugExists = errors.New(\"post with specified slug already exists\")\n\tErrPostPubDateExists = errors.New(\"post with specified publication date exists\")\n\tErrPostTitleExists = errors.New(\"post with specified title already exists\")\n\tErrNoSuchPost = errors.New(\"no such post\")\n)\n"},{"name":"util.gno","body":"package blog\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\nfunc breadcrumb(parts []string) string {\n\treturn \"# \" + strings.Join(parts, \" / \") + \"\\n\\n\"\n}\n\nfunc getTitleKey(title string) string {\n\treturn strings.Replace(title, \" \", \"\", -1)\n}\n\nfunc getPublishedKey(t time.Time) string {\n\treturn t.Format(time.RFC3339)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"cford32","path":"gno.land/p/demo/cford32","files":[{"name":"LICENSE","body":"Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"},{"name":"README.md","body":"# cford32\n\n```\npackage cford32 // import \"gno.land/p/demo/cford32\"\n\nPackage cford32 implements a base32-like encoding/decoding package, with the\nencoding scheme specified by Douglas Crockford.\n\nFrom the website, the requirements of said encoding scheme are to:\n\n - Be human readable and machine readable.\n - Be compact. Humans have difficulty in manipulating long strings of arbitrary\n symbols.\n - Be error resistant. Entering the symbols must not require keyboarding\n gymnastics.\n - Be pronounceable. Humans should be able to accurately transmit the symbols\n to other humans using a telephone.\n\nThis is slightly different from a simple difference in encoding table from\nthe Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\nparsed as 1, and o O is parsed as 0.\n\nThis package additionally provides ways to encode uint64's efficiently, as well\nas efficient encoding to a lowercase variation of the encoding. The encodings\nnever use paddings.\n\n# Uint64 Encoding\n\nAside from lower/uppercase encoding, there is a compact encoding, allowing to\nencode all values in [0,2^34), and the full encoding, allowing all values in\n[0,2^64). The compact encoding uses 7 characters, and the full encoding uses 13\ncharacters. Both are parsed unambiguously by the Uint64 decoder.\n\nThe compact encodings have the first character between ['0','f'], while the\nfull encoding's first character ranges between ['g','z']. Practically, in your\nusage of the package, you should consider which one to use and stick with it,\nwhile considering that the compact encoding, once it reaches 2^34, automatically\nswitches to the full encoding. The properties of the generated strings are still\nmaintained: for instance, any two encoded uint64s x,y consistently generated\nwith the compact encoding, if the numeric value is x \u003c y, will also be x \u003c y in\nlexical ordering. However, values [0,2^34) have a \"double encoding\", which if\nmixed together lose the lexical ordering property.\n\nThe Uint64 encoding is most useful for generating string versions of Uint64 IDs.\nPractically, it allows you to retain sleek and compact IDs for your application\nfor the first 2^34 (\u003e17 billion) entities, while seamlessly rolling over to the\nfull encoding should you exceed that. You are encouraged to use it unless you\nhave a requirement or preferences for IDs consistently being always the same\nsize.\n\nTo use the cford32 encoding for IDs, you may want to consider using package\ngno.land/p/demo/seqid.\n\n[specified by Douglas Crockford]: https://www.crockford.com/base32.html\n\nfunc AppendCompact(id uint64, b []byte) []byte\nfunc AppendDecode(dst, src []byte) ([]byte, error)\nfunc AppendEncode(dst, src []byte) []byte\nfunc AppendEncodeLower(dst, src []byte) []byte\nfunc Decode(dst, src []byte) (n int, err error)\nfunc DecodeString(s string) ([]byte, error)\nfunc DecodedLen(n int) int\nfunc Encode(dst, src []byte)\nfunc EncodeLower(dst, src []byte)\nfunc EncodeToString(src []byte) string\nfunc EncodeToStringLower(src []byte) string\nfunc EncodedLen(n int) int\nfunc NewDecoder(r io.Reader) io.Reader\nfunc NewEncoder(w io.Writer) io.WriteCloser\nfunc NewEncoderLower(w io.Writer) io.WriteCloser\nfunc PutCompact(id uint64) []byte\nfunc PutUint64(id uint64) [13]byte\nfunc PutUint64Lower(id uint64) [13]byte\nfunc Uint64(b []byte) (uint64, error)\ntype CorruptInputError int64\n```\n"},{"name":"cford32.gno","body":"// Modified from the Go Source code for encoding/base32.\n// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package cford32 implements a base32-like encoding/decoding package, with the\n// encoding scheme [specified by Douglas Crockford].\n//\n// From the website, the requirements of said encoding scheme are to:\n//\n// - Be human readable and machine readable.\n// - Be compact. Humans have difficulty in manipulating long strings of arbitrary symbols.\n// - Be error resistant. Entering the symbols must not require keyboarding gymnastics.\n// - Be pronounceable. Humans should be able to accurately transmit the symbols to other humans using a telephone.\n//\n// This is slightly different from a simple difference in encoding table from\n// the Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\n// parsed as 1, and o O is parsed as 0.\n//\n// This package additionally provides ways to encode uint64's efficiently,\n// as well as efficient encoding to a lowercase variation of the encoding.\n// The encodings never use paddings.\n//\n// # Uint64 Encoding\n//\n// Aside from lower/uppercase encoding, there is a compact encoding, allowing\n// to encode all values in [0,2^34), and the full encoding, allowing all\n// values in [0,2^64). The compact encoding uses 7 characters, and the full\n// encoding uses 13 characters. Both are parsed unambiguously by the Uint64\n// decoder.\n//\n// The compact encodings have the first character between ['0','f'], while the\n// full encoding's first character ranges between ['g','z']. Practically, in\n// your usage of the package, you should consider which one to use and stick\n// with it, while considering that the compact encoding, once it reaches 2^34,\n// automatically switches to the full encoding. The properties of the generated\n// strings are still maintained: for instance, any two encoded uint64s x,y\n// consistently generated with the compact encoding, if the numeric value is\n// x \u003c y, will also be x \u003c y in lexical ordering. However, values [0,2^34) have a\n// \"double encoding\", which if mixed together lose the lexical ordering property.\n//\n// The Uint64 encoding is most useful for generating string versions of Uint64\n// IDs. Practically, it allows you to retain sleek and compact IDs for your\n// application for the first 2^34 (\u003e17 billion) entities, while seamlessly\n// rolling over to the full encoding should you exceed that. You are encouraged\n// to use it unless you have a requirement or preferences for IDs consistently\n// being always the same size.\n//\n// To use the cford32 encoding for IDs, you may want to consider using package\n// [gno.land/p/demo/seqid].\n//\n// [specified by Douglas Crockford]: https://www.crockford.com/base32.html\npackage cford32\n\nimport (\n\t\"io\"\n\t\"strconv\"\n)\n\nconst (\n\tencTable = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"\n\tencTableLower = \"0123456789abcdefghjkmnpqrstvwxyz\"\n\n\t// each line is 16 bytes\n\tdecTable = \"\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 00-0f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 10-1f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 20-2f\n\t\t\"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\xff\\xff\\xff\\xff\\xff\\xff\" + // 30-3f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 40-4f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 50-5f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 60-6f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 70-7f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 80-ff (not ASCII)\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n)\n\n// CorruptInputError is returned by parsing functions when an invalid character\n// in the input is found. The integer value represents the byte index where\n// the error occurred.\n//\n// This is typically because the given character does not exist in the encoding.\ntype CorruptInputError int64\n\nfunc (e CorruptInputError) Error() string {\n\treturn \"illegal cford32 data at input byte \" + strconv.FormatInt(int64(e), 10)\n}\n\n// Uint64 parses a cford32-encoded byte slice into a uint64.\n//\n// - The parser requires all provided character to be valid cford32 characters.\n// - The parser disregards case.\n// - If the first character is '0' \u003c= c \u003c= 'f', then the passed value is assumed\n// encoded in the compact encoding, and must be 7 characters long.\n// - If the first character is 'g' \u003c= c \u003c= 'z', then the passed value is\n// assumed encoded in the full encoding, and must be 13 characters long.\n//\n// If any of these requirements fail, a CorruptInputError will be returned.\nfunc Uint64(b []byte) (uint64, error) {\n\tswitch {\n\tdefault:\n\t\treturn 0, CorruptInputError(0)\n\tcase len(b) == 7 \u0026\u0026 b[0] \u003e= '0' \u0026\u0026 b[0] \u003c= 'f':\n\t\tdecVals := [7]byte{\n\t\t\tdecTable[b[0]],\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c30 |\n\t\t\tuint64(decVals[1])\u003c\u003c25 |\n\t\t\tuint64(decVals[2])\u003c\u003c20 |\n\t\t\tuint64(decVals[3])\u003c\u003c15 |\n\t\t\tuint64(decVals[4])\u003c\u003c10 |\n\t\t\tuint64(decVals[5])\u003c\u003c5 |\n\t\t\tuint64(decVals[6]), nil\n\tcase len(b) == 13 \u0026\u0026 b[0] \u003e= 'g' \u0026\u0026 b[0] \u003c= 'z':\n\t\tdecVals := [13]byte{\n\t\t\tdecTable[b[0]] \u0026 0x0F, // disregard high bit\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t\tdecTable[b[7]],\n\t\t\tdecTable[b[8]],\n\t\t\tdecTable[b[9]],\n\t\t\tdecTable[b[10]],\n\t\t\tdecTable[b[11]],\n\t\t\tdecTable[b[12]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c60 |\n\t\t\tuint64(decVals[1])\u003c\u003c55 |\n\t\t\tuint64(decVals[2])\u003c\u003c50 |\n\t\t\tuint64(decVals[3])\u003c\u003c45 |\n\t\t\tuint64(decVals[4])\u003c\u003c40 |\n\t\t\tuint64(decVals[5])\u003c\u003c35 |\n\t\t\tuint64(decVals[6])\u003c\u003c30 |\n\t\t\tuint64(decVals[7])\u003c\u003c25 |\n\t\t\tuint64(decVals[8])\u003c\u003c20 |\n\t\t\tuint64(decVals[9])\u003c\u003c15 |\n\t\t\tuint64(decVals[10])\u003c\u003c10 |\n\t\t\tuint64(decVals[11])\u003c\u003c5 |\n\t\t\tuint64(decVals[12]), nil\n\t}\n}\n\nconst mask = 31\n\n// PutUint64 returns a cford32-encoded byte slice.\nfunc PutUint64(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTable[id\u003e\u003e60\u0026mask|0x10], // specify full encoding\n\t\tencTable[id\u003e\u003e55\u0026mask],\n\t\tencTable[id\u003e\u003e50\u0026mask],\n\t\tencTable[id\u003e\u003e45\u0026mask],\n\t\tencTable[id\u003e\u003e40\u0026mask],\n\t\tencTable[id\u003e\u003e35\u0026mask],\n\t\tencTable[id\u003e\u003e30\u0026mask],\n\t\tencTable[id\u003e\u003e25\u0026mask],\n\t\tencTable[id\u003e\u003e20\u0026mask],\n\t\tencTable[id\u003e\u003e15\u0026mask],\n\t\tencTable[id\u003e\u003e10\u0026mask],\n\t\tencTable[id\u003e\u003e5\u0026mask],\n\t\tencTable[id\u0026mask],\n\t}\n}\n\n// PutUint64Lower returns a cford32-encoded byte array, swapping uppercase\n// letters with lowercase.\n//\n// For more information on how the value is encoded, see [Uint64].\nfunc PutUint64Lower(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t}\n}\n\n// PutCompact returns a cford32-encoded byte slice, using the compact\n// representation of cford32 described in the package documentation where\n// possible (all values of id \u003c 1\u003c\u003c34). The lowercase encoding is used.\n//\n// The resulting byte slice will be 7 bytes long for all compact values,\n// and 13 bytes long for\nfunc PutCompact(id uint64) []byte {\n\treturn AppendCompact(id, nil)\n}\n\n// AppendCompact works like [PutCompact] but appends to the given byte slice\n// instead of allocating one anew.\nfunc AppendCompact(id uint64, b []byte) []byte {\n\tconst maxCompact = 1 \u003c\u003c 34\n\tif id \u003c maxCompact {\n\t\treturn append(b,\n\t\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\t\tencTableLower[id\u0026mask],\n\t\t)\n\t}\n\treturn append(b,\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t)\n}\n\nfunc DecodedLen(n int) int {\n\treturn n/8*5 + n%8*5/8\n}\n\nfunc EncodedLen(n int) int {\n\treturn n/5*8 + (n%5*8+4)/5\n}\n\n// Encode encodes src using the encoding enc,\n// writing [EncodedLen](len(src)) bytes to dst.\n//\n// The encoding does not contain any padding, unlike Go's base32.\nfunc Encode(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTable[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTable[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTable[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTable[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTable[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTable[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTable[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTable[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTable[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTable[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTable[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTable[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTable[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTable[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTable[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// EncodeLower is like [Encode], but uses the lowercase\nfunc EncodeLower(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTableLower[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTableLower[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTableLower[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTableLower[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTableLower[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTableLower[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTableLower[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTableLower[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTableLower[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTableLower[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTableLower[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTableLower[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTableLower[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTableLower[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTableLower[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// AppendEncode appends the cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncode(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncode(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\n// AppendEncodeLower appends the lowercase cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncodeLower(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncodeLower(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\nfunc grow(s []byte, n int) []byte {\n\t// slices.Grow\n\tif n -= cap(s) - len(s); n \u003e 0 {\n\t\tnews := make([]byte, cap(s)+n)\n\t\tcopy(news[:cap(s)], s[:cap(s)])\n\t\treturn news[:len(s)]\n\t}\n\treturn s\n}\n\n// EncodeToString returns the cford32 encoding of src.\nfunc EncodeToString(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncode(buf, src)\n\treturn string(buf)\n}\n\n// EncodeToStringLower returns the cford32 lowercase encoding of src.\nfunc EncodeToStringLower(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncodeLower(buf, src)\n\treturn string(buf)\n}\n\nfunc decode(dst, src []byte) (n int, err error) {\n\tdsti := 0\n\tolen := len(src)\n\n\tfor len(src) \u003e 0 {\n\t\t// Decode quantum using the base32 alphabet\n\t\tvar dbuf [8]byte\n\t\tdlen := 8\n\n\t\tfor j := 0; j \u003c 8; {\n\t\t\tif len(src) == 0 {\n\t\t\t\t// We have reached the end and are not expecting any padding\n\t\t\t\tdlen = j\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tin := src[0]\n\t\t\tsrc = src[1:]\n\t\t\tdbuf[j] = decTable[in]\n\t\t\tif dbuf[j] == 0xFF {\n\t\t\t\treturn n, CorruptInputError(olen - len(src) - 1)\n\t\t\t}\n\t\t\tj++\n\t\t}\n\n\t\t// Pack 8x 5-bit source blocks into 5 byte destination\n\t\t// quantum\n\t\tswitch dlen {\n\t\tcase 8:\n\t\t\tdst[dsti+4] = dbuf[6]\u003c\u003c5 | dbuf[7]\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 7:\n\t\t\tdst[dsti+3] = dbuf[4]\u003c\u003c7 | dbuf[5]\u003c\u003c2 | dbuf[6]\u003e\u003e3\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 5:\n\t\t\tdst[dsti+2] = dbuf[3]\u003c\u003c4 | dbuf[4]\u003e\u003e1\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 4:\n\t\t\tdst[dsti+1] = dbuf[1]\u003c\u003c6 | dbuf[2]\u003c\u003c1 | dbuf[3]\u003e\u003e4\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 2:\n\t\t\tdst[dsti+0] = dbuf[0]\u003c\u003c3 | dbuf[1]\u003e\u003e2\n\t\t\tn++\n\t\t}\n\t\tdsti += 5\n\t}\n\treturn n, nil\n}\n\ntype encoder struct {\n\terr error\n\tw io.Writer\n\tenc func(dst, src []byte)\n\tbuf [5]byte // buffered data waiting to be encoded\n\tnbuf int // number of bytes in buf\n\tout [1024]byte // output buffer\n}\n\nfunc NewEncoder(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: Encode}\n}\n\nfunc NewEncoderLower(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: EncodeLower}\n}\n\nfunc (e *encoder) Write(p []byte) (n int, err error) {\n\tif e.err != nil {\n\t\treturn 0, e.err\n\t}\n\n\t// Leading fringe.\n\tif e.nbuf \u003e 0 {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(p) \u0026\u0026 e.nbuf \u003c 5; i++ {\n\t\t\te.buf[e.nbuf] = p[i]\n\t\t\te.nbuf++\n\t\t}\n\t\tn += i\n\t\tp = p[i:]\n\t\tif e.nbuf \u003c 5 {\n\t\t\treturn\n\t\t}\n\t\te.enc(e.out[0:], e.buf[0:])\n\t\tif _, e.err = e.w.Write(e.out[0:8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\te.nbuf = 0\n\t}\n\n\t// Large interior chunks.\n\tfor len(p) \u003e= 5 {\n\t\tnn := len(e.out) / 8 * 5\n\t\tif nn \u003e len(p) {\n\t\t\tnn = len(p)\n\t\t\tnn -= nn % 5\n\t\t}\n\t\te.enc(e.out[0:], p[0:nn])\n\t\tif _, e.err = e.w.Write(e.out[0 : nn/5*8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\tn += nn\n\t\tp = p[nn:]\n\t}\n\n\t// Trailing fringe.\n\tcopy(e.buf[:], p)\n\te.nbuf = len(p)\n\tn += len(p)\n\treturn\n}\n\n// Close flushes any pending output from the encoder.\n// It is an error to call Write after calling Close.\nfunc (e *encoder) Close() error {\n\t// If there's anything left in the buffer, flush it out\n\tif e.err == nil \u0026\u0026 e.nbuf \u003e 0 {\n\t\te.enc(e.out[0:], e.buf[0:e.nbuf])\n\t\tencodedLen := EncodedLen(e.nbuf)\n\t\te.nbuf = 0\n\t\t_, e.err = e.w.Write(e.out[0:encodedLen])\n\t}\n\treturn e.err\n}\n\n// Decode decodes src using cford32. It writes at most\n// [DecodedLen](len(src)) bytes to dst and returns the number of bytes\n// written. If src contains invalid cford32 data, it will return the\n// number of bytes successfully written and [CorruptInputError].\n// Newline characters (\\r and \\n) are ignored.\nfunc Decode(dst, src []byte) (n int, err error) {\n\tbuf := make([]byte, len(src))\n\tl := stripNewlines(buf, src)\n\treturn decode(dst, buf[:l])\n}\n\n// AppendDecode appends the cford32 decoded src to dst\n// and returns the extended buffer.\n// If the input is malformed, it returns the partially decoded src and an error.\nfunc AppendDecode(dst, src []byte) ([]byte, error) {\n\tn := DecodedLen(len(src))\n\n\tdst = grow(dst, n)\n\tdstsl := dst[len(dst) : len(dst)+n]\n\tn, err := Decode(dstsl, src)\n\treturn dst[:len(dst)+n], err\n}\n\n// DecodeString returns the bytes represented by the cford32 string s.\nfunc DecodeString(s string) ([]byte, error) {\n\tbuf := []byte(s)\n\tl := stripNewlines(buf, buf)\n\tn, err := decode(buf, buf[:l])\n\treturn buf[:n], err\n}\n\n// stripNewlines removes newline characters and returns the number\n// of non-newline characters copied to dst.\nfunc stripNewlines(dst, src []byte) int {\n\toffset := 0\n\tfor _, b := range src {\n\t\tif b == '\\r' || b == '\\n' {\n\t\t\tcontinue\n\t\t}\n\t\tdst[offset] = b\n\t\toffset++\n\t}\n\treturn offset\n}\n\ntype decoder struct {\n\terr error\n\tr io.Reader\n\tbuf [1024]byte // leftover input\n\tnbuf int\n\tout []byte // leftover decoded output\n\toutbuf [1024 / 8 * 5]byte\n}\n\n// NewDecoder constructs a new base32 stream decoder.\nfunc NewDecoder(r io.Reader) io.Reader {\n\treturn \u0026decoder{r: \u0026newlineFilteringReader{r}}\n}\n\nfunc readEncodedData(r io.Reader, buf []byte) (n int, err error) {\n\tfor n \u003c 1 \u0026\u0026 err == nil {\n\t\tvar nn int\n\t\tnn, err = r.Read(buf[n:])\n\t\tn += nn\n\t}\n\treturn\n}\n\nfunc (d *decoder) Read(p []byte) (n int, err error) {\n\t// Use leftover decoded output from last read.\n\tif len(d.out) \u003e 0 {\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t\tif len(d.out) == 0 {\n\t\t\treturn n, d.err\n\t\t}\n\t\treturn n, nil\n\t}\n\n\tif d.err != nil {\n\t\treturn 0, d.err\n\t}\n\n\t// Read nn bytes from input, bounded [8,len(d.buf)]\n\tnn := (len(p)/5 + 1) * 8\n\tif nn \u003e len(d.buf) {\n\t\tnn = len(d.buf)\n\t}\n\n\tnn, d.err = readEncodedData(d.r, d.buf[d.nbuf:nn])\n\td.nbuf += nn\n\tif d.nbuf \u003c 1 {\n\t\treturn 0, d.err\n\t}\n\n\t// Decode chunk into p, or d.out and then p if p is too small.\n\tnr := d.nbuf\n\tif d.err != io.EOF \u0026\u0026 nr%8 != 0 {\n\t\tnr -= nr % 8\n\t}\n\tnw := DecodedLen(d.nbuf)\n\n\tif nw \u003e len(p) {\n\t\tnw, err = decode(d.outbuf[0:], d.buf[0:nr])\n\t\td.out = d.outbuf[0:nw]\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t} else {\n\t\tn, err = decode(p, d.buf[0:nr])\n\t}\n\td.nbuf -= nr\n\tfor i := 0; i \u003c d.nbuf; i++ {\n\t\td.buf[i] = d.buf[i+nr]\n\t}\n\n\tif err != nil \u0026\u0026 (d.err == nil || d.err == io.EOF) {\n\t\td.err = err\n\t}\n\n\tif len(d.out) \u003e 0 {\n\t\t// We cannot return all the decoded bytes to the caller in this\n\t\t// invocation of Read, so we return a nil error to ensure that Read\n\t\t// will be called again. The error stored in d.err, if any, will be\n\t\t// returned with the last set of decoded bytes.\n\t\treturn n, nil\n\t}\n\n\treturn n, d.err\n}\n\ntype newlineFilteringReader struct {\n\twrapped io.Reader\n}\n\nfunc (r *newlineFilteringReader) Read(p []byte) (int, error) {\n\tn, err := r.wrapped.Read(p)\n\tfor n \u003e 0 {\n\t\ts := p[0:n]\n\t\toffset := stripNewlines(s, s)\n\t\tif err != nil || offset \u003e 0 {\n\t\t\treturn offset, err\n\t\t}\n\t\t// Previous buffer entirely whitespace, read again\n\t\tn, err = r.wrapped.Read(p)\n\t}\n\treturn n, err\n}\n"},{"name":"cford32_test.gno","body":"package cford32\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestCompactRoundtrip(t *testing.T) {\n\tbuf := make([]byte, 13)\n\tprev := make([]byte, 13)\n\tfor i := uint64(0); i \u003c (1 \u003c\u003c 12); i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n\tfor i := uint64(1\u003c\u003c34 - 1024); i \u003c (1\u003c\u003c34 + 1024); i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\t// println(string(res))\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n\tfor i := uint64(1\u003c\u003c64 - 5000); i != 0; i++ {\n\t\tres := AppendCompact(i, buf[:0])\n\t\tback, err := Uint64(res)\n\t\ttestEqual(t, \"Uint64(%q) = (%d, %v), want %v\", string(res), back, err, nil)\n\t\ttestEqual(t, \"Uint64(%q) = %d, want %v\", string(res), back, i)\n\n\t\ttestEqual(t, \"bytes.Compare(prev, res) = %d, want %d\", bytes.Compare(prev, res), -1)\n\t\tprev, buf = res, prev\n\t}\n}\n\nfunc BenchmarkCompact(b *testing.B) {\n\tbuf := make([]byte, 13)\n\tfor i := 0; i \u003c b.N; i++ {\n\t\t_ = AppendCompact(uint64(i), buf[:0])\n\t}\n}\n\ntype testpair struct {\n\tdecoded, encoded string\n}\n\nvar pairs = []testpair{\n\t{\"\", \"\"},\n\t{\"f\", \"CR\"},\n\t{\"fo\", \"CSQG\"},\n\t{\"foo\", \"CSQPY\"},\n\t{\"foob\", \"CSQPYRG\"},\n\t{\"fooba\", \"CSQPYRK1\"},\n\t{\"foobar\", \"CSQPYRK1E8\"},\n\n\t{\"sure.\", \"EDTQ4S9E\"},\n\t{\"sure\", \"EDTQ4S8\"},\n\t{\"sur\", \"EDTQ4\"},\n\t{\"su\", \"EDTG\"},\n\t{\"leasure.\", \"DHJP2WVNE9JJW\"},\n\t{\"easure.\", \"CNGQ6XBJCMQ0\"},\n\t{\"asure.\", \"C5SQAWK55R\"},\n}\n\nvar bigtest = testpair{\n\t\"Twas brillig, and the slithy toves\",\n\t\"AHVP2WS0C9S6JV3CD5KJR831DSJ20X38CMG76V39EHM7J83MDXV6AWR\",\n}\n\nfunc testEqual(t *testing.T, msg string, args ...interface{}) bool {\n\tt.Helper()\n\tif args[len(args)-2] != args[len(args)-1] {\n\t\tt.Errorf(msg, args...)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc TestEncode(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tgot := EncodeToString([]byte(p.decoded))\n\t\ttestEqual(t, \"Encode(%q) = %q, want %q\", p.decoded, got, p.encoded)\n\t\tdst := AppendEncode([]byte(\"lead\"), []byte(p.decoded))\n\t\ttestEqual(t, `AppendEncode(\"lead\", %q) = %q, want %q`, p.decoded, string(dst), \"lead\"+p.encoded)\n\t}\n}\n\nfunc TestEncoder(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tbb := \u0026strings.Builder{}\n\t\tencoder := NewEncoder(bb)\n\t\tencoder.Write([]byte(p.decoded))\n\t\tencoder.Close()\n\t\ttestEqual(t, \"Encode(%q) = %q, want %q\", p.decoded, bb.String(), p.encoded)\n\t}\n}\n\nfunc TestEncoderBuffering(t *testing.T) {\n\tinput := []byte(bigtest.decoded)\n\tfor bs := 1; bs \u003c= 12; bs++ {\n\t\tbb := \u0026strings.Builder{}\n\t\tencoder := NewEncoder(bb)\n\t\tfor pos := 0; pos \u003c len(input); pos += bs {\n\t\t\tend := pos + bs\n\t\t\tif end \u003e len(input) {\n\t\t\t\tend = len(input)\n\t\t\t}\n\t\t\tn, err := encoder.Write(input[pos:end])\n\t\t\ttestEqual(t, \"Write(%q) gave error %v, want %v\", input[pos:end], err, error(nil))\n\t\t\ttestEqual(t, \"Write(%q) gave length %v, want %v\", input[pos:end], n, end-pos)\n\t\t}\n\t\terr := encoder.Close()\n\t\ttestEqual(t, \"Close gave error %v, want %v\", err, error(nil))\n\t\ttestEqual(t, \"Encoding/%d of %q = %q, want %q\", bs, bigtest.decoded, bb.String(), bigtest.encoded)\n\t}\n}\n\nfunc TestDecode(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tdbuf := make([]byte, DecodedLen(len(p.encoded)))\n\t\tcount, err := decode(dbuf, []byte(p.encoded))\n\t\ttestEqual(t, \"Decode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, \"Decode(%q) = length %v, want %v\", p.encoded, count, len(p.decoded))\n\t\ttestEqual(t, \"Decode(%q) = %q, want %q\", p.encoded, string(dbuf[0:count]), p.decoded)\n\n\t\tdbuf, err = DecodeString(p.encoded)\n\t\ttestEqual(t, \"DecodeString(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, \"DecodeString(%q) = %q, want %q\", p.encoded, string(dbuf), p.decoded)\n\n\t\t// XXX: https://github.com/gnolang/gno/issues/1570\n\t\tdst, err := AppendDecode(append([]byte(nil), []byte(\"lead\")...), []byte(p.encoded))\n\t\ttestEqual(t, \"AppendDecode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, `AppendDecode(\"lead\", %q) = %q, want %q`, p.encoded, string(dst), \"lead\"+p.decoded)\n\n\t\tdst2, err := AppendDecode(dst[:0:len(p.decoded)], []byte(p.encoded))\n\t\ttestEqual(t, \"AppendDecode(%q) = error %v, want %v\", p.encoded, err, error(nil))\n\t\ttestEqual(t, `AppendDecode(\"\", %q) = %q, want %q`, p.encoded, string(dst2), p.decoded)\n\t\t// XXX: https://github.com/gnolang/gno/issues/1569\n\t\t// old used \u0026dst2[0] != \u0026dst[0] as a check.\n\t\tif len(dst) \u003e 0 \u0026\u0026 len(dst2) \u003e 0 \u0026\u0026 cap(dst2) != len(p.decoded) {\n\t\t\tt.Errorf(\"unexpected capacity growth: got %d, want %d\", cap(dst2), len(p.decoded))\n\t\t}\n\t}\n}\n\n// A minimal variation on strings.Reader.\n// Here, we return a io.EOF immediately on Read if the read has reached the end\n// of the reader. It's used to simplify TestDecoder.\ntype stringReader struct {\n\ts string\n\ti int64\n}\n\nfunc (r *stringReader) Read(b []byte) (n int, err error) {\n\tif r.i \u003e= int64(len(r.s)) {\n\t\treturn 0, io.EOF\n\t}\n\tn = copy(b, r.s[r.i:])\n\tr.i += int64(n)\n\tif r.i \u003e= int64(len(r.s)) {\n\t\treturn n, io.EOF\n\t}\n\treturn\n}\n\nfunc TestDecoder(t *testing.T) {\n\tfor _, p := range pairs {\n\t\tdecoder := NewDecoder(\u0026stringReader{p.encoded, 0})\n\t\tdbuf := make([]byte, DecodedLen(len(p.encoded)))\n\t\tcount, err := decoder.Read(dbuf)\n\t\tif err != nil \u0026\u0026 err != io.EOF {\n\t\t\tt.Fatal(\"Read failed\", err)\n\t\t}\n\t\ttestEqual(t, \"Read from %q = length %v, want %v\", p.encoded, count, len(p.decoded))\n\t\ttestEqual(t, \"Decoding of %q = %q, want %q\", p.encoded, string(dbuf[0:count]), p.decoded)\n\t\tif err != io.EOF {\n\t\t\t_, err = decoder.Read(dbuf)\n\t\t}\n\t\ttestEqual(t, \"Read from %q = %v, want %v\", p.encoded, err, io.EOF)\n\t}\n}\n\ntype badReader struct {\n\tdata []byte\n\terrs []error\n\tcalled int\n\tlimit int\n}\n\n// Populates p with data, returns a count of the bytes written and an\n// error. The error returned is taken from badReader.errs, with each\n// invocation of Read returning the next error in this slice, or io.EOF,\n// if all errors from the slice have already been returned. The\n// number of bytes returned is determined by the size of the input buffer\n// the test passes to decoder.Read and will be a multiple of 8, unless\n// badReader.limit is non zero.\nfunc (b *badReader) Read(p []byte) (int, error) {\n\tlim := len(p)\n\tif b.limit != 0 \u0026\u0026 b.limit \u003c lim {\n\t\tlim = b.limit\n\t}\n\tif len(b.data) \u003c lim {\n\t\tlim = len(b.data)\n\t}\n\tfor i := range p[:lim] {\n\t\tp[i] = b.data[i]\n\t}\n\tb.data = b.data[lim:]\n\terr := io.EOF\n\tif b.called \u003c len(b.errs) {\n\t\terr = b.errs[b.called]\n\t}\n\tb.called++\n\treturn lim, err\n}\n\n// TestIssue20044 tests that decoder.Read behaves correctly when the caller\n// supplied reader returns an error.\nfunc TestIssue20044(t *testing.T) {\n\tbadErr := errors.New(\"bad reader error\")\n\ttestCases := []struct {\n\t\tr badReader\n\t\tres string\n\t\terr error\n\t\tdbuflen int\n\t}{\n\t\t// Check valid input data accompanied by an error is processed and the error is propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"d1jprv3fexqq4v34\"), errs: []error{badErr}},\n\t\t\tres: \"helloworld\", err: badErr,\n\t\t},\n\t\t// Check a read error accompanied by input data consisting of newlines only is propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"\\n\\n\\n\\n\\n\\n\\n\\n\"), errs: []error{badErr, nil}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader will be called twice. The first time it will return 8 newline characters. The\n\t\t// second time valid base32 encoded data and an error. The data should be decoded\n\t\t// correctly and the error should be propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"\\n\\n\\n\\n\\n\\n\\n\\nd1jprv3fexqq4v34\"), errs: []error{nil, badErr}},\n\t\t\tres: \"helloworld\", err: badErr, dbuflen: 8,\n\t\t},\n\t\t// Reader returns invalid input data (too short) and an error. Verify the reader\n\t\t// error is returned.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"c\"), errs: []error{badErr}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader returns invalid input data (too short) but no error. Verify io.ErrUnexpectedEOF\n\t\t// is returned.\n\t\t// NOTE(thehowl): I don't think this should applyto us?\n\t\t/* {\n\t\t\tr: badReader{data: []byte(\"c\"), errs: []error{nil}},\n\t\t\tres: \"\", err: io.ErrUnexpectedEOF,\n\t\t},*/\n\t\t// Reader returns invalid input data and an error. Verify the reader and not the\n\t\t// decoder error is returned.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"cu\"), errs: []error{badErr}},\n\t\t\tres: \"\", err: badErr,\n\t\t},\n\t\t// Reader returns valid data and io.EOF. Check data is decoded and io.EOF is propagated.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"csqpyrk1\"), errs: []error{io.EOF}},\n\t\t\tres: \"fooba\", err: io.EOF,\n\t\t},\n\t\t// Check errors are properly reported when decoder.Read is called multiple times.\n\t\t// decoder.Read will be called 8 times, badReader.Read will be called twice, returning\n\t\t// valid data both times but an error on the second call.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjwc9g\"), errs: []error{nil, badErr}},\n\t\t\tres: \"leasure.10\", err: badErr, dbuflen: 1,\n\t\t},\n\t\t// Check io.EOF is properly reported when decoder.Read is called multiple times.\n\t\t// decoder.Read will be called 8 times, badReader.Read will be called twice, returning\n\t\t// valid data both times but io.EOF on the second call.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{nil, io.EOF}},\n\t\t\tres: \"leasure.\", err: io.EOF, dbuflen: 1,\n\t\t},\n\t\t// The following two test cases check that errors are propagated correctly when more than\n\t\t// 8 bytes are read at a time.\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{io.EOF}},\n\t\t\tres: \"leasure.\", err: io.EOF, dbuflen: 11,\n\t\t},\n\t\t{\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjwc9g\"), errs: []error{badErr}},\n\t\t\tres: \"leasure.10\", err: badErr, dbuflen: 11,\n\t\t},\n\t\t// Check that errors are correctly propagated when the reader returns valid bytes in\n\t\t// groups that are not divisible by 8. The first read will return 11 bytes and no\n\t\t// error. The second will return 7 and an error. The data should be decoded correctly\n\t\t// and the error should be propagated.\n\t\t// NOTE(thehowl): again, this is on the assumption that this is padded, and it's not.\n\t\t/* {\n\t\t\tr: badReader{data: []byte(\"dhjp2wvne9jjw\"), errs: []error{nil, badErr}, limit: 11},\n\t\t\tres: \"leasure.\", err: badErr,\n\t\t}, */\n\t}\n\n\tfor idx, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%d-%s\", idx, string(tc.res)), func(t *testing.T) {\n\t\t\tinput := tc.r.data\n\t\t\tdecoder := NewDecoder(\u0026tc.r)\n\t\t\tvar dbuflen int\n\t\t\tif tc.dbuflen \u003e 0 {\n\t\t\t\tdbuflen = tc.dbuflen\n\t\t\t} else {\n\t\t\t\tdbuflen = DecodedLen(len(input))\n\t\t\t}\n\t\t\tdbuf := make([]byte, dbuflen)\n\t\t\tvar err error\n\t\t\tvar res []byte\n\t\t\tfor err == nil {\n\t\t\t\tvar n int\n\t\t\t\tn, err = decoder.Read(dbuf)\n\t\t\t\tif n \u003e 0 {\n\t\t\t\t\tres = append(res, dbuf[:n]...)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttestEqual(t, \"Decoding of %q = %q, want %q\", string(input), string(res), tc.res)\n\t\t\ttestEqual(t, \"Decoding of %q err = %v, expected %v\", string(input), err, tc.err)\n\t\t})\n\t}\n}\n\n// TestDecoderError verifies decode errors are propagated when there are no read\n// errors.\nfunc TestDecoderError(t *testing.T) {\n\tfor _, readErr := range []error{io.EOF, nil} {\n\t\tinput := \"ucsqpyrk1u\"\n\t\tdbuf := make([]byte, DecodedLen(len(input)))\n\t\tbr := badReader{data: []byte(input), errs: []error{readErr}}\n\t\tdecoder := NewDecoder(\u0026br)\n\t\tn, err := decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\tif _, ok := err.(CorruptInputError); !ok {\n\t\t\tt.Errorf(\"Corrupt input error expected. Found %T\", err)\n\t\t}\n\t}\n}\n\n// TestReaderEOF ensures decoder.Read behaves correctly when input data is\n// exhausted.\nfunc TestReaderEOF(t *testing.T) {\n\tfor _, readErr := range []error{io.EOF, nil} {\n\t\tinput := \"MZXW6YTB\"\n\t\tbr := badReader{data: []byte(input), errs: []error{nil, readErr}}\n\t\tdecoder := NewDecoder(\u0026br)\n\t\tdbuf := make([]byte, DecodedLen(len(input)))\n\t\tn, err := decoder.Read(dbuf)\n\t\ttestEqual(t, \"Decoding of %q err = %v, expected %v\", input, err, error(nil))\n\t\tn, err = decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\ttestEqual(t, \"Read after EOF, err = %v, expected %v\", err, io.EOF)\n\t\tn, err = decoder.Read(dbuf)\n\t\ttestEqual(t, \"Read after EOF, n = %d, expected %d\", n, 0)\n\t\ttestEqual(t, \"Read after EOF, err = %v, expected %v\", err, io.EOF)\n\t}\n}\n\nfunc TestDecoderBuffering(t *testing.T) {\n\tfor bs := 1; bs \u003c= 12; bs++ {\n\t\tdecoder := NewDecoder(strings.NewReader(bigtest.encoded))\n\t\tbuf := make([]byte, len(bigtest.decoded)+12)\n\t\tvar total int\n\t\tvar n int\n\t\tvar err error\n\t\tfor total = 0; total \u003c len(bigtest.decoded) \u0026\u0026 err == nil; {\n\t\t\tn, err = decoder.Read(buf[total : total+bs])\n\t\t\ttotal += n\n\t\t}\n\t\tif err != nil \u0026\u0026 err != io.EOF {\n\t\t\tt.Errorf(\"Read from %q at pos %d = %d, unexpected error %v\", bigtest.encoded, total, n, err)\n\t\t}\n\t\ttestEqual(t, \"Decoding/%d of %q = %q, want %q\", bs, bigtest.encoded, string(buf[0:total]), bigtest.decoded)\n\t}\n}\n\nfunc TestDecodeCorrupt(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput string\n\t\toffset int // -1 means no corruption.\n\t}{\n\t\t{\"\", -1},\n\t\t{\"iIoOlL\", -1},\n\t\t{\"!!!!\", 0},\n\t\t{\"uxp10\", 0},\n\t\t{\"x===\", 1},\n\t\t{\"AA=A====\", 2},\n\t\t{\"AAA=AAAA\", 3},\n\t\t// Much fewer cases compared to Go as there are much fewer cases where input\n\t\t// can be \"corrupted\".\n\t}\n\tfor _, tc := range testCases {\n\t\tdbuf := make([]byte, DecodedLen(len(tc.input)))\n\t\t_, err := Decode(dbuf, []byte(tc.input))\n\t\tif tc.offset == -1 {\n\t\t\tif err != nil {\n\t\t\t\tt.Error(\"Decoder wrongly detected corruption in\", tc.input)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch err := err.(type) {\n\t\tcase CorruptInputError:\n\t\t\ttestEqual(t, \"Corruption in %q at offset %v, want %v\", tc.input, int(err), tc.offset)\n\t\tdefault:\n\t\t\tt.Error(\"Decoder failed to detect corruption in\", tc)\n\t\t}\n\t}\n}\n\nfunc TestBig(t *testing.T) {\n\tn := 3*1000 + 1\n\traw := make([]byte, n)\n\tconst alpha = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\tfor i := 0; i \u003c n; i++ {\n\t\traw[i] = alpha[i%len(alpha)]\n\t}\n\tencoded := new(bytes.Buffer)\n\tw := NewEncoder(encoded)\n\tnn, err := w.Write(raw)\n\tif nn != n || err != nil {\n\t\tt.Fatalf(\"Encoder.Write(raw) = %d, %v want %d, nil\", nn, err, n)\n\t}\n\terr = w.Close()\n\tif err != nil {\n\t\tt.Fatalf(\"Encoder.Close() = %v want nil\", err)\n\t}\n\tdecoded, err := io.ReadAll(NewDecoder(encoded))\n\tif err != nil {\n\t\tt.Fatalf(\"io.ReadAll(NewDecoder(...)): %v\", err)\n\t}\n\n\tif !bytes.Equal(raw, decoded) {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(decoded) \u0026\u0026 i \u003c len(raw); i++ {\n\t\t\tif decoded[i] != raw[i] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tt.Errorf(\"Decode(Encode(%d-byte string)) failed at offset %d\", n, i)\n\t}\n}\n\nfunc testStringEncoding(t *testing.T, expected string, examples []string) {\n\tfor _, e := range examples {\n\t\tbuf, err := DecodeString(e)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Decode(%q) failed: %v\", e, err)\n\t\t\tcontinue\n\t\t}\n\t\tif s := string(buf); s != expected {\n\t\t\tt.Errorf(\"Decode(%q) = %q, want %q\", e, s, expected)\n\t\t}\n\t}\n}\n\nfunc TestNewLineCharacters(t *testing.T) {\n\t// Each of these should decode to the string \"sure\", without errors.\n\texamples := []string{\n\t\t\"EDTQ4S8\",\n\t\t\"EDTQ4S8\\r\",\n\t\t\"EDTQ4S8\\n\",\n\t\t\"EDTQ4S8\\r\\n\",\n\t\t\"EDTQ4S\\r\\n8\",\n\t\t\"EDT\\rQ4S\\n8\",\n\t\t\"edt\\nq4s\\r8\",\n\t\t\"edt\\nq4s8\",\n\t\t\"EDTQ4S\\n8\",\n\t}\n\ttestStringEncoding(t, \"sure\", examples)\n}\n\nfunc BenchmarkEncode(b *testing.B) {\n\tdata := make([]byte, 8192)\n\tbuf := make([]byte, EncodedLen(len(data)))\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tEncode(buf, data)\n\t}\n}\n\nfunc BenchmarkEncodeToString(b *testing.B) {\n\tdata := make([]byte, 8192)\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tEncodeToString(data)\n\t}\n}\n\nfunc BenchmarkDecode(b *testing.B) {\n\tdata := make([]byte, EncodedLen(8192))\n\tEncode(data, make([]byte, 8192))\n\tbuf := make([]byte, 8192)\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tDecode(buf, data)\n\t}\n}\n\nfunc BenchmarkDecodeString(b *testing.B) {\n\tdata := EncodeToString(make([]byte, 8192))\n\tb.SetBytes(int64(len(data)))\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tDecodeString(data)\n\t}\n}\n\n/* TODO: rewrite without using goroutines\nfunc TestBufferedDecodingSameError(t *testing.T) {\n\ttestcases := []struct {\n\t\tprefix string\n\t\tchunkCombinations [][]string\n\t\texpected error\n\t}{\n\t\t// Normal case, this is valid input\n\t\t{\"helloworld\", [][]string{\n\t\t\t{\"D1JP\", \"RV3F\", \"EXQQ\", \"4V34\"},\n\t\t\t{\"D1JPRV3FEXQQ4V34\"},\n\t\t\t{\"D1J\", \"PRV\", \"3FE\", \"XQQ\", \"4V3\", \"4\"},\n\t\t\t{\"D1JPRV3FEXQQ4V\", \"34\"},\n\t\t}, nil},\n\n\t\t// Normal case, this is valid input\n\t\t{\"fooba\", [][]string{\n\t\t\t{\"CSQPYRK1\"},\n\t\t\t{\"CSQPYRK\", \"1\"},\n\t\t\t{\"CSQPYR\", \"K1\"},\n\t\t\t{\"CSQPY\", \"RK1\"},\n\t\t\t{\"CSQPY\", \"RK\", \"1\"},\n\t\t\t{\"CSQPY\", \"RK1\"},\n\t\t\t{\"CSQP\", \"YR\", \"K1\"},\n\t\t}, nil},\n\n\t\t// NOTE: many test cases have been removed as we don't return ErrUnexpectedEOF.\n\t}\n\n\tfor _, testcase := range testcases {\n\t\tfor _, chunks := range testcase.chunkCombinations {\n\t\t\tpr, pw := io.Pipe()\n\n\t\t\t// Write the encoded chunks into the pipe\n\t\t\tgo func() {\n\t\t\t\tfor _, chunk := range chunks {\n\t\t\t\t\tpw.Write([]byte(chunk))\n\t\t\t\t}\n\t\t\t\tpw.Close()\n\t\t\t}()\n\n\t\t\tdecoder := NewDecoder(pr)\n\t\t\tback, err := io.ReadAll(decoder)\n\n\t\t\tif err != testcase.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v; case %s %+v\", testcase.expected, err, testcase.prefix, chunks)\n\t\t\t}\n\t\t\tif testcase.expected == nil {\n\t\t\t\ttestEqual(t, \"Decode from NewDecoder(chunkReader(%v)) = %q, want %q\", chunks, string(back), testcase.prefix)\n\t\t\t}\n\t\t}\n\t}\n}\n*/\n\nfunc TestEncodedLen(t *testing.T) {\n\ttype test struct {\n\t\tn int\n\t\twant int64\n\t}\n\ttests := []test{\n\t\t{0, 0},\n\t\t{1, 2},\n\t\t{2, 4},\n\t\t{3, 5},\n\t\t{4, 7},\n\t\t{5, 8},\n\t\t{6, 10},\n\t\t{7, 12},\n\t\t{10, 16},\n\t\t{11, 18},\n\t}\n\t// check overflow\n\ttests = append(tests, test{(math.MaxInt-4)/8 + 1, 1844674407370955162})\n\ttests = append(tests, test{math.MaxInt/8*5 + 4, math.MaxInt})\n\tfor _, tt := range tests {\n\t\tif got := EncodedLen(tt.n); int64(got) != tt.want {\n\t\t\tt.Errorf(\"EncodedLen(%d): got %d, want %d\", tt.n, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestDecodedLen(t *testing.T) {\n\ttype test struct {\n\t\tn int\n\t\twant int64\n\t}\n\ttests := []test{\n\t\t{0, 0},\n\t\t{2, 1},\n\t\t{4, 2},\n\t\t{5, 3},\n\t\t{7, 4},\n\t\t{8, 5},\n\t\t{10, 6},\n\t\t{12, 7},\n\t\t{16, 10},\n\t\t{18, 11},\n\t}\n\t// check overflow\n\ttests = append(tests, test{math.MaxInt/5 + 1, 1152921504606846976})\n\ttests = append(tests, test{math.MaxInt, 5764607523034234879})\n\tfor _, tt := range tests {\n\t\tif got := DecodedLen(tt.n); int64(got) != tt.want {\n\t\t\tt.Errorf(\"DecodedLen(%d): got %d, want %d\", tt.n, got, tt.want)\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"combinederr","path":"gno.land/p/demo/combinederr","files":[{"name":"combinederr.gno","body":"package combinederr\n\nimport \"strings\"\n\n// CombinedError is a combined execution error\ntype CombinedError struct {\n\terrors []error\n}\n\n// Error returns the combined execution error\nfunc (e *CombinedError) Error() string {\n\tif len(e.errors) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tfor _, err := range e.errors {\n\t\tsb.WriteString(err.Error() + \"; \")\n\t}\n\n\t// Remove the last semicolon and space\n\tresult := sb.String()\n\n\treturn result[:len(result)-2]\n}\n\n// Add adds a new error to the execution error\nfunc (e *CombinedError) Add(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\te.errors = append(e.errors, err)\n}\n\n// Size returns a\nfunc (e *CombinedError) Size() int {\n\treturn len(e.errors)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"context","path":"gno.land/p/demo/context","files":[{"name":"context.gno","body":"// Package context provides a minimal implementation of Go context with support\n// for Value and WithValue.\n//\n// Adapted from https://github.com/golang/go/tree/master/src/context/.\n// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\npackage context\n\ntype Context interface {\n\t// Value returns the value associated with this context for key, or nil\n\t// if no value is associated with key.\n\tValue(key interface{}) interface{}\n}\n\n// Empty returns a non-nil, empty context, similar with context.Background and\n// context.TODO in Go.\nfunc Empty() Context {\n\treturn \u0026emptyCtx{}\n}\n\ntype emptyCtx struct{}\n\nfunc (ctx emptyCtx) Value(key interface{}) interface{} {\n\treturn nil\n}\n\nfunc (ctx emptyCtx) String() string {\n\treturn \"context.Empty\"\n}\n\ntype valueCtx struct {\n\tparent Context\n\tkey, val interface{}\n}\n\nfunc (ctx *valueCtx) Value(key interface{}) interface{} {\n\tif ctx.key == key {\n\t\treturn ctx.val\n\t}\n\treturn ctx.parent.Value(key)\n}\n\nfunc stringify(v interface{}) string {\n\tswitch s := v.(type) {\n\tcase stringer:\n\t\treturn s.String()\n\tcase string:\n\t\treturn s\n\t}\n\treturn \"non-stringer\"\n}\n\ntype stringer interface {\n\tString() string\n}\n\nfunc (c *valueCtx) String() string {\n\treturn stringify(c.parent) + \".WithValue(\" +\n\t\tstringify(c.key) + \", \" +\n\t\tstringify(c.val) + \")\"\n}\n\n// WithValue returns a copy of parent in which the value associated with key is\n// val.\nfunc WithValue(parent Context, key, val interface{}) Context {\n\tif key == nil {\n\t\tpanic(\"nil key\")\n\t}\n\t// XXX: if !reflect.TypeOf(key).Comparable() { panic(\"key is not comparable\") }\n\treturn \u0026valueCtx{parent, key, val}\n}\n"},{"name":"context_test.gno","body":"package context\n\nimport \"testing\"\n\nfunc TestContextExample(t *testing.T) {\n\ttype favContextKey string\n\n\tk := favContextKey(\"language\")\n\tctx := WithValue(Empty(), k, \"Gno\")\n\n\tif v := ctx.Value(k); v != nil {\n\t\tif string(v) != \"Gno\" {\n\t\t\tt.Errorf(\"language value should be Gno, but is %s\", v)\n\t\t}\n\t} else {\n\t\tt.Errorf(\"language key value was not found\")\n\t}\n\n\tif v := ctx.Value(favContextKey(\"color\")); v != nil {\n\t\tt.Errorf(\"color key was found\")\n\t}\n}\n\n// otherContext is a Context that's not one of the types defined in context.go.\n// This lets us test code paths that differ based on the underlying type of the\n// Context.\ntype otherContext struct {\n\tContext\n}\n\ntype (\n\tkey1 int\n\tkey2 int\n)\n\n// func (k key2) String() string { return fmt.Sprintf(\"%[1]T(%[1]d)\", k) }\n\nvar (\n\tk1 = key1(1)\n\tk2 = key2(1) // same int as k1, different type\n\tk3 = key2(3) // same type as k2, different int\n)\n\nfunc TestValues(t *testing.T) {\n\tcheck := func(c Context, nm, v1, v2, v3 string) {\n\t\tif v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {\n\t\t\tt.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {\n\t\t\tt.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {\n\t\t\tt.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)\n\t\t}\n\t}\n\n\tc0 := Empty()\n\tcheck(c0, \"c0\", \"\", \"\", \"\")\n\n\tt.Skip() // XXX: depends on https://github.com/gnolang/gno/issues/2386\n\n\tc1 := WithValue(Empty(), k1, \"c1k1\")\n\tcheck(c1, \"c1\", \"c1k1\", \"\", \"\")\n\n\t/*if got, want := c1.String(), `context.Empty.WithValue(context_test.key1, c1k1)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc2 := WithValue(c1, k2, \"c2k2\")\n\tcheck(c2, \"c2\", \"c1k1\", \"c2k2\", \"\")\n\n\t/*if got, want := fmt.Sprint(c2), `context.Empty.WithValue(context_test.key1, c1k1).WithValue(context_test.key2(1), c2k2)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc3 := WithValue(c2, k3, \"c3k3\")\n\tcheck(c3, \"c2\", \"c1k1\", \"c2k2\", \"c3k3\")\n\n\tc4 := WithValue(c3, k1, nil)\n\tcheck(c4, \"c4\", \"\", \"c2k2\", \"c3k3\")\n\n\to0 := otherContext{Empty()}\n\tcheck(o0, \"o0\", \"\", \"\", \"\")\n\n\to1 := otherContext{WithValue(Empty(), k1, \"c1k1\")}\n\tcheck(o1, \"o1\", \"c1k1\", \"\", \"\")\n\n\to2 := WithValue(o1, k2, \"o2k2\")\n\tcheck(o2, \"o2\", \"c1k1\", \"o2k2\", \"\")\n\n\to3 := otherContext{c4}\n\tcheck(o3, \"o3\", \"\", \"c2k2\", \"c3k3\")\n\n\to4 := WithValue(o3, k3, nil)\n\tcheck(o4, \"o4\", \"\", \"c2k2\", \"\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"dao","path":"gno.land/p/demo/dao","files":[{"name":"dao.gno","body":"package dao\n\nconst (\n\tProposalAddedEvent = \"ProposalAdded\" // emitted when a new proposal has been added\n\tProposalAcceptedEvent = \"ProposalAccepted\" // emitted when a proposal has been accepted\n\tProposalNotAcceptedEvent = \"ProposalNotAccepted\" // emitted when a proposal has not been accepted\n\tProposalExecutedEvent = \"ProposalExecuted\" // emitted when a proposal has been executed\n\n\tProposalEventIDKey = \"proposal-id\"\n\tProposalEventAuthorKey = \"proposal-author\"\n\tProposalEventExecutionKey = \"exec-status\"\n)\n\n// ProposalRequest is a single govdao proposal request\n// that contains the necessary information to\n// log and generate a valid proposal\ntype ProposalRequest struct {\n\tDescription string // the description associated with the proposal\n\tExecutor Executor // the proposal executor\n}\n\n// DAO defines the DAO abstraction\ntype DAO interface {\n\t// PropStore is the DAO proposal storage\n\tPropStore\n\n\t// Propose adds a new proposal to the executor-based GOVDAO.\n\t// Returns the generated proposal ID\n\tPropose(request ProposalRequest) (uint64, error)\n\n\t// ExecuteProposal executes the proposal with the given ID\n\tExecuteProposal(id uint64) error\n}\n"},{"name":"doc.gno","body":"// Package dao houses common DAO building blocks (framework), which can be used or adopted by any\n// specific DAO implementation. By design, the DAO should house the proposals it receives, but not the actual\n// DAO members or proposal votes. These abstractions should be implemented by a separate entity, to keep the DAO\n// agnostic of implementation details such as these (member / vote management).\npackage dao\n"},{"name":"events.gno","body":"package dao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// EmitProposalAdded emits an event signaling that\n// a given proposal was added\nfunc EmitProposalAdded(id uint64, proposer std.Address) {\n\tstd.Emit(\n\t\tProposalAddedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tProposalEventAuthorKey, proposer.String(),\n\t)\n}\n\n// EmitProposalAccepted emits an event signaling that\n// a given proposal was accepted\nfunc EmitProposalAccepted(id uint64) {\n\tstd.Emit(\n\t\tProposalAcceptedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t)\n}\n\n// EmitProposalNotAccepted emits an event signaling that\n// a given proposal was not accepted\nfunc EmitProposalNotAccepted(id uint64) {\n\tstd.Emit(\n\t\tProposalNotAcceptedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t)\n}\n\n// EmitProposalExecuted emits an event signaling that\n// a given proposal was executed, with the given status\nfunc EmitProposalExecuted(id uint64, status ProposalStatus) {\n\tstd.Emit(\n\t\tProposalExecutedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tProposalEventExecutionKey, status.String(),\n\t)\n}\n\n// EmitVoteAdded emits an event signaling that\n// a vote was cast for a given proposal\nfunc EmitVoteAdded(id uint64, voter std.Address, option VoteOption) {\n\tstd.Emit(\n\t\tVoteAddedEvent,\n\t\tVoteAddedIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tVoteAddedAuthorKey, voter.String(),\n\t\tVoteAddedOptionKey, option.String(),\n\t)\n}\n"},{"name":"executor.gno","body":"package dao\n\n// Executor represents a minimal closure-oriented proposal design.\n// It is intended to be used by a govdao governance proposal (v1, v2, etc)\ntype Executor interface {\n\t// Execute executes the given proposal, and returns any error encountered\n\t// during the execution\n\tExecute() error\n}\n"},{"name":"proposals.gno","body":"package dao\n\nimport \"std\"\n\n// ProposalStatus is the currently active proposal status,\n// changed based on DAO functionality.\n// Status transitions:\n//\n// ACTIVE -\u003e ACCEPTED -\u003e EXECUTION(SUCCEEDED/FAILED)\n//\n// ACTIVE -\u003e NOT ACCEPTED\ntype ProposalStatus string\n\nvar (\n\tActive ProposalStatus = \"active\" // proposal is still active\n\tAccepted ProposalStatus = \"accepted\" // proposal gathered quorum\n\tNotAccepted ProposalStatus = \"not accepted\" // proposal failed to gather quorum\n\tExecutionSuccessful ProposalStatus = \"execution successful\" // proposal is executed successfully\n\tExecutionFailed ProposalStatus = \"execution failed\" // proposal is failed during execution\n)\n\nfunc (s ProposalStatus) String() string {\n\treturn string(s)\n}\n\n// PropStore defines the proposal storage abstraction\ntype PropStore interface {\n\t// Proposals returns the given paginated proposals\n\tProposals(offset, count uint64) []Proposal\n\n\t// ProposalByID returns the proposal associated with\n\t// the given ID, if any\n\tProposalByID(id uint64) (Proposal, error)\n\n\t// Size returns the number of proposals in\n\t// the proposal store\n\tSize() int\n}\n\n// Proposal is the single proposal abstraction\ntype Proposal interface {\n\t// Author returns the author of the proposal\n\tAuthor() std.Address\n\n\t// Description returns the description of the proposal\n\tDescription() string\n\n\t// Status returns the status of the proposal\n\tStatus() ProposalStatus\n\n\t// Executor returns the proposal executor\n\tExecutor() Executor\n\n\t// Stats returns the voting stats of the proposal\n\tStats() Stats\n\n\t// IsExpired returns a flag indicating if the proposal expired\n\tIsExpired() bool\n\n\t// Render renders the proposal in a readable format\n\tRender() string\n}\n"},{"name":"vote.gno","body":"package dao\n\n// NOTE:\n// This voting pods will be removed in a future version of the\n// p/demo/dao package. A DAO shouldn't have to comply with or define how the voting mechanism works internally;\n// it should be viewed as an entity that makes decisions\n//\n// The extent of \"votes being enforced\" in this implementation is just in the context\n// of types a DAO can use (import), and in the context of \"Stats\", where\n// there is a notion of \"Yay\", \"Nay\" and \"Abstain\" votes.\nconst (\n\tVoteAddedEvent = \"VoteAdded\" // emitted when a vote was cast for a proposal\n\n\tVoteAddedIDKey = \"proposal-id\"\n\tVoteAddedAuthorKey = \"author\"\n\tVoteAddedOptionKey = \"option\"\n)\n\n// VoteOption is the limited voting option for a DAO proposal\ntype VoteOption string\n\nconst (\n\tYesVote VoteOption = \"YES\" // Proposal should be accepted\n\tNoVote VoteOption = \"NO\" // Proposal should be rejected\n\tAbstainVote VoteOption = \"ABSTAIN\" // Side is not chosen\n)\n\nfunc (v VoteOption) String() string {\n\treturn string(v)\n}\n\n// Stats encompasses the proposal voting stats\ntype Stats struct {\n\tYayVotes uint64\n\tNayVotes uint64\n\tAbstainVotes uint64\n\n\tTotalVotingPower uint64\n}\n\n// YayPercent returns the percentage (0-100) of the yay votes\n// in relation to the total voting power\nfunc (v Stats) YayPercent() uint64 {\n\treturn v.YayVotes * 100 / v.TotalVotingPower\n}\n\n// NayPercent returns the percentage (0-100) of the nay votes\n// in relation to the total voting power\nfunc (v Stats) NayPercent() uint64 {\n\treturn v.NayVotes * 100 / v.TotalVotingPower\n}\n\n// AbstainPercent returns the percentage (0-100) of the abstain votes\n// in relation to the total voting power\nfunc (v Stats) AbstainPercent() uint64 {\n\treturn v.AbstainVotes * 100 / v.TotalVotingPower\n}\n\n// MissingVotes returns the summed voting power that has not\n// participated in proposal voting yet\nfunc (v Stats) MissingVotes() uint64 {\n\treturn v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes)\n}\n\n// MissingVotesPercent returns the percentage (0-100) of the missing votes\n// in relation to the total voting power\nfunc (v Stats) MissingVotesPercent() uint64 {\n\treturn v.MissingVotes() * 100 / v.TotalVotingPower\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"dom","path":"gno.land/p/demo/dom","files":[{"name":"dom.gno","body":"// XXX This is only used for testing in ./tests.\n// Otherwise this package is deprecated.\n// TODO: replace with a package that is supported, and delete this.\n\npackage dom\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Plot struct {\n\tName string\n\tPosts avl.Tree // postsCtr -\u003e *Post\n\tPostsCtr int\n}\n\nfunc (plot *Plot) AddPost(title string, body string) {\n\tctr := plot.PostsCtr\n\tplot.PostsCtr++\n\tkey := strconv.Itoa(ctr)\n\tpost := \u0026Post{\n\t\tTitle: title,\n\t\tBody: body,\n\t}\n\tplot.Posts.Set(key, post)\n}\n\nfunc (plot *Plot) String() string {\n\tstr := \"# [plot] \" + plot.Name + \"\\n\"\n\tif plot.Posts.Size() \u003e 0 {\n\t\tplot.Posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Post).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Post struct {\n\tTitle string\n\tBody string\n\tComments avl.Tree\n}\n\nfunc (post *Post) String() string {\n\tstr := \"## \" + post.Title + \"\\n\"\n\tstr += \"\"\n\tstr += post.Body\n\tif post.Comments.Size() \u003e 0 {\n\t\tpost.Comments.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Comment).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Comment struct {\n\tCreator string\n\tBody string\n}\n\nfunc (cmm Comment) String() string {\n\treturn cmm.Body + \" - @\" + cmm.Creator + \"\\n\"\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"entropy","path":"gno.land/p/demo/entropy","files":[{"name":"entropy.gno","body":"// Entropy generates fully deterministic, cost-effective, and hard to guess\n// numbers.\n//\n// It is designed both for single-usage, like seeding math/rand or for being\n// reused which increases the entropy and its cost effectiveness.\n//\n// Disclaimer: this package is unsafe and won't prevent others to guess values\n// in advance.\n//\n// It uses the Bernstein's hash djb2 to be CPU-cycle efficient.\npackage entropy\n\nimport (\n\t\"math\"\n\t\"std\"\n\t\"time\"\n)\n\ntype Instance struct {\n\tvalue uint32\n}\n\nfunc New() *Instance {\n\tr := Instance{value: 5381}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc FromSeed(seed uint32) *Instance {\n\tr := Instance{value: seed}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc (i *Instance) Seed() uint32 {\n\treturn i.value\n}\n\nfunc (i *Instance) djb2String(input string) {\n\tfor _, c := range input {\n\t\ti.djb2Uint32(uint32(c))\n\t}\n}\n\n// super fast random algorithm.\n// http://www.cse.yorku.ca/~oz/hash.html\nfunc (i *Instance) djb2Uint32(input uint32) {\n\ti.value = (i.value \u003c\u003c 5) + i.value + input\n}\n\n// AddEntropy uses various runtime variables to add entropy to the existing seed.\nfunc (i *Instance) addEntropy() {\n\t// FIXME: reapply the 5381 initial value?\n\n\t// inherit previous entropy\n\t// nothing to do\n\n\t// handle callers\n\t{\n\t\tcaller1 := std.GetCallerAt(1).String()\n\t\ti.djb2String(caller1)\n\t\tcaller2 := std.GetCallerAt(2).String()\n\t\ti.djb2String(caller2)\n\t}\n\n\t// height\n\t{\n\t\theight := std.GetHeight()\n\t\tif height \u003e= math.MaxUint32 {\n\t\t\theight -= math.MaxUint32\n\t\t}\n\t\ti.djb2Uint32(uint32(height))\n\t}\n\n\t// time\n\t{\n\t\tsecs := time.Now().Second()\n\t\ti.djb2Uint32(uint32(secs))\n\t\tnsecs := time.Now().Nanosecond()\n\t\ti.djb2Uint32(uint32(nsecs))\n\t}\n\n\t// FIXME: compute other hard-to-guess but deterministic variables, like real gas?\n}\n\nfunc (i *Instance) Value() uint32 {\n\ti.addEntropy()\n\treturn i.value\n}\n"},{"name":"entropy_test.gno","body":"package entropy\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestInstance(t *testing.T) {\n\tinstance := New()\n\tif instance == nil {\n\t\tt.Errorf(\"instance should not be nil\")\n\t}\n}\n\nfunc TestInstanceValue(t *testing.T) {\n\tbaseEntropy := New()\n\tbaseResult := computeValue(t, baseEntropy)\n\n\tsameHeightEntropy := New()\n\tsameHeightResult := computeValue(t, sameHeightEntropy)\n\n\tif baseResult != sameHeightResult {\n\t\tt.Errorf(\"should have the same result: new=%s, base=%s\", sameHeightResult, baseResult)\n\t}\n\n\tstd.TestSkipHeights(1)\n\tdifferentHeightEntropy := New()\n\tdifferentHeightResult := computeValue(t, differentHeightEntropy)\n\n\tif baseResult == differentHeightResult {\n\t\tt.Errorf(\"should have different result: new=%s, base=%s\", differentHeightResult, baseResult)\n\t}\n}\n\nfunc computeValue(t *testing.T, r *Instance) string {\n\tt.Helper()\n\n\tout := \"\"\n\tfor i := 0; i \u003c 10; i++ {\n\t\tval := int(r.Value())\n\t\tout += strconv.Itoa(val) + \" \"\n\t}\n\n\treturn out\n}\n"},{"name":"z_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/entropy\"\n)\n\nfunc main() {\n\t// initial\n\tprintln(\"---\")\n\tr := entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\n\t// should be the same\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\n\tstd.TestSkipHeights(1)\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n}\n\n// Output:\n// ---\n// 4129293727\n// 2141104956\n// 1950222777\n// 3348280598\n// 438354259\n// ---\n// 4129293727\n// 2141104956\n// 1950222777\n// 3348280598\n// 438354259\n// ---\n// 49506731\n// 1539580078\n// 2695928529\n// 1895482388\n// 3462727799\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"flow","path":"gno.land/p/demo/flow","files":[{"name":"LICENSE","body":"https://github.com/mxk/go-flowrate/blob/master/LICENSE\nBSD 3-Clause \"New\" or \"Revised\" License\n\nCopyright (c) 2014 The Go-FlowRate Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the\n distribution.\n\n * Neither the name of the go-flowrate project nor the names of its\n contributors may be used to endorse or promote products derived\n from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"},{"name":"README.md","body":"Data Flow Rate Control\n======================\n\nTo download and install this package run:\n\ngo get github.com/mxk/go-flowrate/flowrate\n\nThe documentation is available at:\n\nhttp://godoc.org/github.com/mxk/go-flowrate/flowrate\n"},{"name":"flow.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n// XXX modified to disable blocking, time.Sleep().\n\n// Package flow provides the tools for monitoring and limiting the flow rate\n// of an arbitrary data stream.\npackage flow\n\nimport (\n\t\"math\"\n\t// \"sync\"\n\t\"time\"\n)\n\n// Monitor monitors and limits the transfer rate of a data stream.\ntype Monitor struct {\n\t// mu sync.Mutex // Mutex guarding access to all internal fields\n\tactive bool // Flag indicating an active transfer\n\tstart time.Duration // Transfer start time (clock() value)\n\tbytes int64 // Total number of bytes transferred\n\tsamples int64 // Total number of samples taken\n\n\trSample float64 // Most recent transfer rate sample (bytes per second)\n\trEMA float64 // Exponential moving average of rSample\n\trPeak float64 // Peak transfer rate (max of all rSamples)\n\trWindow float64 // rEMA window (seconds)\n\n\tsBytes int64 // Number of bytes transferred since sLast\n\tsLast time.Duration // Most recent sample time (stop time when inactive)\n\tsRate time.Duration // Sampling rate\n\n\ttBytes int64 // Number of bytes expected in the current transfer\n\ttLast time.Duration // Time of the most recent transfer of at least 1 byte\n}\n\n// New creates a new flow control monitor. Instantaneous transfer rate is\n// measured and updated for each sampleRate interval. windowSize determines the\n// weight of each sample in the exponential moving average (EMA) calculation.\n// The exact formulas are:\n//\n//\tsampleTime = currentTime - prevSampleTime\n//\tsampleRate = byteCount / sampleTime\n//\tweight = 1 - exp(-sampleTime/windowSize)\n//\tnewRate = weight*sampleRate + (1-weight)*oldRate\n//\n// The default values for sampleRate and windowSize (if \u003c= 0) are 100ms and 1s,\n// respectively.\nfunc New(sampleRate, windowSize time.Duration) *Monitor {\n\tif sampleRate = clockRound(sampleRate); sampleRate \u003c= 0 {\n\t\tsampleRate = 5 * clockRate\n\t}\n\tif windowSize \u003c= 0 {\n\t\twindowSize = 1 * time.Second\n\t}\n\tnow := clock()\n\treturn \u0026Monitor{\n\t\tactive: true,\n\t\tstart: now,\n\t\trWindow: windowSize.Seconds(),\n\t\tsLast: now,\n\t\tsRate: sampleRate,\n\t\ttLast: now,\n\t}\n}\n\n// Update records the transfer of n bytes and returns n. It should be called\n// after each Read/Write operation, even if n is 0.\nfunc (m *Monitor) Update(n int) int {\n\t// m.mu.Lock()\n\tm.update(n)\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// Hack to set the current rEMA.\nfunc (m *Monitor) SetREMA(rEMA float64) {\n\t// m.mu.Lock()\n\tm.rEMA = rEMA\n\tm.samples++\n\t// m.mu.Unlock()\n}\n\n// IO is a convenience method intended to wrap io.Reader and io.Writer method\n// execution. It calls m.Update(n) and then returns (n, err) unmodified.\nfunc (m *Monitor) IO(n int, err error) (int, error) {\n\treturn m.Update(n), err\n}\n\n// Done marks the transfer as finished and prevents any further updates or\n// limiting. Instantaneous and current transfer rates drop to 0. Update, IO, and\n// Limit methods become NOOPs. It returns the total number of bytes transferred.\nfunc (m *Monitor) Done() int64 {\n\t// m.mu.Lock()\n\tif now := m.update(0); m.sBytes \u003e 0 {\n\t\tm.reset(now)\n\t}\n\tm.active = false\n\tm.tLast = 0\n\tn := m.bytes\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// timeRemLimit is the maximum Status.TimeRem value.\nconst timeRemLimit = 999*time.Hour + 59*time.Minute + 59*time.Second\n\n// Status represents the current Monitor status. All transfer rates are in bytes\n// per second rounded to the nearest byte.\ntype Status struct {\n\tActive bool // Flag indicating an active transfer\n\tStart time.Time // Transfer start time\n\tDuration time.Duration // Time period covered by the statistics\n\tIdle time.Duration // Time since the last transfer of at least 1 byte\n\tBytes int64 // Total number of bytes transferred\n\tSamples int64 // Total number of samples taken\n\tInstRate int64 // Instantaneous transfer rate\n\tCurRate int64 // Current transfer rate (EMA of InstRate)\n\tAvgRate int64 // Average transfer rate (Bytes / Duration)\n\tPeakRate int64 // Maximum instantaneous transfer rate\n\tBytesRem int64 // Number of bytes remaining in the transfer\n\tTimeRem time.Duration // Estimated time to completion\n\tProgress Percent // Overall transfer progress\n}\n\nfunc (s Status) String() string {\n\treturn \"STATUS{}\"\n}\n\n// Status returns current transfer status information. The returned value\n// becomes static after a call to Done.\nfunc (m *Monitor) Status() Status {\n\t// m.mu.Lock()\n\tnow := m.update(0)\n\ts := Status{\n\t\tActive: m.active,\n\t\tStart: clockToTime(m.start),\n\t\tDuration: m.sLast - m.start,\n\t\tIdle: now - m.tLast,\n\t\tBytes: m.bytes,\n\t\tSamples: m.samples,\n\t\tPeakRate: round(m.rPeak),\n\t\tBytesRem: m.tBytes - m.bytes,\n\t\tProgress: percentOf(float64(m.bytes), float64(m.tBytes)),\n\t}\n\tif s.BytesRem \u003c 0 {\n\t\ts.BytesRem = 0\n\t}\n\tif s.Duration \u003e 0 {\n\t\trAvg := float64(s.Bytes) / s.Duration.Seconds()\n\t\ts.AvgRate = round(rAvg)\n\t\tif s.Active {\n\t\t\ts.InstRate = round(m.rSample)\n\t\t\ts.CurRate = round(m.rEMA)\n\t\t\tif s.BytesRem \u003e 0 {\n\t\t\t\tif tRate := 0.8*m.rEMA + 0.2*rAvg; tRate \u003e 0 {\n\t\t\t\t\tns := float64(s.BytesRem) / tRate * 1e9\n\t\t\t\t\tif ns \u003e float64(timeRemLimit) {\n\t\t\t\t\t\tns = float64(timeRemLimit)\n\t\t\t\t\t}\n\t\t\t\t\ts.TimeRem = clockRound(time.Duration(ns))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t// m.mu.Unlock()\n\treturn s\n}\n\n// Limit restricts the instantaneous (per-sample) data flow to rate bytes per\n// second. It returns the maximum number of bytes (0 \u003c= n \u003c= want) that may be\n// transferred immediately without exceeding the limit. If block == true, the\n// call blocks until n \u003e 0. want is returned unmodified if want \u003c 1, rate \u003c 1,\n// or the transfer is inactive (after a call to Done).\n//\n// At least one byte is always allowed to be transferred in any given sampling\n// period. Thus, if the sampling rate is 100ms, the lowest achievable flow rate\n// is 10 bytes per second.\n//\n// For usage examples, see the implementation of Reader and Writer in io.go.\nfunc (m *Monitor) Limit(want int, rate int64, block bool) (n int) {\n\tif block {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\tif want \u003c 1 || rate \u003c 1 {\n\t\treturn want\n\t}\n\t// m.mu.Lock()\n\n\t// Determine the maximum number of bytes that can be sent in one sample\n\tlimit := round(float64(rate) * m.sRate.Seconds())\n\tif limit \u003c= 0 {\n\t\tlimit = 1\n\t}\n\n\t_ = m.update(0)\n\t/* XXX\n\t// If block == true, wait until m.sBytes \u003c limit\n\tif now := m.update(0); block {\n\t\tfor m.sBytes \u003e= limit \u0026\u0026 m.active {\n\t\t\tnow = m.waitNextSample(now)\n\t\t}\n\t}\n\t*/\n\n\t// Make limit \u003c= want (unlimited if the transfer is no longer active)\n\tif limit -= m.sBytes; limit \u003e int64(want) || !m.active {\n\t\tlimit = int64(want)\n\t}\n\t// m.mu.Unlock()\n\n\tif limit \u003c 0 {\n\t\tlimit = 0\n\t}\n\treturn int(limit)\n}\n\n// SetTransferSize specifies the total size of the data transfer, which allows\n// the Monitor to calculate the overall progress and time to completion.\nfunc (m *Monitor) SetTransferSize(bytes int64) {\n\tif bytes \u003c 0 {\n\t\tbytes = 0\n\t}\n\t// m.mu.Lock()\n\tm.tBytes = bytes\n\t// m.mu.Unlock()\n}\n\n// update accumulates the transferred byte count for the current sample until\n// clock() - m.sLast \u003e= m.sRate. The monitor status is updated once the current\n// sample is done.\nfunc (m *Monitor) update(n int) (now time.Duration) {\n\tif !m.active {\n\t\treturn\n\t}\n\tif now = clock(); n \u003e 0 {\n\t\tm.tLast = now\n\t}\n\tm.sBytes += int64(n)\n\tif sTime := now - m.sLast; sTime \u003e= m.sRate {\n\t\tt := sTime.Seconds()\n\t\tif m.rSample = float64(m.sBytes) / t; m.rSample \u003e m.rPeak {\n\t\t\tm.rPeak = m.rSample\n\t\t}\n\n\t\t// Exponential moving average using a method similar to *nix load\n\t\t// average calculation. Longer sampling periods carry greater weight.\n\t\tif m.samples \u003e 0 {\n\t\t\tw := math.Exp(-t / m.rWindow)\n\t\t\tm.rEMA = m.rSample + w*(m.rEMA-m.rSample)\n\t\t} else {\n\t\t\tm.rEMA = m.rSample\n\t\t}\n\t\tm.reset(now)\n\t}\n\treturn\n}\n\n// reset clears the current sample state in preparation for the next sample.\nfunc (m *Monitor) reset(sampleTime time.Duration) {\n\tm.bytes += m.sBytes\n\tm.samples++\n\tm.sBytes = 0\n\tm.sLast = sampleTime\n}\n\n/*\n// waitNextSample sleeps for the remainder of the current sample. The lock is\n// released and reacquired during the actual sleep period, so it's possible for\n// the transfer to be inactive when this method returns.\nfunc (m *Monitor) waitNextSample(now time.Duration) time.Duration {\n\tconst minWait = 5 * time.Millisecond\n\tcurrent := m.sLast\n\n\t// sleep until the last sample time changes (ideally, just one iteration)\n\tfor m.sLast == current \u0026\u0026 m.active {\n\t\td := current + m.sRate - now\n\t\t// m.mu.Unlock()\n\t\tif d \u003c minWait {\n\t\t\td = minWait\n\t\t}\n\t\ttime.Sleep(d)\n\t\t// m.mu.Lock()\n\t\tnow = m.update(0)\n\t}\n\treturn now\n}\n*/\n"},{"name":"io.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// ErrLimit is returned by the Writer when a non-blocking write is short due to\n// the transfer rate limit.\nvar ErrLimit = errors.New(\"flowrate: flow rate limit exceeded\")\n\n// Limiter is implemented by the Reader and Writer to provide a consistent\n// interface for monitoring and controlling data transfer.\ntype Limiter interface {\n\tDone() int64\n\tStatus() Status\n\tSetTransferSize(bytes int64)\n\tSetLimit(new int64) (old int64)\n\tSetBlocking(new bool) (old bool)\n}\n\n// Reader implements io.ReadCloser with a restriction on the rate of data\n// transfer.\ntype Reader struct {\n\tio.Reader // Data source\n\t*Monitor // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool // What to do when no new bytes can be read due to the limit\n}\n\n// NewReader restricts all Read operations on r to limit bytes per second.\nfunc NewReader(r io.Reader, limit int64) *Reader {\n\treturn \u0026Reader{r, New(0, 0), limit, false} // XXX default false\n}\n\n// Read reads up to len(p) bytes into p without exceeding the current transfer\n// rate limit. It returns (0, nil) immediately if r is non-blocking and no new\n// bytes can be read at this time.\nfunc (r *Reader) Read(p []byte) (n int, err error) {\n\tp = p[:r.Limit(len(p), r.limit, r.block)]\n\tif len(p) \u003e 0 {\n\t\tn, err = r.IO(r.Reader.Read(p))\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (r *Reader) SetLimit(new int64) (old int64) {\n\told, r.limit = r.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Read call on a non-blocking reader returns immediately if no additional bytes\n// may be read at this time due to the rate limit.\nfunc (r *Reader) SetBlocking(new bool) (old bool) {\n\tif new == true {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\told, r.block = r.block, new\n\treturn\n}\n\n// Close closes the underlying reader if it implements the io.Closer interface.\nfunc (r *Reader) Close() error {\n\tdefer r.Done()\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\n// Writer implements io.WriteCloser with a restriction on the rate of data\n// transfer.\ntype Writer struct {\n\tio.Writer // Data destination\n\t*Monitor // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool // What to do when no new bytes can be written due to the limit\n}\n\n// NewWriter restricts all Write operations on w to limit bytes per second. The\n// transfer rate and the default blocking behavior (true) can be changed\n// directly on the returned *Writer.\nfunc NewWriter(w io.Writer, limit int64) *Writer {\n\treturn \u0026Writer{w, New(0, 0), limit, false} // XXX default false\n}\n\n// Write writes len(p) bytes from p to the underlying data stream without\n// exceeding the current transfer rate limit. It returns (n, ErrLimit) if w is\n// non-blocking and no additional bytes can be written at this time.\nfunc (w *Writer) Write(p []byte) (n int, err error) {\n\tvar c int\n\tfor len(p) \u003e 0 \u0026\u0026 err == nil {\n\t\ts := p[:w.Limit(len(p), w.limit, w.block)]\n\t\tif len(s) \u003e 0 {\n\t\t\tc, err = w.IO(w.Writer.Write(s))\n\t\t} else {\n\t\t\treturn n, ErrLimit\n\t\t}\n\t\tp = p[c:]\n\t\tn += c\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (w *Writer) SetLimit(new int64) (old int64) {\n\told, w.limit = w.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Write call on a non-blocking writer returns as soon as no additional bytes\n// may be written at this time due to the rate limit.\nfunc (w *Writer) SetBlocking(new bool) (old bool) {\n\told, w.block = w.block, new\n\treturn\n}\n\n// Close closes the underlying writer if it implements the io.Closer interface.\nfunc (w *Writer) Close() error {\n\tdefer w.Done()\n\tif c, ok := w.Writer.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n"},{"name":"io_test.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\tios_test \"internal/os_test\"\n)\n\n// XXX ugh, I can't even sleep milliseconds.\n// XXX\n\nconst (\n\t_50ms = 50 * time.Millisecond\n\t_100ms = 100 * time.Millisecond\n\t_200ms = 200 * time.Millisecond\n\t_300ms = 300 * time.Millisecond\n\t_400ms = 400 * time.Millisecond\n\t_500ms = 500 * time.Millisecond\n)\n\nfunc nextStatus(m *Monitor) Status {\n\tsamples := m.samples\n\tfor i := 0; i \u003c 30; i++ {\n\t\tif s := m.Status(); s.Samples != samples {\n\t\t\treturn s\n\t\t}\n\t\tios_test.Sleep(5 * time.Millisecond)\n\t}\n\treturn m.Status()\n}\n\nfunc TestReader(t *testing.T) {\n\tin := make([]byte, 100)\n\tfor i := range in {\n\t\tin[i] = byte(i)\n\t}\n\tb := make([]byte, 100)\n\tr := NewReader(bytes.NewReader(in), 100)\n\tstart := time.Now()\n\n\t// Make sure r implements Limiter\n\t_ = Limiter(r)\n\n\t// 1st read of 10 bytes is performed immediately\n\tif n, err := r.Read(b); n != 10 {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\t// No new Reads allowed in the current sample\n\tr.SetBlocking(false)\n\tif n, err := r.Read(b); n != 0 {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\tstatus := [6]Status{0: r.Status()} // No samples in the first status\n\n\t// 2nd read of 10 bytes blocks until the next sample\n\t// r.SetBlocking(true)\n\tios_test.Sleep(100 * time.Millisecond)\n\tif n, err := r.Read(b[10:]); n != 10 {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _100ms {\n\t\tt.Fatalf(\"r.Read(b[10:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tstatus[1] = r.Status() // 1st sample\n\tstatus[2] = nextStatus(r.Monitor) // 2nd sample\n\tstatus[3] = nextStatus(r.Monitor) // No activity for the 3rd sample\n\n\tif n := r.Done(); n != 20 {\n\t\tt.Fatalf(\"r.Done() expected 20; got %v\", n)\n\t}\n\n\tstatus[4] = r.Status()\n\tstatus[5] = nextStatus(r.Monitor) // Timeout\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n\t\t{true, start, _100ms, 0, 10, 1, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _200ms, _100ms, 20, 2, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _300ms, _200ms, 20, 3, 0, 90, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"r.Status(%v)\\nexpected: %v\\ngot : %v\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b[:20], in[:20]) {\n\t\tt.Errorf(\"r.Read() input doesn't match output\")\n\t}\n}\n\n// XXX blocking writer test doesn't work.\nfunc _TestWriter(t *testing.T) {\n\tb := make([]byte, 100)\n\tfor i := range b {\n\t\tb[i] = byte(i)\n\t}\n\tw := NewWriter(\u0026bytes.Buffer{}, 200)\n\tstart := time.Now()\n\n\t// Make sure w implements Limiter\n\t_ = Limiter(w)\n\n\t// Non-blocking 20-byte write for the first sample returns ErrLimit\n\tw.SetBlocking(false)\n\tif n, err := w.Write(b); n != 20 || err != ErrLimit {\n\t\tt.Fatalf(\"w.Write(b) expected 20 (ErrLimit); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"w.Write(b) took too long (%v)\", rt)\n\t}\n\n\t// Blocking 80-byte write\n\t// w.SetBlocking(true)\n\t// XXX This test doesn't work, because w.Write calls w.Limit(block=false),\n\t// XXX and it returns ErrLimit after 20. What we want is to keep waiting until 80 is returned,\n\t// XXX but blocking isn't supported. Sleeping 800 shouldn't be sufficient either (its a burst).\n\t// XXX This limits the usage of Limiter and m.Limit().\n\tios_test.Sleep(800 * time.Millisecond)\n\tif n, err := w.Write(b[20:]); n \u003c 80 {\n\t} else if n != 80 || err != nil {\n\t\tt.Fatalf(\"w.Write(b[20:]) expected 80 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _300ms {\n\t\t// Explanation for `rt \u003c _300ms` (as opposed to `\u003c _400ms`)\n\t\t//\n\t\t// |\u003c-- start | |\n\t\t// epochs: -----0ms|---100ms|---200ms|---300ms|---400ms\n\t\t// sends: 20|20 |20 |20 |20#\n\t\t//\n\t\t// NOTE: The '#' symbol can thus happen before 400ms is up.\n\t\t// Thus, we can only panic if rt \u003c _300ms.\n\t\tt.Fatalf(\"w.Write(b[20:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tw.SetTransferSize(100)\n\tstatus := []Status{w.Status(), nextStatus(w.Monitor)}\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, _400ms, 0, 80, 4, 200, 200, 200, 200, 20, _100ms, 80000},\n\t\t{true, start, _500ms, _100ms, 100, 5, 200, 200, 200, 200, 0, 0, 100000},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"w.Status(%v)\\nexpected: %v\\ngot : %v\\n\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b, w.Writer.(*bytes.Buffer).Bytes()) {\n\t\tt.Errorf(\"w.Write() input doesn't match output\")\n\t}\n}\n\nconst (\n\tmaxDeviationForDuration = 50 * time.Millisecond\n\tmaxDeviationForRate int64 = 50\n)\n\n// statusesAreEqual returns true if s1 is equal to s2. Equality here means\n// general equality of fields except for the duration and rates, which can\n// drift due to unpredictable delays (e.g. thread wakes up 25ms after\n// `time.Sleep` has ended).\nfunc statusesAreEqual(s1 *Status, s2 *Status) bool {\n\tif s1.Active == s2.Active \u0026\u0026\n\t\ts1.Start == s2.Start \u0026\u0026\n\t\tdurationsAreEqual(s1.Duration, s2.Duration, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Idle == s2.Idle \u0026\u0026\n\t\ts1.Bytes == s2.Bytes \u0026\u0026\n\t\ts1.Samples == s2.Samples \u0026\u0026\n\t\tratesAreEqual(s1.InstRate, s2.InstRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.CurRate, s2.CurRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.AvgRate, s2.AvgRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.PeakRate, s2.PeakRate, maxDeviationForRate) \u0026\u0026\n\t\ts1.BytesRem == s2.BytesRem \u0026\u0026\n\t\tdurationsAreEqual(s1.TimeRem, s2.TimeRem, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Progress == s2.Progress {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc durationsAreEqual(d1 time.Duration, d2 time.Duration, maxDeviation time.Duration) bool {\n\treturn d2-d1 \u003c= maxDeviation\n}\n\nfunc ratesAreEqual(r1 int64, r2 int64, maxDeviation int64) bool {\n\tsub := r1 - r2\n\tif sub \u003c 0 {\n\t\tsub = -sub\n\t}\n\tif sub \u003c= maxDeviation {\n\t\treturn true\n\t}\n\treturn false\n}\n"},{"name":"util.gno","body":"//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// clockRate is the resolution and precision of clock().\nconst clockRate = 20 * time.Millisecond\n\n// czero is the process start time rounded down to the nearest clockRate\n// increment.\nvar czero = time.Now().Round(clockRate)\n\n// clock returns a low resolution timestamp relative to the process start time.\nfunc clock() time.Duration {\n\treturn time.Now().Round(clockRate).Sub(czero)\n}\n\n// clockToTime converts a clock() timestamp to an absolute time.Time value.\nfunc clockToTime(c time.Duration) time.Time {\n\treturn czero.Add(c)\n}\n\n// clockRound returns d rounded to the nearest clockRate increment.\nfunc clockRound(d time.Duration) time.Duration {\n\treturn (d + clockRate\u003e\u003e1) / clockRate * clockRate\n}\n\n// round returns x rounded to the nearest int64 (non-negative values only).\nfunc round(x float64) int64 {\n\tif _, frac := math.Modf(x); frac \u003e= 0.5 {\n\t\treturn int64(math.Ceil(x))\n\t}\n\treturn int64(math.Floor(x))\n}\n\n// Percent represents a percentage in increments of 1/1000th of a percent.\ntype Percent uint32\n\n// percentOf calculates what percent of the total is x.\nfunc percentOf(x, total float64) Percent {\n\tif x \u003c 0 || total \u003c= 0 {\n\t\treturn 0\n\t} else if p := round(x / total * 1e5); p \u003c= math.MaxUint32 {\n\t\treturn Percent(p)\n\t}\n\treturn Percent(math.MaxUint32)\n}\n\nfunc (p Percent) Float() float64 {\n\treturn float64(p) * 1e-3\n}\n\nfunc (p Percent) String() string {\n\tvar buf [12]byte\n\tb := strconv.AppendUint(buf[:0], uint64(p)/1000, 10)\n\tn := len(b)\n\tb = strconv.AppendUint(b, 1000+uint64(p)%1000, 10)\n\tb[n] = '.'\n\treturn string(append(b, '%'))\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"fqname","path":"gno.land/p/demo/fqname","files":[{"name":"fqname.gno","body":"// Package fqname provides utilities for handling fully qualified identifiers in\n// Gno. A fully qualified identifier typically includes a package path followed\n// by a dot (.) and then the name of a variable, function, type, or other\n// package-level declaration.\npackage fqname\n\nimport \"strings\"\n\n// Parse splits a fully qualified identifier into its package path and name\n// components. It handles cases with and without slashes in the package path.\n//\n//\tpkgpath, name := fqname.Parse(\"gno.land/p/demo/avl.Tree\")\n//\tufmt.Sprintf(\"Package: %s, Name: %s\\n\", id.Package, id.Name)\n//\t// Output: Package: gno.land/p/demo/avl, Name: Tree\nfunc Parse(fqname string) (pkgpath, name string) {\n\t// Find the index of the last slash.\n\tlastSlashIndex := strings.LastIndex(fqname, \"/\")\n\tif lastSlashIndex == -1 {\n\t\t// No slash found, handle it as a simple package name with dot notation.\n\t\tdotIndex := strings.LastIndex(fqname, \".\")\n\t\tif dotIndex == -1 {\n\t\t\treturn fqname, \"\"\n\t\t}\n\t\treturn fqname[:dotIndex], fqname[dotIndex+1:]\n\t}\n\n\t// Get the part after the last slash.\n\tafterSlash := fqname[lastSlashIndex+1:]\n\n\t// Check for a dot in the substring after the last slash.\n\tdotIndex := strings.Index(afterSlash, \".\")\n\tif dotIndex == -1 {\n\t\t// No dot found after the last slash\n\t\treturn fqname, \"\"\n\t}\n\n\t// Split at the dot to separate the base and the suffix.\n\tbase := fqname[:lastSlashIndex+1+dotIndex]\n\tsuffix := afterSlash[dotIndex+1:]\n\n\treturn base, suffix\n}\n\n// Construct a qualified identifier.\n//\n//\tfqName := fqname.Construct(\"gno.land/r/demo/foo20\", \"GRC20\")\n//\tfmt.Println(\"Fully Qualified Name:\", fqName)\n//\t// Output: gno.land/r/demo/foo20.GRC20\nfunc Construct(pkgpath, name string) string {\n\t// TODO: ensure pkgpath is valid - and as such last part does not contain a dot.\n\tif name == \"\" {\n\t\treturn pkgpath\n\t}\n\treturn pkgpath + \".\" + name\n}\n\n// RenderLink creates a formatted link for a fully qualified identifier.\n// If the package path starts with \"gno.land\", it converts it to a markdown link.\n// If the domain is different or missing, it returns the input as is.\nfunc RenderLink(pkgPath, slug string) string {\n\tif strings.HasPrefix(pkgPath, \"gno.land\") {\n\t\tpkgLink := strings.TrimPrefix(pkgPath, \"gno.land\")\n\t\tif slug != \"\" {\n\t\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \").\" + slug\n\t\t}\n\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \")\"\n\t}\n\tif slug != \"\" {\n\t\treturn pkgPath + \".\" + slug\n\t}\n\treturn pkgPath\n}\n"},{"name":"fqname_test.gno","body":"package fqname\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpectedPkgPath string\n\t\texpectedName string\n\t}{\n\t\t{\"gno.land/p/demo/avl.Tree\", \"gno.land/p/demo/avl\", \"Tree\"},\n\t\t{\"gno.land/p/demo/avl\", \"gno.land/p/demo/avl\", \"\"},\n\t\t{\"gno.land/p/demo/avl.Tree.Node\", \"gno.land/p/demo/avl\", \"Tree.Node\"},\n\t\t{\"gno.land/p/demo/avl/nested.Package.Func\", \"gno.land/p/demo/avl/nested\", \"Package.Func\"},\n\t\t{\"path/filepath.Split\", \"path/filepath\", \"Split\"},\n\t\t{\"path.Split\", \"path\", \"Split\"},\n\t\t{\"path/filepath\", \"path/filepath\", \"\"},\n\t\t{\"path\", \"path\", \"\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tpkgpath, name := Parse(tt.input)\n\t\tuassert.Equal(t, tt.expectedPkgPath, pkgpath, \"Package path did not match\")\n\t\tuassert.Equal(t, tt.expectedName, name, \"Name did not match\")\n\t}\n}\n\nfunc TestConstruct(t *testing.T) {\n\ttests := []struct {\n\t\tpkgpath string\n\t\tname string\n\t\texpected string\n\t}{\n\t\t{\"gno.land/r/demo/foo20\", \"GRC20\", \"gno.land/r/demo/foo20.GRC20\"},\n\t\t{\"gno.land/r/demo/foo20\", \"\", \"gno.land/r/demo/foo20\"},\n\t\t{\"path\", \"\", \"path\"},\n\t\t{\"path\", \"Split\", \"path.Split\"},\n\t\t{\"path/filepath\", \"\", \"path/filepath\"},\n\t\t{\"path/filepath\", \"Split\", \"path/filepath.Split\"},\n\t\t{\"\", \"JustName\", \".JustName\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := Construct(tt.pkgpath, tt.name)\n\t\tuassert.Equal(t, tt.expected, result, \"Constructed FQName did not match expected\")\n\t}\n}\n\nfunc TestRenderLink(t *testing.T) {\n\ttests := []struct {\n\t\tpkgPath string\n\t\tslug string\n\t\texpected string\n\t}{\n\t\t{\"gno.land/p/demo/avl\", \"Tree\", \"[gno.land/p/demo/avl](/p/demo/avl).Tree\"},\n\t\t{\"gno.land/p/demo/avl\", \"\", \"[gno.land/p/demo/avl](/p/demo/avl)\"},\n\t\t{\"github.com/a/b\", \"C\", \"github.com/a/b.C\"},\n\t\t{\"example.com/pkg\", \"Func\", \"example.com/pkg.Func\"},\n\t\t{\"gno.land/r/demo/foo20\", \"GRC20\", \"[gno.land/r/demo/foo20](/r/demo/foo20).GRC20\"},\n\t\t{\"gno.land/r/demo/foo20\", \"\", \"[gno.land/r/demo/foo20](/r/demo/foo20)\"},\n\t\t{\"\", \"\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult := RenderLink(tt.pkgPath, tt.slug)\n\t\tuassert.Equal(t, tt.expected, result, \"Rendered link did not match expected\")\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"gnode","path":"gno.land/p/demo/gnode","files":[{"name":"gnode.gno","body":"package gnode\n\n// XXX what about Gnodes signing on behalf of others?\n// XXX like a multi-sig of Gnodes?\n\ntype Name string\n\ntype Gnode interface {\n\t//----------------------------------------\n\t// Basic properties\n\tGetName() Name\n\n\t//----------------------------------------\n\t// Affiliate Gnodes\n\tNumAffiliates() int\n\tGetAffiliates(Name) Affiliate\n\tAddAffiliate(Affiliate) error // must be affiliated\n\tRemAffiliate(Name) error // must have become unaffiliated\n\n\t//----------------------------------------\n\t// Signing\n\tNumSignedDocuments() int\n\tGetSignedDocument(idx int) Document\n\tSignDocument(doc Document) (int, error) // index relative to signer\n\n\t//----------------------------------------\n\t// Rendering\n\tRenderLines() []string\n}\n\ntype Affiliate struct {\n\tType string\n\tGnode Gnode\n\tTags []string\n}\n\ntype MyGnode struct {\n\tName\n\t// Owners // voting set, something that gives authority of action.\n\t// Treasury //\n\t// Affiliates //\n\t// Board // discussions\n\t// Data // XXX ?\n}\n\ntype Affiliates []*Affiliate\n\n// Documents are equal if they compare equal.\n// NOTE: requires all fields to be comparable.\ntype Document struct {\n\tAuthors string\n\t// Timestamp\n\t// Body\n\t// Attachments\n}\n\n// ACTIONS\n\n// * Lend tokens\n// * Pay tokens\n// * Administrate transferrable and non-transferrable tokens\n// * Sum tokens\n// * Passthrough dependencies\n// * Code\n// * ...\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"agent","path":"gno.land/p/demo/gnorkle/agent","files":[{"name":"whitelist.gno","body":"package agent\n\nimport \"gno.land/p/demo/avl\"\n\n// Whitelist manages whitelisted agent addresses.\ntype Whitelist struct {\n\tstore *avl.Tree\n}\n\n// ClearAddresses removes all addresses from the whitelist and puts into a state\n// that indicates it is moot and has no whitelist defined.\nfunc (m *Whitelist) ClearAddresses() {\n\tm.store = nil\n}\n\n// AddAddresses adds the given addresses to the whitelist.\nfunc (m *Whitelist) AddAddresses(addresses []string) {\n\tif m.store == nil {\n\t\tm.store = avl.NewTree()\n\t}\n\n\tfor _, address := range addresses {\n\t\tm.store.Set(address, struct{}{})\n\t}\n}\n\n// RemoveAddress removes the given address from the whitelist if it exists.\nfunc (m *Whitelist) RemoveAddress(address string) {\n\tif m.store == nil {\n\t\treturn\n\t}\n\n\tm.store.Remove(address)\n}\n\n// HasDefinition returns true if the whitelist has a definition. It retuns false if\n// `ClearAddresses` has been called without any subsequent `AddAddresses` calls, or\n// if `AddAddresses` has never been called.\nfunc (m Whitelist) HasDefinition() bool {\n\treturn m.store != nil\n}\n\n// HasAddress returns true if the given address is in the whitelist.\nfunc (m Whitelist) HasAddress(address string) bool {\n\tif m.store == nil {\n\t\treturn false\n\t}\n\n\treturn m.store.Has(address)\n}\n"},{"name":"whitelist_test.gno","body":"package agent_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestWhitelist(t *testing.T) {\n\tvar whitelist agent.Whitelist\n\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist should not be defined initially\")\n\n\twhitelist.AddAddresses([]string{\"a\", \"b\"})\n\tuassert.True(t, whitelist.HasAddress(\"a\"), `whitelist should have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should have address \"b\"`)\n\tuassert.True(t, whitelist.HasDefinition(), \"whitelist should be defined after adding addresses\")\n\n\twhitelist.RemoveAddress(\"a\")\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist should not have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should still have address \"b\"`)\n\n\twhitelist.ClearAddresses()\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist cleared; should not have address \"a\"`)\n\tuassert.False(t, whitelist.HasAddress(\"b\"), `whitelist cleared; should still have address \"b\"`)\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist cleared; should not be defined\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"feed","path":"gno.land/p/demo/gnorkle/feed","files":[{"name":"errors.gno","body":"package feed\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined feed\")\n"},{"name":"task.gno","body":"package feed\n\n// Task is a unit of work that can be part of a `Feed` definition. Tasks\n// are executed by agents.\ntype Task interface {\n\tMarshalJSON() ([]byte, error)\n}\n"},{"name":"type.gno","body":"package feed\n\n// Type indicates the type of a feed.\ntype Type int\n\nconst (\n\t// TypeStatic indicates a feed cannot be changed once the first value is committed.\n\tTypeStatic Type = iota\n\t// TypeContinuous indicates a feed can continuously ingest values and will publish\n\t// a new value on request using the values it has ingested.\n\tTypeContinuous\n\t// TypePeriodic indicates a feed can accept one or more values within a certain period\n\t// and will proceed to commit these values at the end up each period to produce an\n\t// aggregate value before starting a new period.\n\tTypePeriodic\n)\n"},{"name":"value.gno","body":"package feed\n\nimport \"time\"\n\n// Value represents a value published by a feed. The `Time` is when the value was published.\ntype Value struct {\n\tString string\n\tTime time.Time\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ingester","path":"gno.land/p/demo/gnorkle/ingester","files":[{"name":"errors.gno","body":"package ingester\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"ingester undefined\")\n"},{"name":"type.gno","body":"package ingester\n\n// Type indicates an ingester type.\ntype Type int\n\nconst (\n\t// TypeSingle indicates an ingester that can only ingest a single within a given period or no period.\n\tTypeSingle Type = iota\n\t// TypeMulti indicates an ingester that can ingest multiple within a given period or no period\n\tTypeMulti\n)\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"message","path":"gno.land/p/demo/gnorkle/message","files":[{"name":"parse.gno","body":"package message\n\nimport \"strings\"\n\n// ParseFunc parses a raw message and returns the message function\n// type extracted from the remainder of the message.\nfunc ParseFunc(rawMsg string) (FuncType, string) {\n\tfuncType, remainder := parseFirstToken(rawMsg)\n\treturn FuncType(funcType), remainder\n}\n\n// ParseID parses a raw message and returns the ID extracted from\n// the remainder of the message.\nfunc ParseID(rawMsg string) (string, string) {\n\treturn parseFirstToken(rawMsg)\n}\n\nfunc parseFirstToken(rawMsg string) (string, string) {\n\tmsgParts := strings.SplitN(rawMsg, \",\", 2)\n\tif len(msgParts) \u003c 2 {\n\t\treturn msgParts[0], \"\"\n\t}\n\n\treturn msgParts[0], msgParts[1]\n}\n"},{"name":"parse_test.gno","body":"package message_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestParseFunc(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\texpFuncType message.FuncType\n\t\texpRemainder string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t},\n\t\t{\n\t\t\tname: \"func only\",\n\t\t\tinput: \"ingest\",\n\t\t\texpFuncType: message.FuncTypeIngest,\n\t\t},\n\t\t{\n\t\t\tname: \"func with short remainder\",\n\t\t\tinput: \"commit,asdf\",\n\t\t\texpFuncType: message.FuncTypeCommit,\n\t\t\texpRemainder: \"asdf\",\n\t\t},\n\t\t{\n\t\t\tname: \"func with long remainder\",\n\t\t\tinput: \"request,hello,world,goodbye\",\n\t\t\texpFuncType: message.FuncTypeRequest,\n\t\t\texpRemainder: \"hello,world,goodbye\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfuncType, remainder := message.ParseFunc(tt.input)\n\n\t\t\tuassert.Equal(t, string(tt.expFuncType), string(funcType))\n\t\t\tuassert.Equal(t, tt.expRemainder, remainder)\n\t\t})\n\t}\n}\n"},{"name":"type.gno","body":"package message\n\n// FuncType is the type of function that is being called by the agent.\ntype FuncType string\n\nconst (\n\t// FuncTypeIngest means the agent is sending data for ingestion.\n\tFuncTypeIngest FuncType = \"ingest\"\n\t// FuncTypeCommit means the agent is requesting a feed commit the transitive data\n\t// being held by its ingester.\n\tFuncTypeCommit FuncType = \"commit\"\n\t// FuncTypeRequest means the agent is requesting feed definitions for all those\n\t// that it is whitelisted to provide data for.\n\tFuncTypeRequest FuncType = \"request\"\n)\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"gnorkle","path":"gno.land/p/demo/gnorkle/gnorkle","files":[{"name":"feed.gno","body":"package gnorkle\n\nimport (\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Feed is an abstraction used by a gnorkle `Instance` to ingest data from\n// agents and provide data feeds to consumers.\ntype Feed interface {\n\tID() string\n\tType() feed.Type\n\tValue() (value feed.Value, dataType string, consumable bool)\n\tIngest(funcType message.FuncType, rawMessage, providerAddress string) error\n\tMarshalJSON() ([]byte, error)\n\tTasks() []feed.Task\n\tIsActive() bool\n}\n\n// FeedWithWhitelist associates a `Whitelist` with a `Feed`.\ntype FeedWithWhitelist struct {\n\tFeed\n\tWhitelist\n}\n"},{"name":"ingester.gno","body":"package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/ingester\"\n\n// Ingester is the abstraction that allows a `Feed` to ingest data from agents\n// and commit it to storage using zero or more intermediate aggregation steps.\ntype Ingester interface {\n\tType() ingester.Type\n\tIngest(value, providerAddress string) (canAutoCommit bool, err error)\n\tCommitValue(storage Storage, providerAddress string) error\n}\n"},{"name":"instance.gno","body":"package gnorkle\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Instance is a single instance of an oracle.\ntype Instance struct {\n\tfeeds *avl.Tree\n\twhitelist agent.Whitelist\n}\n\n// NewInstance creates a new instance of an oracle.\nfunc NewInstance() *Instance {\n\treturn \u0026Instance{\n\t\tfeeds: avl.NewTree(),\n\t}\n}\n\nfunc assertValidID(id string) error {\n\tif len(id) == 0 {\n\t\treturn errors.New(\"feed ids cannot be empty\")\n\t}\n\n\tif strings.Contains(id, \",\") {\n\t\treturn errors.New(\"feed ids cannot contain commas\")\n\t}\n\n\treturn nil\n}\n\nfunc (i *Instance) assertFeedDoesNotExist(id string) error {\n\tif i.feeds.Has(id) {\n\t\treturn errors.New(\"feed already exists\")\n\t}\n\n\treturn nil\n}\n\n// AddFeeds adds feeds to the instance with empty whitelists.\nfunc (i *Instance) AddFeeds(feeds ...Feed) error {\n\tfor _, feed := range feeds {\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: new(agent.Whitelist),\n\t\t\t\tFeed: feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// AddFeedsWithWhitelists adds feeds to the instance with the given whitelists.\nfunc (i *Instance) AddFeedsWithWhitelists(feeds ...FeedWithWhitelist) error {\n\tfor _, feed := range feeds {\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: feed.Whitelist,\n\t\t\t\tFeed: feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// RemoveFeed removes a feed from the instance.\nfunc (i *Instance) RemoveFeed(id string) {\n\ti.feeds.Remove(id)\n}\n\n// PostMessageHandler is a type that allows for post-processing of feed state after a feed\n// ingests a message from an agent.\ntype PostMessageHandler interface {\n\tHandle(i *Instance, funcType message.FuncType, feed Feed) error\n}\n\n// HandleMessage handles a message from an agent and routes to either the logic that returns\n// feed definitions or the logic that allows a feed to ingest a message.\n//\n// TODO: Consider further message types that could allow administrative action such as modifying\n// a feed's whitelist without the owner of this oracle having to maintain a reference to it.\nfunc (i *Instance) HandleMessage(msg string, postHandler PostMessageHandler) (string, error) {\n\tcaller := string(std.GetOrigCaller())\n\n\tfuncType, msg := message.ParseFunc(msg)\n\n\tswitch funcType {\n\tcase message.FuncTypeRequest:\n\t\treturn i.GetFeedDefinitions(caller)\n\n\tdefault:\n\t\tid, msg := message.ParseID(msg)\n\t\tif err := assertValidID(id); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tfeedWithWhitelist, err := i.getFeedWithWhitelist(id)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, caller, nil) {\n\t\t\treturn \"\", errors.New(\"caller not whitelisted\")\n\t\t}\n\n\t\tif err := feedWithWhitelist.Ingest(funcType, msg, caller); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif postHandler != nil {\n\t\t\tpostHandler.Handle(i, funcType, feedWithWhitelist)\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (i *Instance) getFeed(id string) (Feed, error) {\n\tuntypedFeed, ok := i.feeds.Get(id)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeed, ok := untypedFeed.(Feed)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid feed type\")\n\t}\n\n\treturn feed, nil\n}\n\nfunc (i *Instance) getFeedWithWhitelist(id string) (FeedWithWhitelist, error) {\n\tuntypedFeedWithWhitelist, ok := i.feeds.Get(id)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeedWithWhitelist, ok := untypedFeedWithWhitelist.(FeedWithWhitelist)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid feed with whitelist type\")\n\t}\n\n\treturn feedWithWhitelist, nil\n}\n\n// GetFeedValue returns the most recently published value of a feed along with a string\n// representation of the value's type and boolean indicating whether the value is\n// okay for consumption.\nfunc (i *Instance) GetFeedValue(id string) (feed.Value, string, bool, error) {\n\tfoundFeed, err := i.getFeed(id)\n\tif err != nil {\n\t\treturn feed.Value{}, \"\", false, err\n\t}\n\n\tvalue, valueType, consumable := foundFeed.Value()\n\treturn value, valueType, consumable, nil\n}\n\n// GetFeedDefinitions returns a JSON string representing the feed definitions for which the given\n// agent address is whitelisted to provide values for ingestion.\nfunc (i *Instance) GetFeedDefinitions(forAddress string) (string, error) {\n\tinstanceHasAddressWhitelisted := !i.whitelist.HasDefinition() || i.whitelist.HasAddress(forAddress)\n\n\tbuf := new(strings.Builder)\n\tbuf.WriteString(\"[\")\n\tfirst := true\n\tvar err error\n\n\t// The boolean value returned by this callback function indicates whether to stop iterating.\n\ti.feeds.Iterate(\"\", \"\", func(_ string, value interface{}) bool {\n\t\tfeedWithWhitelist, ok := value.(FeedWithWhitelist)\n\t\tif !ok {\n\t\t\terr = errors.New(\"invalid feed type\")\n\t\t\treturn true\n\t\t}\n\n\t\t// Don't give agents the ability to try to publish to inactive feeds.\n\t\tif !feedWithWhitelist.IsActive() {\n\t\t\treturn false\n\t\t}\n\n\t\t// Skip feeds the address is not whitelisted for.\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, forAddress, \u0026instanceHasAddressWhitelisted) {\n\t\t\treturn false\n\t\t}\n\n\t\tvar taskBytes []byte\n\t\tif taskBytes, err = feedWithWhitelist.Feed.MarshalJSON(); err != nil {\n\t\t\treturn true\n\t\t}\n\n\t\t// Guard against any tasks that shouldn't be returned; maybe they are not active because they have\n\t\t// already been completed.\n\t\tif len(taskBytes) == 0 {\n\t\t\treturn false\n\t\t}\n\n\t\tif !first {\n\t\t\tbuf.WriteString(\",\")\n\t\t}\n\n\t\tfirst = false\n\t\tbuf.Write(taskBytes)\n\t\treturn false\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf.WriteString(\"]\")\n\treturn buf.String(), nil\n}\n"},{"name":"storage.gno","body":"package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/feed\"\n\n// Storage defines how published feed values should be read\n// and written.\ntype Storage interface {\n\tPut(value string) error\n\tGetLatest() feed.Value\n\tGetHistory() []feed.Value\n}\n"},{"name":"whitelist.gno","body":"package gnorkle\n\n// Whitelist is used to manage which agents are allowed to interact.\ntype Whitelist interface {\n\tClearAddresses()\n\tAddAddresses(addresses []string)\n\tRemoveAddress(address string)\n\tHasDefinition() bool\n\tHasAddress(address string) bool\n}\n\n// ClearWhitelist clears the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) ClearWhitelist(feedID string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.ClearAddresses()\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.ClearAddresses()\n\treturn nil\n}\n\n// AddToWhitelist adds the given addresses to the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) AddToWhitelist(feedID string, addresses []string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.AddAddresses(addresses)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.AddAddresses(addresses)\n\treturn nil\n}\n\n// RemoveFromWhitelist removes the given address from the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) RemoveFromWhitelist(feedID string, address string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.RemoveAddress(address)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.RemoveAddress(address)\n\treturn nil\n}\n\n// addressWhiteListed returns true if:\n// - the feed has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has no white list.\nfunc addressIsWhitelisted(instanceWhitelist, feedWhitelist Whitelist, address string, instanceWhitelistedOverride *bool) bool {\n\t// A feed whitelist takes priority, so it will return false if the feed has a whitelist and the caller is\n\t// not a part of it. An empty whitelist defers to the instance whitelist.\n\tif feedWhitelist != nil {\n\t\tif feedWhitelist.HasDefinition() \u0026\u0026 !feedWhitelist.HasAddress(address) {\n\t\t\treturn false\n\t\t}\n\n\t\t// Getting to this point means that one of the following is true:\n\t\t// - the feed has no defined whitelist (so it can't possibly have the address whitelisted)\n\t\t// - the feed has a defined whitelist and the caller is a part of it\n\t\t//\n\t\t// In this case, we can be sure that the boolean indicating whether the feed has this address whitelisted\n\t\t// is equivalent to the boolean indicating whether the feed has a defined whitelist.\n\t\tif feedWhitelist.HasDefinition() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif instanceWhitelistedOverride != nil {\n\t\treturn *instanceWhitelistedOverride\n\t}\n\n\t// We were unable able to determine whether this address is allowed after looking at the feed whitelist,\n\t// so fall back to the instance whitelist. A complete absence of values in the instance whitelist means\n\t// that the instance has no whitelist so we can return true because everything is allowed by default.\n\tif instanceWhitelist == nil || !instanceWhitelist.HasDefinition() {\n\t\treturn true\n\t}\n\n\t// The instance whitelist is defined so if the address is present then it is allowed.\n\treturn instanceWhitelist.HasAddress(address)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"storage","path":"gno.land/p/demo/gnorkle/storage","files":[{"name":"errors.gno","body":"package storage\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined storage\")\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"simple","path":"gno.land/p/demo/gnorkle/storage/simple","files":[{"name":"storage.gno","body":"package simple\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/storage\"\n)\n\n// Storage is simple, bounded storage for published feed values.\ntype Storage struct {\n\tvalues []feed.Value\n\tmaxValues uint\n}\n\n// NewStorage creates a new Storage with the given maximum number of values.\n// If maxValues is 0, the storage is bounded to a size of one. If this is not desirable,\n// then don't provide a value of 0.\nfunc NewStorage(maxValues uint) *Storage {\n\tif maxValues == 0 {\n\t\tmaxValues = 1\n\t}\n\n\treturn \u0026Storage{\n\t\tmaxValues: maxValues,\n\t}\n}\n\n// Put adds a new value to the storage. If the storage is full, the oldest value\n// is removed. If maxValues is 0, the storage is bounded to a size of one.\nfunc (s *Storage) Put(value string) error {\n\tif s == nil {\n\t\treturn storage.ErrUndefined\n\t}\n\n\ts.values = append(s.values, feed.Value{String: value, Time: time.Now()})\n\tif uint(len(s.values)) \u003e s.maxValues {\n\t\ts.values = s.values[1:]\n\t}\n\n\treturn nil\n}\n\n// GetLatest returns the most recently added value, or an empty value if none exist.\nfunc (s Storage) GetLatest() feed.Value {\n\tif len(s.values) == 0 {\n\t\treturn feed.Value{}\n\t}\n\n\treturn s.values[len(s.values)-1]\n}\n\n// GetHistory returns all values in the storage, from oldest to newest.\nfunc (s Storage) GetHistory() []feed.Value {\n\treturn s.values\n}\n"},{"name":"storage_test.gno","body":"package simple_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/storage\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestStorage(t *testing.T) {\n\tvar undefinedStorage *simple.Storage\n\terr := undefinedStorage.Put(\"\")\n\tuassert.ErrorIs(t, err, storage.ErrUndefined, \"expected storage.ErrUndefined on undefined storage\")\n\n\ttests := []struct {\n\t\tname string\n\t\tvaluesToPut []string\n\t\texpLatestValueString string\n\t\texpLatestValueTimeIsZero bool\n\t\texpHistoricalValueStrings []string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\texpLatestValueTimeIsZero: true,\n\t\t},\n\t\t{\n\t\t\tname: \"one value\",\n\t\t\tvaluesToPut: []string{\"one\"},\n\t\t\texpLatestValueString: \"one\",\n\t\t\texpHistoricalValueStrings: []string{\"one\"},\n\t\t},\n\t\t{\n\t\t\tname: \"two values\",\n\t\t\tvaluesToPut: []string{\"one\", \"two\"},\n\t\t\texpLatestValueString: \"two\",\n\t\t\texpHistoricalValueStrings: []string{\"one\", \"two\"},\n\t\t},\n\t\t{\n\t\t\tname: \"three values\",\n\t\t\tvaluesToPut: []string{\"one\", \"two\", \"three\"},\n\t\t\texpLatestValueString: \"three\",\n\t\t\texpHistoricalValueStrings: []string{\"two\", \"three\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsimpleStorage := simple.NewStorage(2)\n\t\t\tfor _, value := range tt.valuesToPut {\n\t\t\t\terr := simpleStorage.Put(value)\n\t\t\t\turequire.NoError(t, err, \"unexpected error putting value in storage\")\n\t\t\t}\n\n\t\t\tlatestValue := simpleStorage.GetLatest()\n\t\t\tuassert.Equal(t, tt.expLatestValueString, latestValue.String)\n\t\t\tuassert.Equal(t, tt.expLatestValueTimeIsZero, latestValue.Time.IsZero())\n\n\t\t\thistoricalValues := simpleStorage.GetHistory()\n\t\t\turequire.Equal(t, len(tt.expHistoricalValueStrings), len(historicalValues), \"historical values length does not match\")\n\n\t\t\tfor i, expValue := range tt.expHistoricalValueStrings {\n\t\t\t\tuassert.Equal(t, historicalValues[i].String, expValue)\n\t\t\t\turequire.False(t, historicalValues[i].Time.IsZero(), ufmt.Sprintf(\"unexpeced zero time for historical value at index %d\", i))\n\t\t\t}\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"single","path":"gno.land/p/demo/gnorkle/ingesters/single","files":[{"name":"ingester.gno","body":"package single\n\nimport (\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n)\n\n// ValueIngester is an ingester that ingests a single value.\ntype ValueIngester struct {\n\tvalue string\n}\n\n// Type returns the type of the ingester.\nfunc (i *ValueIngester) Type() ingester.Type {\n\treturn ingester.TypeSingle\n}\n\n// Ingest ingests a value provided by the given agent address.\nfunc (i *ValueIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i == nil {\n\t\treturn false, ingester.ErrUndefined\n\t}\n\n\ti.value = value\n\treturn true, nil\n}\n\n// CommitValue commits the ingested value to the given storage instance.\nfunc (i *ValueIngester) CommitValue(valueStorer gnorkle.Storage, providerAddress string) error {\n\tif i == nil {\n\t\treturn ingester.ErrUndefined\n\t}\n\n\treturn valueStorer.Put(i.value)\n}\n"},{"name":"ingester_test.gno","body":"package single_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/ingester\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestValueIngester(t *testing.T) {\n\tstorage := simple.NewStorage(1)\n\n\tvar undefinedIngester *single.ValueIngester\n\t_, err := undefinedIngester.Ingest(\"asdf\", \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to Ingest should return ingester.ErrUndefined\")\n\n\terr = undefinedIngester.CommitValue(storage, \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to CommitValue should return ingester.ErrUndefined\")\n\n\tvar valueIngester single.ValueIngester\n\ttyp := valueIngester.Type()\n\tuassert.Equal(t, int(ingester.TypeSingle), int(typ), \"single value ingester should return type ingester.TypeSingle\")\n\n\tingestValue := \"value\"\n\tautocommit, err := valueIngester.Ingest(ingestValue, \"gno11111\")\n\tuassert.True(t, autocommit, \"single value ingester should return autocommit true\")\n\tuassert.NoError(t, err)\n\n\terr = valueIngester.CommitValue(storage, \"gno11111\")\n\tuassert.NoError(t, err)\n\n\tlatestValue := storage.GetLatest()\n\tuassert.Equal(t, ingestValue, latestValue.String)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"static","path":"gno.land/p/demo/gnorkle/feeds/static","files":[{"name":"feed.gno","body":"package static\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Feed is a static feed.\ntype Feed struct {\n\tid string\n\tisLocked bool\n\tvalueDataType string\n\tingester gnorkle.Ingester\n\tstorage gnorkle.Storage\n\ttasks []feed.Task\n}\n\n// NewFeed creates a new static feed.\nfunc NewFeed(\n\tid string,\n\tvalueDataType string,\n\tingester gnorkle.Ingester,\n\tstorage gnorkle.Storage,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn \u0026Feed{\n\t\tid: id,\n\t\tvalueDataType: valueDataType,\n\t\tingester: ingester,\n\t\tstorage: storage,\n\t\ttasks: tasks,\n\t}\n}\n\n// NewSingleValueFeed is a convenience function for creating a static feed\n// that autocommits a value after a single ingestion.\nfunc NewSingleValueFeed(\n\tid string,\n\tvalueDataType string,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn NewFeed(\n\t\tid,\n\t\tvalueDataType,\n\t\t\u0026single.ValueIngester{},\n\t\tsimple.NewStorage(1),\n\t\ttasks...,\n\t)\n}\n\n// ID returns the feed's ID.\nfunc (f Feed) ID() string {\n\treturn f.id\n}\n\n// Type returns the feed's type.\nfunc (f Feed) Type() feed.Type {\n\treturn feed.TypeStatic\n}\n\n// Ingest ingests a message into the feed. It either adds the value to the ingester's\n// pending values or commits the value to the storage.\nfunc (f *Feed) Ingest(funcType message.FuncType, msg, providerAddress string) error {\n\tif f == nil {\n\t\treturn feed.ErrUndefined\n\t}\n\n\tif f.isLocked {\n\t\treturn errors.New(\"feed locked\")\n\t}\n\n\tswitch funcType {\n\tcase message.FuncTypeIngest:\n\t\t// Autocommit the ingester's value if it's a single value ingester\n\t\t// because this is a static feed and this is the only value it will ever have.\n\t\tif canAutoCommit, err := f.ingester.Ingest(msg, providerAddress); canAutoCommit \u0026\u0026 err == nil {\n\t\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tf.isLocked = true\n\t\t} else if err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase message.FuncTypeCommit:\n\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tf.isLocked = true\n\n\tdefault:\n\t\treturn errors.New(\"invalid message function \" + string(funcType))\n\t}\n\n\treturn nil\n}\n\n// Value returns the feed's latest value, it's data type, and whether or not it can\n// be safely consumed. In this case it uses `f.isLocked` because, this being a static\n// feed, it will only ever have one value; once that value is committed the feed is locked\n// and there is a valid, non-empty value to consume.\nfunc (f Feed) Value() (feed.Value, string, bool) {\n\treturn f.storage.GetLatest(), f.valueDataType, f.isLocked\n}\n\n// MarshalJSON marshals the components of the feed that are needed for\n// an agent to execute tasks and send values for ingestion.\nfunc (f Feed) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write([]byte(\n\t\t`{\"id\":\"` + f.id +\n\t\t\t`\",\"type\":\"` + ufmt.Sprintf(\"%d\", int(f.Type())) +\n\t\t\t`\",\"value_type\":\"` + f.valueDataType +\n\t\t\t`\",\"tasks\":[`),\n\t)\n\n\tfirst := true\n\tfor _, task := range f.tasks {\n\t\tif !first {\n\t\t\tw.WriteString(\",\")\n\t\t}\n\n\t\ttaskJSON, err := task.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.Write(taskJSON)\n\t\tfirst = false\n\t}\n\n\tw.Write([]byte(\"]}\"))\n\tw.Flush()\n\n\treturn buf.Bytes(), nil\n}\n\n// Tasks returns the feed's tasks. This allows task consumers to extract task\n// contents without having to marshal the entire feed.\nfunc (f Feed) Tasks() []feed.Task {\n\treturn f.tasks\n}\n\n// IsActive returns true if the feed is accepting ingestion requests from agents.\nfunc (f Feed) IsActive() bool {\n\treturn !f.isLocked\n}\n"},{"name":"feed_test.gno","body":"package static_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\ntype mockIngester struct {\n\tcanAutoCommit bool\n\tingestErr error\n\tcommitErr error\n\tvalue string\n\tproviderAddress string\n}\n\nfunc (i mockIngester) Type() ingester.Type {\n\treturn ingester.Type(0)\n}\n\nfunc (i *mockIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i.ingestErr != nil {\n\t\treturn false, i.ingestErr\n\t}\n\n\ti.value = value\n\ti.providerAddress = providerAddress\n\treturn i.canAutoCommit, nil\n}\n\nfunc (i *mockIngester) CommitValue(storage gnorkle.Storage, providerAddress string) error {\n\tif i.commitErr != nil {\n\t\treturn i.commitErr\n\t}\n\n\treturn storage.Put(i.value)\n}\n\nfunc TestNewSingleValueFeed(t *testing.T) {\n\tstaticFeed := static.NewSingleValueFeed(\"1\", \"\")\n\n\tuassert.Equal(t, \"1\", staticFeed.ID())\n\tuassert.Equal(t, int(feed.TypeStatic), int(staticFeed.Type()))\n}\n\nfunc TestFeed_Ingest(t *testing.T) {\n\tvar undefinedFeed *static.Feed\n\terr := undefinedFeed.Ingest(\"\", \"\", \"\")\n\tuassert.ErrorIs(t, err, feed.ErrUndefined)\n\n\ttests := []struct {\n\t\tname string\n\t\tingester *mockIngester\n\t\tverifyIsLocked bool\n\t\tdoCommit bool\n\t\tfuncType message.FuncType\n\t\tmsg string\n\t\tproviderAddress string\n\t\texpFeedValueString string\n\t\texpErrText string\n\t\texpIsActive bool\n\t}{\n\t\t{\n\t\t\tname: \"func invalid error\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncType(\"derp\"),\n\t\t\texpErrText: \"invalid message function derp\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest ingest error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tingestErr: errors.New(\"ingest error\"),\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\texpErrText: \"ingest error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr: errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\texpErrText: \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func commit commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr: errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeCommit,\n\t\t\texpErrText: \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"only ingest\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ingest autocommit\",\n\t\t\tingester: \u0026mockIngester{canAutoCommit: true},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpFeedValueString: \"still active feed\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname: \"commit no value\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeCommit,\n\t\t\tmsg: \"shouldn't be stored\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ingest then commmit\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"blahblah\",\n\t\t\tdoCommit: true,\n\t\t\texpFeedValueString: \"blahblah\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewFeed(\n\t\t\t\t\"1\",\n\t\t\t\t\"string\",\n\t\t\t\ttt.ingester,\n\t\t\t\tsimple.NewStorage(1),\n\t\t\t\tnil,\n\t\t\t)\n\n\t\t\tvar errText string\n\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\n\t\t\tif tt.doCommit {\n\t\t\t\terr := staticFeed.Ingest(message.FuncTypeCommit, \"\", \"\")\n\t\t\t\turequire.NoError(t, err, \"follow up commit failed\")\n\t\t\t}\n\n\t\t\tif tt.verifyIsLocked {\n\t\t\t\terrText = \"\"\n\t\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\t\terrText = err.Error()\n\t\t\t\t}\n\n\t\t\t\turequire.Equal(t, \"feed locked\", errText)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tt.providerAddress, tt.ingester.providerAddress)\n\n\t\t\tfeedValue, dataType, isLocked := staticFeed.Value()\n\t\t\tuassert.Equal(t, tt.expFeedValueString, feedValue.String)\n\t\t\tuassert.Equal(t, \"string\", dataType)\n\t\t\tuassert.Equal(t, tt.verifyIsLocked, isLocked)\n\t\t\tuassert.Equal(t, tt.expIsActive, staticFeed.IsActive())\n\t\t})\n\t}\n}\n\ntype mockTask struct {\n\terr error\n\tvalue string\n}\n\nfunc (t mockTask) MarshalJSON() ([]byte, error) {\n\tif t.err != nil {\n\t\treturn nil, t.err\n\t}\n\n\treturn []byte(`{\"value\":\"` + t.value + `\"}`), nil\n}\n\nfunc TestFeed_Tasks(t *testing.T) {\n\tid := \"99\"\n\tvalueDataType := \"int\"\n\n\ttests := []struct {\n\t\tname string\n\t\ttasks []feed.Task\n\t\texpErrText string\n\t\texpJSON string\n\t}{\n\t\t{\n\t\t\tname: \"no tasks\",\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"marshal error\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{err: errors.New(\"marshal error\")},\n\t\t\t},\n\t\t\texpErrText: \"marshal error\",\n\t\t},\n\t\t{\n\t\t\tname: \"one task\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"single\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"single\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"two tasks\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"first\"},\n\t\t\t\tmockTask{value: \"second\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"first\"},{\"value\":\"second\"}]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewSingleValueFeed(\n\t\t\t\tid,\n\t\t\t\tvalueDataType,\n\t\t\t\ttt.tasks...,\n\t\t\t)\n\n\t\t\turequire.Equal(t, len(tt.tasks), len(staticFeed.Tasks()))\n\n\t\t\tvar errText string\n\t\t\tjson, err := staticFeed.MarshalJSON()\n\t\t\tif err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\t\t\turequire.Equal(t, tt.expJSON, string(json))\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"exts","path":"gno.land/p/demo/grc/exts","files":[{"name":"token_metadata.gno","body":"package exts\n\ntype TokenMetadata interface {\n\t// Returns the name of the token.\n\tGetName() string\n\n\t// Returns the symbol of the token, usually a shorter version of the\n\t// name.\n\tGetSymbol() string\n\n\t// Returns the decimals places of the token.\n\tGetDecimals() uint\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc1155","path":"gno.land/p/demo/grc/grc1155","files":[{"name":"README.md","body":"# GRC-1155 Spec: Multi Token Standard\n\nGRC1155 is a specification for managing multiple tokens based on Gnoland. The name and design is based on Ethereum's ERC1155 standard.\n\n## See also:\n\n[ERC-1155 Spec][erc-1155]\n\n[erc-1155]: https://eips.ethereum.org/EIPS/eip-1155"},{"name":"basic_grc1155_token.gno","body":"package grc1155\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype basicGRC1155Token struct {\n\turi string\n\tbalances avl.Tree // \"TokenId:Address\" -\u003e uint64\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\nvar _ IGRC1155 = (*basicGRC1155Token)(nil)\n\n// Returns new basic GRC1155 token\nfunc NewBasicGRC1155Token(uri string) *basicGRC1155Token {\n\treturn \u0026basicGRC1155Token{\n\t\turi: uri,\n\t\tbalances: avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicGRC1155Token) Uri() string { return s.uri }\n\n// BalanceOf returns the input address's balance of the token type requested\nfunc (s *basicGRC1155Token) BalanceOf(addr std.Address, tid TokenID) (uint64, error) {\n\tif !isValidAddress(addr) {\n\t\treturn 0, ErrInvalidAddress\n\t}\n\n\tkey := string(tid) + \":\" + addr.String()\n\tbalance, found := s.balances.Get(key)\n\tif !found {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(uint64), nil\n}\n\n// BalanceOfBatch returns the balance of multiple account/token pairs\nfunc (s *basicGRC1155Token) BalanceOfBatch(owners []std.Address, batch []TokenID) ([]uint64, error) {\n\tif len(owners) != len(batch) {\n\t\treturn nil, ErrMismatchLength\n\t}\n\n\tbalanceOfBatch := make([]uint64, len(owners))\n\n\tfor i := 0; i \u003c len(owners); i++ {\n\t\tbalanceOfBatch[i], _ = s.BalanceOf(owners[i], batch[i])\n\t}\n\n\treturn balanceOfBatch, nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicGRC1155Token) SetApprovalForAll(operator std.Address, approved bool) error {\n\tif !isValidAddress(operator) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.GetOrigCaller()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// IsApprovedForAll returns true if operator is the owner or is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicGRC1155Token) IsApprovedForAll(owner, operator std.Address) bool {\n\tif operator == owner {\n\t\treturn true\n\t}\n\tkey := owner.String() + \":\" + operator.String()\n\t_, found := s.operatorApprovals.Get(key)\n\tif !found {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeTransferFrom(from, to std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.GetOrigCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, from, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, to, tid, amount})\n\n\treturn nil\n}\n\n// Safely transfers a `batch` of tokens from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.GetOrigCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, from, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, to, batch, amounts})\n\n\treturn nil\n}\n\n// Creates `amount` tokens of token type `id`, and assigns them to `to`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeMint(to std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.mintBatch(to, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, zeroAddress, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, zeroAddress, to, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `SafeMint()`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeBatchMint(to std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.mintBatch(to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, zeroAddress, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, zeroAddress, to, batch, amounts})\n\n\treturn nil\n}\n\n// Destroys `amount` tokens of token type `id` from `from`.\nfunc (s *basicGRC1155Token) Burn(from std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.burnBatch(from, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, zeroAddress, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `Burn()`\nfunc (s *basicGRC1155Token) BatchBurn(from std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.GetOrigCaller()\n\n\terr := s.burnBatch(from, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, zeroAddress, batch, amounts})\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll(): approve `operator` to operate on all of `owner` tokens\nfunc (s *basicGRC1155Token) setApprovalForAll(owner, operator std.Address, approved bool) error {\n\tif owner == operator {\n\t\treturn nil\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\tif approved {\n\t\ts.operatorApprovals.Set(key, approved)\n\t} else {\n\t\ts.operatorApprovals.Remove(key)\n\t}\n\n\temit(\u0026ApprovalForAllEvent{owner, operator, approved})\n\n\treturn nil\n}\n\n// Helper for SafeTransferFrom() and SafeBatchTransferFrom()\nfunc (s *basicGRC1155Token) safeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) || !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\tcaller := std.GetOrigCaller()\n\ts.beforeTokenTransfer(caller, from, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrInsufficientBalance\n\t\t}\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfromBalance -= amount\n\t\ttoBalance += amount\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for SafeMint() and SafeBatchMint()\nfunc (s *basicGRC1155Token) mintBatch(to std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.GetOrigCaller()\n\ts.beforeTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoBalance += amount\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for Burn() and BurnBatch()\nfunc (s *basicGRC1155Token) burnBatch(from std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.GetOrigCaller()\n\ts.beforeTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrBurnAmountExceedsBalance\n\t\t}\n\t\tfromBalance -= amount\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\treturn nil\n}\n\nfunc (s *basicGRC1155Token) setUri(newUri string) {\n\ts.uri = newUri\n\temit(\u0026UpdateURIEvent{newUri})\n}\n\nfunc (s *basicGRC1155Token) beforeTokenTransfer(operator, from, to std.Address, batch []TokenID, amounts []uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) afterTokenTransfer(operator, from, to std.Address, batch []TokenID, amounts []uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) doSafeTransferAcceptanceCheck(operator, from, to std.Address, tid TokenID, amount uint64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) doSafeBatchTransferAcceptanceCheck(operator, from, to std.Address, batch []TokenID, amounts []uint64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# URI:%s\\n\", s.uri)\n\n\treturn\n}\n"},{"name":"basic_grc1155_token_test.gno","body":"package grc1155\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nconst dummyURI = \"ipfs://xyz\"\n\nfunc TestNewBasicGRC1155Token(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestUri(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\tuassert.Equal(t, dummyURI, dummy.Uri())\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tbalanceZeroAddressOfToken1, err := dummy.BalanceOf(zeroAddress, tid1)\n\tuassert.Error(t, err, \"should result in error\")\n\n\tbalanceAddr1OfToken1, err := dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceAddr1OfToken1)\n\n\tdummy.mintBatch(addr1, []TokenID{tid1, tid2}, []uint64{10, 100})\n\tdummy.mintBatch(addr2, []TokenID{tid1}, []uint64{20})\n\n\tbalanceAddr1OfToken1, err = dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr1OfToken2, err := dummy.BalanceOf(addr1, tid2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr2OfToken1, err := dummy.BalanceOf(addr2, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, uint64(10), balanceAddr1OfToken1)\n\tuassert.Equal(t, uint64(100), balanceAddr1OfToken2)\n\tuassert.Equal(t, uint64(20), balanceAddr2OfToken1)\n}\n\nfunc TestBalanceOfBatch(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceBatch[0])\n\tuassert.Equal(t, uint64(0), balanceBatch[1])\n\n\tdummy.mintBatch(addr1, []TokenID{tid1}, []uint64{10})\n\tdummy.mintBatch(addr2, []TokenID{tid2}, []uint64{20})\n\n\tbalanceBatch, err = dummy.BalanceOfBatch([]std.Address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(10), balanceBatch[0])\n\tuassert.Equal(t, uint64(20), balanceBatch[1])\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.GetOrigCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n\n\terr = dummy.SetApprovalForAll(addr, false)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.GetOrigCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid := TokenID(\"1\")\n\n\tdummy.mintBatch(caller, []TokenID{tid}, []uint64{100})\n\n\terr := dummy.SafeTransferFrom(caller, zeroAddress, tid, 10)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 160)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 60)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(40), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(60), balanceOfAddr)\n}\n\nfunc TestSafeBatchTransferFrom(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.GetOrigCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(caller, []TokenID{tid1, tid2}, []uint64{10, 100})\n\n\terr := dummy.SafeBatchTransferFrom(caller, zeroAddress, []TokenID{tid1, tid2}, []uint64{4, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []uint64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1}, []uint64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []uint64{4, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{caller, addr, caller, addr}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of caller after batch transfer\n\tuassert.Equal(t, uint64(6), balanceBatch[0])\n\n\t// Check token1's balance of addr after batch transfer\n\tuassert.Equal(t, uint64(4), balanceBatch[1])\n\n\t// Check token2's balance of caller after batch transfer\n\tuassert.Equal(t, uint64(40), balanceBatch[2])\n\n\t// Check token2's balance of addr after batch transfer\n\tuassert.Equal(t, uint64(60), balanceBatch[3])\n}\n\nfunc TestSafeMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeMint(zeroAddress, tid1, 100)\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeMint(addr1, tid1, 100)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr1, tid2, 200)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr2, tid1, 50)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2, addr1}, []TokenID{tid1, tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after mint\n\tuassert.Equal(t, uint64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after mint\n\tuassert.Equal(t, uint64(50), balanceBatch[1])\n\t// Check token2's balance of addr1 after mint\n\tuassert.Equal(t, uint64(200), balanceBatch[2])\n}\n\nfunc TestSafeBatchMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeBatchMint(zeroAddress, []TokenID{tid1, tid2}, []uint64{100, 200})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchMint(addr1, []TokenID{tid1, tid2}, []uint64{100, 200})\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeBatchMint(addr2, []TokenID{tid1, tid2}, []uint64{300, 400})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2, addr1, addr2}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after batch mint\n\tuassert.Equal(t, uint64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after batch mint\n\tuassert.Equal(t, uint64(300), balanceBatch[1])\n\t// Check token2's balance of addr1 after batch mint\n\tuassert.Equal(t, uint64(200), balanceBatch[2])\n\t// Check token2's balance of addr2 after batch mint\n\tuassert.Equal(t, uint64(400), balanceBatch[3])\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []uint64{100, 200})\n\terr := dummy.Burn(zeroAddress, tid1, uint64(60))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, uint64(160))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, uint64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Burn(addr, tid2, uint64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after burn\n\tuassert.Equal(t, uint64(40), balanceBatch[0])\n\t// Check token2's balance of addr after burn\n\tuassert.Equal(t, uint64(140), balanceBatch[1])\n}\n\nfunc TestBatchBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []uint64{100, 200})\n\terr := dummy.BatchBurn(zeroAddress, []TokenID{tid1, tid2}, []uint64{60, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []uint64{160, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []uint64{60, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after batch burn\n\tuassert.Equal(t, uint64(40), balanceBatch[0])\n\t// Check token2's balance of addr after batch burn\n\tuassert.Equal(t, uint64(140), balanceBatch[1])\n}\n"},{"name":"errors.gno","body":"package grc1155\n\nimport \"errors\"\n\nvar (\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrMismatchLength = errors.New(\"accounts and ids length mismatch\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n\tErrTransferToRejectedOrNonGRC1155Receiver = errors.New(\"transfer to rejected or non GRC1155Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrInsufficientBalance = errors.New(\"insufficient balance for transfer\")\n\tErrBurnAmountExceedsBalance = errors.New(\"burn amount exceeds balance\")\n)\n"},{"name":"igrc1155.gno","body":"package grc1155\n\nimport \"std\"\n\ntype IGRC1155 interface {\n\tSafeTransferFrom(from, to std.Address, tid TokenID, amount uint64) error\n\tSafeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error\n\tBalanceOf(owner std.Address, tid TokenID) (uint64, error)\n\tBalanceOfBatch(owners []std.Address, batch []TokenID) ([]uint64, error)\n\tSetApprovalForAll(operator std.Address, approved bool) error\n\tIsApprovedForAll(owner, operator std.Address) bool\n}\n\ntype TokenID string\n\ntype TransferSingleEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tTokenID TokenID\n\tAmount uint64\n}\n\ntype TransferBatchEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tBatch []TokenID\n\tAmounts []uint64\n}\n\ntype ApprovalForAllEvent struct {\n\tOwner std.Address\n\tOperator std.Address\n\tApproved bool\n}\n\ntype UpdateURIEvent struct {\n\tURI string\n}\n"},{"name":"util.gno","body":"package grc1155\n\nimport (\n\t\"std\"\n)\n\nconst zeroAddress std.Address = \"\"\n\nfunc isValidAddress(addr std.Address) bool {\n\tif !addr.IsValid() {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc emit(event interface{}) {\n\t// TODO: setup a pubsub system here?\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc20","path":"gno.land/p/demo/grc/grc20","files":[{"name":"banker.gno","body":"package grc20\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Banker implements a token banker with admin privileges.\n//\n// The Banker is intended to be used in two main ways:\n// 1. as a temporary object used to make the initial minting, then deleted.\n// 2. preserved in an unexported variable to support conditional administrative\n// tasks protected by the contract.\ntype Banker struct {\n\tname string\n\tsymbol string\n\tdecimals uint\n\ttotalSupply uint64\n\tbalances avl.Tree // std.Address(owner) -\u003e uint64\n\tallowances avl.Tree // string(owner+\":\"+spender) -\u003e uint64\n\ttoken *token // to share the same pointer\n}\n\nfunc NewBanker(name, symbol string, decimals uint) *Banker {\n\tif name == \"\" {\n\t\tpanic(\"name should not be empty\")\n\t}\n\tif symbol == \"\" {\n\t\tpanic(\"symbol should not be empty\")\n\t}\n\t// XXX additional checks (length, characters, limits, etc)\n\n\tb := Banker{\n\t\tname: name,\n\t\tsymbol: symbol,\n\t\tdecimals: decimals,\n\t}\n\tt := \u0026token{banker: \u0026b}\n\tb.token = t\n\treturn \u0026b\n}\n\nfunc (b Banker) Token() Token { return b.token } // Token returns a grc20 safe-object implementation.\nfunc (b Banker) GetName() string { return b.name }\nfunc (b Banker) GetSymbol() string { return b.symbol }\nfunc (b Banker) GetDecimals() uint { return b.decimals }\nfunc (b Banker) TotalSupply() uint64 { return b.totalSupply }\nfunc (b Banker) KnownAccounts() int { return b.balances.Size() }\n\nfunc (b *Banker) Mint(address std.Address, amount uint64) error {\n\tif !address.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\t// TODO: check for overflow\n\n\tb.totalSupply += amount\n\tcurrentBalance := b.BalanceOf(address)\n\tnewBalance := currentBalance + amount\n\n\tb.balances.Set(string(address), newBalance)\n\n\tstd.Emit(\n\t\tMintEvent,\n\t\t\"from\", \"\",\n\t\t\"to\", string(address),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b *Banker) Burn(address std.Address, amount uint64) error {\n\tif !address.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\t// TODO: check for overflow\n\n\tcurrentBalance := b.BalanceOf(address)\n\tif currentBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tb.totalSupply -= amount\n\tnewBalance := currentBalance - amount\n\n\tb.balances.Set(string(address), newBalance)\n\n\tstd.Emit(\n\t\tBurnEvent,\n\t\t\"from\", string(address),\n\t\t\"to\", \"\",\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b Banker) BalanceOf(address std.Address) uint64 {\n\tbalance, found := b.balances.Get(address.String())\n\tif !found {\n\t\treturn 0\n\t}\n\treturn balance.(uint64)\n}\n\nfunc (b *Banker) SpendAllowance(owner, spender std.Address, amount uint64) error {\n\tif !owner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcurrentAllowance := b.Allowance(owner, spender)\n\tif currentAllowance \u003c amount {\n\t\treturn ErrInsufficientAllowance\n\t}\n\n\tkey := allowanceKey(owner, spender)\n\tnewAllowance := currentAllowance - amount\n\n\tif newAllowance == 0 {\n\t\tb.allowances.Remove(key)\n\t} else {\n\t\tb.allowances.Set(key, newAllowance)\n\t}\n\n\treturn nil\n}\n\nfunc (b *Banker) Transfer(from, to std.Address, amount uint64) error {\n\tif !from.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\ttoBalance := b.BalanceOf(to)\n\tfromBalance := b.BalanceOf(from)\n\n\tif fromBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tnewToBalance := toBalance + amount\n\tnewFromBalance := fromBalance - amount\n\n\tb.balances.Set(string(to), newToBalance)\n\tb.balances.Set(string(from), newFromBalance)\n\n\tstd.Emit(\n\t\tTransferEvent,\n\t\t\"from\", from.String(),\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b *Banker) TransferFrom(spender, from, to std.Address, amount uint64) error {\n\tif err := b.SpendAllowance(from, spender, amount); err != nil {\n\t\treturn err\n\t}\n\treturn b.Transfer(from, to, amount)\n}\n\nfunc (b *Banker) Allowance(owner, spender std.Address) uint64 {\n\tallowance, found := b.allowances.Get(allowanceKey(owner, spender))\n\tif !found {\n\t\treturn 0\n\t}\n\treturn allowance.(uint64)\n}\n\nfunc (b *Banker) Approve(owner, spender std.Address, amount uint64) error {\n\tif !owner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tb.allowances.Set(allowanceKey(owner, spender), amount)\n\n\tstd.Emit(\n\t\tApprovalEvent,\n\t\t\"owner\", string(owner),\n\t\t\"spender\", string(spender),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\nfunc (b *Banker) RenderHome() string {\n\tstr := \"\"\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", b.name, b.symbol)\n\tstr += ufmt.Sprintf(\"* **Decimals**: %d\\n\", b.decimals)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", b.totalSupply)\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", b.KnownAccounts())\n\treturn str\n}\n\nfunc allowanceKey(owner, spender std.Address) string {\n\treturn owner.String() + \":\" + spender.String()\n}\n"},{"name":"banker_test.gno","body":"package grc20\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestBankerImpl(t *testing.T) {\n\tdummy := NewBanker(\"Dummy\", \"DUMMY\", 4)\n\turequire.False(t, dummy == nil, \"dummy should not be nil\")\n}\n\nfunc TestAllowance(t *testing.T) {\n\tvar (\n\t\towner = testutils.TestAddress(\"owner\")\n\t\tspender = testutils.TestAddress(\"spender\")\n\t\tdest = testutils.TestAddress(\"dest\")\n\t)\n\n\tb := NewBanker(\"Dummy\", \"DUMMY\", 6)\n\turequire.NoError(t, b.Mint(owner, 100000000))\n\turequire.NoError(t, b.Approve(owner, spender, 5000000))\n\turequire.Error(t, b.TransferFrom(spender, owner, dest, 10000000), ErrInsufficientAllowance.Error(), \"should not be able to transfer more than approved\")\n\n\ttests := []struct {\n\t\tspend uint64\n\t\texp uint64\n\t}{\n\t\t{3, 4999997},\n\t\t{999997, 4000000},\n\t\t{4000000, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb0 := b.BalanceOf(dest)\n\t\turequire.NoError(t, b.TransferFrom(spender, owner, dest, tt.spend))\n\t\ta := b.Allowance(owner, spender)\n\t\turequire.Equal(t, a, tt.exp, ufmt.Sprintf(\"allowance exp: %d, got %d\", tt.exp, a))\n\t\tb := b.BalanceOf(dest)\n\t\texpB := b0 + tt.spend\n\t\turequire.Equal(t, b, expB, ufmt.Sprintf(\"balance exp: %d, got %d\", expB, b))\n\t}\n\n\turequire.Error(t, b.TransferFrom(spender, owner, dest, 1), \"no allowance\")\n\tkey := allowanceKey(owner, spender)\n\turequire.False(t, b.allowances.Has(key), \"allowance should be removed\")\n\turequire.Equal(t, b.Allowance(owner, spender), uint64(0), \"allowance should be 0\")\n}\n"},{"name":"token.gno","body":"package grc20\n\nimport (\n\t\"std\"\n)\n\n// token implements the Token interface.\n//\n// It is generated with Banker.Token().\n// It can safely be exposed publicly.\ntype token struct {\n\tbanker *Banker\n}\n\n// var _ Token = (*token)(nil)\nfunc (t *token) GetName() string { return t.banker.name }\nfunc (t *token) GetSymbol() string { return t.banker.symbol }\nfunc (t *token) GetDecimals() uint { return t.banker.decimals }\nfunc (t *token) TotalSupply() uint64 { return t.banker.totalSupply }\n\nfunc (t *token) BalanceOf(owner std.Address) uint64 {\n\treturn t.banker.BalanceOf(owner)\n}\n\nfunc (t *token) Transfer(to std.Address, amount uint64) error {\n\tcaller := std.PrevRealm().Addr()\n\treturn t.banker.Transfer(caller, to, amount)\n}\n\nfunc (t *token) Allowance(owner, spender std.Address) uint64 {\n\treturn t.banker.Allowance(owner, spender)\n}\n\nfunc (t *token) Approve(spender std.Address, amount uint64) error {\n\tcaller := std.PrevRealm().Addr()\n\treturn t.banker.Approve(caller, spender, amount)\n}\n\nfunc (t *token) TransferFrom(from, to std.Address, amount uint64) error {\n\tspender := std.PrevRealm().Addr()\n\tif err := t.banker.SpendAllowance(from, spender, amount); err != nil {\n\t\treturn err\n\t}\n\treturn t.banker.Transfer(from, to, amount)\n}\n"},{"name":"token_test.gno","body":"package grc20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestUserTokenImpl(t *testing.T) {\n\tbank := NewBanker(\"Dummy\", \"DUMMY\", 4)\n\ttok := bank.Token()\n\t_ = tok\n}\n\nfunc TestUserApprove(t *testing.T) {\n\towner := testutils.TestAddress(\"owner\")\n\tspender := testutils.TestAddress(\"spender\")\n\tdest := testutils.TestAddress(\"dest\")\n\n\tbank := NewBanker(\"Dummy\", \"DUMMY\", 6)\n\ttok := bank.Token()\n\n\t// Set owner as the original caller\n\tstd.TestSetOrigCaller(owner)\n\t// Mint 100000000 tokens for owner\n\turequire.NoError(t, bank.Mint(owner, 100000000))\n\n\t// Approve spender to spend 5000000 tokens\n\turequire.NoError(t, tok.Approve(spender, 5000000))\n\n\t// Set spender as the original caller\n\tstd.TestSetOrigCaller(spender)\n\t// Try to transfer 10000000 tokens from owner to dest, should fail because it exceeds allowance\n\turequire.Error(t,\n\t\ttok.TransferFrom(owner, dest, 10000000),\n\t\tErrInsufficientAllowance.Error(),\n\t\t\"should not be able to transfer more than approved\",\n\t)\n\n\t// Define a set of test data with spend amount and expected remaining allowance\n\ttests := []struct {\n\t\tspend uint64 // Spend amount\n\t\texp uint64 // Remaining allowance\n\t}{\n\t\t{3, 4999997},\n\t\t{999997, 4000000},\n\t\t{4000000, 0},\n\t}\n\n\t// perform transfer operation,and check if allowance and balance are correct\n\tfor _, tt := range tests {\n\t\tb0 := tok.BalanceOf(dest)\n\t\t// Perform transfer from owner to dest\n\t\turequire.NoError(t, tok.TransferFrom(owner, dest, tt.spend))\n\t\ta := tok.Allowance(owner, spender)\n\t\t// Check if allowance equals expected value\n\t\turequire.True(t, a == tt.exp, ufmt.Sprintf(\"allowance exp: %d,got %d\", tt.exp, a))\n\n\t\t// Get dest current balance\n\t\tb := tok.BalanceOf(dest)\n\t\t// Calculate expected balance ,should be initial balance plus transfer amount\n\t\texpB := b0 + tt.spend\n\t\t// Check if balance equals expected value\n\t\turequire.True(t, b == expB, ufmt.Sprintf(\"balance exp: %d,got %d\", expB, b))\n\t}\n\n\t// Try to transfer one token from owner to dest ,should fail because no allowance left\n\turequire.Error(t, tok.TransferFrom(owner, dest, 1), ErrInsufficientAllowance.Error(), \"no allowance\")\n}\n"},{"name":"types.gno","body":"package grc20\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/exts\"\n)\n\nvar (\n\tErrInsufficientBalance = errors.New(\"insufficient balance\")\n\tErrInsufficientAllowance = errors.New(\"insufficient allowance\")\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n)\n\ntype Token interface {\n\texts.TokenMetadata\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() uint64\n\n\t// Returns the amount of tokens owned by `account`.\n\tBalanceOf(account std.Address) uint64\n\n\t// Moves `amount` tokens from the caller's account to `to`.\n\t//\n\t// Returns an error if the operation failed.\n\tTransfer(to std.Address, amount uint64) error\n\n\t// Returns the remaining number of tokens that `spender` will be\n\t// allowed to spend on behalf of `owner` through {transferFrom}. This is\n\t// zero by default.\n\t//\n\t// This value changes when {approve} or {transferFrom} are called.\n\tAllowance(owner, spender std.Address) uint64\n\n\t// Sets `amount` as the allowance of `spender` over the caller's tokens.\n\t//\n\t// Returns an error if the operation failed.\n\t//\n\t// IMPORTANT: Beware that changing an allowance with this method brings the risk\n\t// that someone may use both the old and the new allowance by unfortunate\n\t// transaction ordering. One possible solution to mitigate this race\n\t// condition is to first reduce the spender's allowance to 0 and set the\n\t// desired value afterwards:\n\t// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n\tApprove(spender std.Address, amount uint64) error\n\n\t// Moves `amount` tokens from `from` to `to` using the\n\t// allowance mechanism. `amount` is then deducted from the caller's\n\t// allowance.\n\t//\n\t// Returns an error if the operation failed.\n\tTransferFrom(from, to std.Address, amount uint64) error\n}\n\nconst (\n\tMintEvent = \"Mint\"\n\tBurnEvent = \"Burn\"\n\tTransferEvent = \"Transfer\"\n\tApprovalEvent = \"Approval\"\n)\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc721","path":"gno.land/p/demo/grc/grc721","files":[{"name":"basic_nft.gno","body":"package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype basicNFT struct {\n\tname string\n\tsymbol string\n\towners avl.Tree // tokenId -\u003e OwnerAddress\n\tbalances avl.Tree // OwnerAddress -\u003e TokenCount\n\ttokenApprovals avl.Tree // TokenId -\u003e ApprovedAddress\n\ttokenURIs avl.Tree // TokenId -\u003e URIs\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\n// Returns new basic NFT\nfunc NewBasicNFT(name string, symbol string) *basicNFT {\n\treturn \u0026basicNFT{\n\t\tname: name,\n\t\tsymbol: symbol,\n\n\t\towners: avl.Tree{},\n\t\tbalances: avl.Tree{},\n\t\ttokenApprovals: avl.Tree{},\n\t\ttokenURIs: avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicNFT) Name() string { return s.name }\nfunc (s *basicNFT) Symbol() string { return s.symbol }\nfunc (s *basicNFT) TokenCount() uint64 { return uint64(s.owners.Size()) }\n\n// BalanceOf returns balance of input address\nfunc (s *basicNFT) BalanceOf(addr std.Address) (uint64, error) {\n\tif err := isValidAddress(addr); err != nil {\n\t\treturn 0, err\n\t}\n\n\tbalance, found := s.balances.Get(addr.String())\n\tif !found {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(uint64), nil\n}\n\n// OwnerOf returns owner of input token id\nfunc (s *basicNFT) OwnerOf(tid TokenID) (std.Address, error) {\n\towner, found := s.owners.Get(string(tid))\n\tif !found {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn owner.(std.Address), nil\n}\n\n// TokenURI returns the URI of input token id\nfunc (s *basicNFT) TokenURI(tid TokenID) (string, error) {\n\turi, found := s.tokenURIs.Get(string(tid))\n\tif !found {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn uri.(string), nil\n}\n\nfunc (s *basicNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) {\n\t// check for invalid TokenID\n\tif !s.exists(tid) {\n\t\treturn false, ErrInvalidTokenId\n\t}\n\n\t// check for the right owner\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner {\n\t\treturn false, ErrCallerIsNotOwner\n\t}\n\ts.tokenURIs.Set(string(tid), string(tURI))\n\treturn true, nil\n}\n\n// IsApprovedForAll returns true if operator is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicNFT) IsApprovedForAll(owner, operator std.Address) bool {\n\tkey := owner.String() + \":\" + operator.String()\n\t_, found := s.operatorApprovals.Get(key)\n\tif !found {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Approve approves the input address for particular token\nfunc (s *basicNFT) Approve(to std.Address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner == to {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner \u0026\u0026 !s.IsApprovedForAll(owner, caller) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\ts.tokenApprovals.Set(string(tid), to.String())\n\tevent := ApprovalEvent{owner, to, tid}\n\temit(\u0026event)\n\n\treturn nil\n}\n\n// GetApproved return the approved address for token\nfunc (s *basicNFT) GetApproved(tid TokenID) (std.Address, error) {\n\taddr, found := s.tokenApprovals.Get(string(tid))\n\tif !found {\n\t\treturn zeroAddress, ErrTokenIdNotHasApproved\n\t}\n\n\treturn std.Address(addr.(string)), nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicNFT) SetApprovalForAll(operator std.Address, approved bool) error {\n\tif err := isValidAddress(operator); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.PrevRealm().Addr()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC721 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error {\n\tcaller := std.PrevRealm().Addr()\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(from, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\n// Transfers `tokenId` token from `from` to `to`.\nfunc (s *basicNFT) TransferFrom(from, to std.Address, tid TokenID) error {\n\tcaller := std.PrevRealm().Addr()\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Mints `tokenId` and transfers it to `to`.\nfunc (s *basicNFT) Mint(to std.Address, tid TokenID) error {\n\treturn s.mint(to, tid)\n}\n\n// Mints `tokenId` and transfers it to `to`. Also checks that\n// contract recipients are using GRC721 protocol\nfunc (s *basicNFT) SafeMint(to std.Address, tid TokenID) error {\n\terr := s.mint(to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(zeroAddress, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\nfunc (s *basicNFT) Burn(tid TokenID) error {\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.beforeTokenTransfer(owner, zeroAddress, tid, 1)\n\n\ts.tokenApprovals.Remove(string(tid))\n\tbalance, err := s.BalanceOf(owner)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbalance -= 1\n\ts.balances.Set(owner.String(), balance)\n\ts.owners.Remove(string(tid))\n\n\tevent := TransferEvent{owner, zeroAddress, tid}\n\temit(\u0026event)\n\n\ts.afterTokenTransfer(owner, zeroAddress, tid, 1)\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll()\nfunc (s *basicNFT) setApprovalForAll(owner, operator std.Address, approved bool) error {\n\tif owner == operator {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\ts.operatorApprovals.Set(key, approved)\n\n\tevent := ApprovalForAllEvent{owner, operator, approved}\n\temit(\u0026event)\n\n\treturn nil\n}\n\n// Helper for TransferFrom() and SafeTransferFrom()\nfunc (s *basicNFT) transfer(from, to std.Address, tid TokenID) error {\n\tif err := isValidAddress(from); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\tif err := isValidAddress(to); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.beforeTokenTransfer(from, to, tid, 1)\n\n\t// Check that tokenId was not transferred by `beforeTokenTransfer`\n\towner, err = s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.tokenApprovals.Remove(string(tid))\n\tfromBalance, err := s.BalanceOf(from)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfromBalance -= 1\n\ttoBalance += 1\n\ts.balances.Set(from.String(), fromBalance)\n\ts.balances.Set(to.String(), toBalance)\n\ts.owners.Set(string(tid), to)\n\n\tevent := TransferEvent{from, to, tid}\n\temit(\u0026event)\n\n\ts.afterTokenTransfer(from, to, tid, 1)\n\n\treturn nil\n}\n\n// Helper for Mint() and SafeMint()\nfunc (s *basicNFT) mint(to std.Address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ts.beforeTokenTransfer(zeroAddress, to, tid, 1)\n\n\t// Check that tokenId was not minted by `beforeTokenTransfer`\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance += 1\n\ts.balances.Set(to.String(), toBalance)\n\ts.owners.Set(string(tid), to)\n\n\tevent := TransferEvent{zeroAddress, to, tid}\n\temit(\u0026event)\n\n\ts.afterTokenTransfer(zeroAddress, to, tid, 1)\n\n\treturn nil\n}\n\nfunc (s *basicNFT) isApprovedOrOwner(addr std.Address, tid TokenID) bool {\n\towner, found := s.owners.Get(string(tid))\n\tif !found {\n\t\treturn false\n\t}\n\n\tif addr == owner.(std.Address) || s.IsApprovedForAll(owner.(std.Address), addr) {\n\t\treturn true\n\t}\n\n\t_, err := s.GetApproved(tid)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Checks if token id already exists\nfunc (s *basicNFT) exists(tid TokenID) bool {\n\t_, found := s.owners.Get(string(tid))\n\treturn found\n}\n\nfunc (s *basicNFT) beforeTokenTransfer(from, to std.Address, firstTokenId TokenID, batchSize uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicNFT) afterTokenTransfer(from, to std.Address, firstTokenId TokenID, batchSize uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicNFT) checkOnGRC721Received(from, to std.Address, tid TokenID) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicNFT) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", s.name, s.symbol)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", s.TokenCount())\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", s.balances.Size())\n\n\treturn\n}\n"},{"name":"basic_nft_test.gno","body":"package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tdummyNFTName = \"DummyNFT\"\n\tdummyNFTSymbol = \"DNFT\"\n)\n\nfunc TestNewBasicNFT(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestName(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tname := dummy.Name()\n\tuassert.Equal(t, dummyNFTName, name)\n}\n\nfunc TestSymbol(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tsymbol := dummy.Symbol()\n\tuassert.Equal(t, dummyNFTSymbol, symbol)\n}\n\nfunc TestTokenCount(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcount := dummy.TokenCount()\n\tuassert.Equal(t, uint64(0), count)\n\n\tdummy.mint(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", TokenID(\"1\"))\n\tdummy.mint(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", TokenID(\"2\"))\n\n\tcount = dummy.TokenCount()\n\tuassert.Equal(t, uint64(2), count)\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tbalanceAddr1, err := dummy.BalanceOf(addr1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceAddr1)\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\tdummy.mint(addr1, TokenID(\"2\"))\n\tdummy.mint(addr2, TokenID(\"3\"))\n\n\tbalanceAddr1, err = dummy.BalanceOf(addr1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceAddr2, err := dummy.BalanceOf(addr2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, uint64(2), balanceAddr1)\n\tuassert.Equal(t, uint64(1), balanceAddr2)\n}\n\nfunc TestOwnerOf(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\towner, err := dummy.OwnerOf(TokenID(\"invalid\"))\n\tuassert.Error(t, err, \"should not result in error\")\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\tdummy.mint(addr2, TokenID(\"2\"))\n\n\t// Checking for token id \"1\"\n\towner, err = dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr1.String(), owner.String())\n\n\t// Checking for token id \"2\"\n\towner, err = dummy.OwnerOf(TokenID(\"2\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr2.String(), owner.String())\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n}\n\nfunc TestGetApproved(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tapprovedAddr, err := dummy.GetApproved(TokenID(\"invalid\"))\n\tuassert.Error(t, err, \"should result in error\")\n}\n\nfunc TestApprove(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\n\t_, err := dummy.GetApproved(TokenID(\"1\"))\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.Approve(addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tapprovedAddr, err := dummy.GetApproved(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, addr.String(), approvedAddr.String())\n}\n\nfunc TestTransferFrom(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\tdummy.mint(caller, TokenID(\"2\"))\n\n\terr := dummy.TransferFrom(caller, addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller)\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfAddr)\n\n\t// Check Owner of transferred Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, addr.String(), owner.String())\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PrevRealm().Addr()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\tdummy.mint(caller, TokenID(\"2\"))\n\n\terr := dummy.SafeTransferFrom(caller, addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfAddr)\n\n\t// Check Owner of transferred Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr.String(), owner.String())\n}\n\nfunc TestMint(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\terr := dummy.Mint(addr1, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Mint(addr1, TokenID(\"2\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Mint(addr2, TokenID(\"3\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Try minting duplicate token id\n\terr = dummy.Mint(addr2, TokenID(\"1\"))\n\tuassert.Error(t, err, \"should not result in error\")\n\n\t// Check Owner of Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr1.String(), owner.String())\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(addr, TokenID(\"1\"))\n\tdummy.mint(addr, TokenID(\"2\"))\n\n\terr := dummy.Burn(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check Owner of Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.Error(t, err, \"should result in error\")\n}\n\nfunc TestSetTokenURI(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\ttokenURI := \"http://example.com/token\"\n\n\tstd.TestSetOrigCaller(std.Address(addr1)) // addr1\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\t_, derr := dummy.SetTokenURI(TokenID(\"1\"), TokenURI(tokenURI))\n\tuassert.NoError(t, derr, \"should not result in error\")\n\n\t// Test case: Invalid token ID\n\t_, err := dummy.SetTokenURI(TokenID(\"3\"), TokenURI(tokenURI))\n\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\n\tstd.TestSetOrigCaller(std.Address(addr2)) // addr2\n\n\t_, cerr := dummy.SetTokenURI(TokenID(\"1\"), TokenURI(tokenURI)) // addr2 trying to set URI for token 1\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Test case: Retrieving TokenURI\n\tstd.TestSetOrigCaller(std.Address(addr1)) // addr1\n\n\tdummyTokenURI, err := dummy.TokenURI(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"TokenURI error\")\n\tuassert.Equal(t, string(tokenURI), string(dummyTokenURI))\n}\n"},{"name":"errors.gno","body":"package grc721\n\nimport \"errors\"\n\nvar (\n\tErrInvalidTokenId = errors.New(\"invalid token id\")\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrTokenIdNotHasApproved = errors.New(\"token id not approved for anyone\")\n\tErrApprovalToCurrentOwner = errors.New(\"approval to current owner\")\n\tErrCallerIsNotOwner = errors.New(\"caller is not token owner\")\n\tErrCallerNotApprovedForAll = errors.New(\"caller is not approved for all\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n\tErrTransferFromIncorrectOwner = errors.New(\"transfer from incorrect owner\")\n\tErrTransferToNonGRC721Receiver = errors.New(\"transfer to non GRC721Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrTokenIdAlreadyExists = errors.New(\"token id already exists\")\n\n\t// ERC721Royalty\n\tErrInvalidRoyaltyPercentage = errors.New(\"invalid royalty percentage\")\n\tErrInvalidRoyaltyPaymentAddress = errors.New(\"invalid royalty paymentAddress\")\n\tErrCannotCalculateRoyaltyAmount = errors.New(\"cannot calculate royalty amount\")\n)\n"},{"name":"grc721_metadata.gno","body":"package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// metadataNFT represents an NFT with metadata extensions.\ntype metadataNFT struct {\n\t*basicNFT // Embedded basicNFT struct for basic NFT functionality\n\textensions *avl.Tree // AVL tree for storing metadata extensions\n}\n\n// Ensure that metadataNFT implements the IGRC721MetadataOnchain interface.\nvar _ IGRC721MetadataOnchain = (*metadataNFT)(nil)\n\n// NewNFTWithMetadata creates a new basic NFT with metadata extensions.\nfunc NewNFTWithMetadata(name string, symbol string) *metadataNFT {\n\t// Create a new basic NFT\n\tnft := NewBasicNFT(name, symbol)\n\n\t// Return a metadataNFT with basicNFT embedded and an empty AVL tree for extensions\n\treturn \u0026metadataNFT{\n\t\tbasicNFT: nft,\n\t\textensions: avl.NewTree(),\n\t}\n}\n\n// SetTokenMetadata sets metadata for a given token ID.\nfunc (s *metadataNFT) SetTokenMetadata(tid TokenID, metadata Metadata) error {\n\t// Check if the caller is the owner of the token\n\towner, err := s.basicNFT.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set the metadata for the token ID in the extensions AVL tree\n\ts.extensions.Set(string(tid), metadata)\n\treturn nil\n}\n\n// TokenMetadata retrieves metadata for a given token ID.\nfunc (s *metadataNFT) TokenMetadata(tid TokenID) (Metadata, error) {\n\t// Retrieve metadata from the extensions AVL tree\n\tmetadata, found := s.extensions.Get(string(tid))\n\tif !found {\n\t\treturn Metadata{}, ErrInvalidTokenId\n\t}\n\n\treturn metadata.(Metadata), nil\n}\n\n// mint mints a new token and assigns it to the specified address.\nfunc (s *metadataNFT) mint(to std.Address, tid TokenID) error {\n\t// Check if the address is valid\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\t// Check if the token ID already exists\n\tif s.basicNFT.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ts.basicNFT.beforeTokenTransfer(zeroAddress, to, tid, 1)\n\n\t// Check if the token ID was minted by beforeTokenTransfer\n\tif s.basicNFT.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\t// Increment balance of the recipient address\n\ttoBalance, err := s.basicNFT.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance += 1\n\ts.basicNFT.balances.Set(to.String(), toBalance)\n\n\t// Set owner of the token ID to the recipient address\n\ts.basicNFT.owners.Set(string(tid), to)\n\n\t// Emit transfer event\n\tevent := TransferEvent{zeroAddress, to, tid}\n\temit(\u0026event)\n\n\ts.basicNFT.afterTokenTransfer(zeroAddress, to, tid, 1)\n\n\treturn nil\n}\n"},{"name":"grc721_metadata_test.gno","body":"package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSetMetadata(t *testing.T) {\n\t// Create a new dummy NFT with metadata\n\tdummy := NewNFTWithMetadata(dummyNFTName, dummyNFTSymbol)\n\tif dummy == nil {\n\t\tt.Errorf(\"should not be nil\")\n\t}\n\n\t// Define addresses for testing purposes\n\taddr1 := testutils.TestAddress(\"alice\")\n\taddr2 := testutils.TestAddress(\"bob\")\n\n\t// Define metadata attributes\n\tname := \"test\"\n\tdescription := \"test\"\n\timage := \"test\"\n\timageData := \"test\"\n\texternalURL := \"test\"\n\tattributes := []Trait{}\n\tbackgroundColor := \"test\"\n\tanimationURL := \"test\"\n\tyoutubeURL := \"test\"\n\n\t// Set the original caller to addr1\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\t// Mint a new token for addr1\n\tdummy.mint(addr1, TokenID(\"1\"))\n\n\t// Set metadata for token 1\n\tderr := dummy.SetTokenMetadata(TokenID(\"1\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if there was an error setting metadata\n\tuassert.NoError(t, derr, \"Should not result in error\")\n\n\t// Test case: Invalid token ID\n\terr := dummy.SetTokenMetadata(TokenID(\"3\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if the error returned matches the expected error\n\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\n\t// Set the original caller to addr2\n\tstd.TestSetOrigCaller(addr2) // addr2\n\n\t// Try to set metadata for token 1 from addr2 (should fail)\n\tcerr := dummy.SetTokenMetadata(TokenID(\"1\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if the error returned matches the expected error\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Set the original caller back to addr1\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\t// Retrieve metadata for token 1\n\tdummyMetadata, err := dummy.TokenMetadata(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"Metadata error\")\n\n\t// Check if metadata attributes match expected values\n\tuassert.Equal(t, image, dummyMetadata.Image)\n\tuassert.Equal(t, imageData, dummyMetadata.ImageData)\n\tuassert.Equal(t, externalURL, dummyMetadata.ExternalURL)\n\tuassert.Equal(t, description, dummyMetadata.Description)\n\tuassert.Equal(t, name, dummyMetadata.Name)\n\tuassert.Equal(t, len(attributes), len(dummyMetadata.Attributes))\n\tuassert.Equal(t, backgroundColor, dummyMetadata.BackgroundColor)\n\tuassert.Equal(t, animationURL, dummyMetadata.AnimationURL)\n\tuassert.Equal(t, youtubeURL, dummyMetadata.YoutubeURL)\n}\n"},{"name":"grc721_royalty.gno","body":"package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// royaltyNFT represents a non-fungible token (NFT) with royalty functionality.\ntype royaltyNFT struct {\n\t*metadataNFT // Embedding metadataNFT for NFT functionality\n\ttokenRoyaltyInfo *avl.Tree // AVL tree to store royalty information for each token\n\tmaxRoyaltyPercentage uint64 // maxRoyaltyPercentage represents the maximum royalty percentage that can be charged every sale\n}\n\n// Ensure that royaltyNFT implements the IGRC2981 interface.\nvar _ IGRC2981 = (*royaltyNFT)(nil)\n\n// NewNFTWithRoyalty creates a new royalty NFT with the specified name, symbol, and royalty calculator.\nfunc NewNFTWithRoyalty(name string, symbol string) *royaltyNFT {\n\t// Create a new NFT with metadata\n\tnft := NewNFTWithMetadata(name, symbol)\n\n\treturn \u0026royaltyNFT{\n\t\tmetadataNFT: nft,\n\t\ttokenRoyaltyInfo: avl.NewTree(),\n\t\tmaxRoyaltyPercentage: 100,\n\t}\n}\n\n// SetTokenRoyalty sets the royalty information for a specific token ID.\nfunc (r *royaltyNFT) SetTokenRoyalty(tid TokenID, royaltyInfo RoyaltyInfo) error {\n\t// Validate the payment address\n\tif err := isValidAddress(royaltyInfo.PaymentAddress); err != nil {\n\t\treturn ErrInvalidRoyaltyPaymentAddress\n\t}\n\n\t// Check if royalty percentage exceeds maxRoyaltyPercentage\n\tif royaltyInfo.Percentage \u003e r.maxRoyaltyPercentage {\n\t\treturn ErrInvalidRoyaltyPercentage\n\t}\n\n\t// Check if the caller is the owner of the token\n\towner, err := r.metadataNFT.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := std.PrevRealm().Addr()\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set royalty information for the token\n\tr.tokenRoyaltyInfo.Set(string(tid), royaltyInfo)\n\n\treturn nil\n}\n\n// RoyaltyInfo returns the royalty information for the given token ID and sale price.\nfunc (r *royaltyNFT) RoyaltyInfo(tid TokenID, salePrice uint64) (std.Address, uint64, error) {\n\t// Retrieve royalty information for the token\n\tval, found := r.tokenRoyaltyInfo.Get(string(tid))\n\tif !found {\n\t\treturn \"\", 0, ErrInvalidTokenId\n\t}\n\n\troyaltyInfo := val.(RoyaltyInfo)\n\n\t// Calculate royalty amount\n\troyaltyAmount, _ := r.calculateRoyaltyAmount(salePrice, royaltyInfo.Percentage)\n\n\treturn royaltyInfo.PaymentAddress, royaltyAmount, nil\n}\n\nfunc (r *royaltyNFT) calculateRoyaltyAmount(salePrice, percentage uint64) (uint64, error) {\n\troyaltyAmount := (salePrice * percentage) / 100\n\treturn royaltyAmount, nil\n}\n"},{"name":"grc721_royalty_test.gno","body":"package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSetTokenRoyalty(t *testing.T) {\n\tdummy := NewNFTWithRoyalty(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := testutils.TestAddress(\"alice\")\n\taddr2 := testutils.TestAddress(\"bob\")\n\n\tpaymentAddress := testutils.TestAddress(\"john\")\n\tpercentage := uint64(10) // 10%\n\n\tsalePrice := uint64(1000)\n\texpectRoyaltyAmount := uint64(100)\n\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\n\tderr := dummy.SetTokenRoyalty(TokenID(\"1\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.NoError(t, derr, \"Should not result in error\")\n\n\t// Test case: Invalid token ID\n\terr := dummy.SetTokenRoyalty(TokenID(\"3\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, derr, ErrInvalidTokenId)\n\n\tstd.TestSetOrigCaller(addr2) // addr2\n\n\tcerr := dummy.SetTokenRoyalty(TokenID(\"1\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Test case: Invalid payment address\n\taerr := dummy.SetTokenRoyalty(TokenID(\"4\"), RoyaltyInfo{\n\t\tPaymentAddress: std.Address(\"###\"), // invalid address\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, aerr, ErrInvalidRoyaltyPaymentAddress)\n\n\t// Test case: Invalid percentage\n\tperr := dummy.SetTokenRoyalty(TokenID(\"5\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: uint64(200), // over maxRoyaltyPercentage\n\t})\n\tuassert.ErrorIs(t, perr, ErrInvalidRoyaltyPercentage)\n\n\t// Test case: Retrieving Royalty Info\n\tstd.TestSetOrigCaller(addr1) // addr1\n\n\tdummyPaymentAddress, dummyRoyaltyAmount, rerr := dummy.RoyaltyInfo(TokenID(\"1\"), salePrice)\n\tuassert.NoError(t, rerr, \"RoyaltyInfo error\")\n\tuassert.Equal(t, paymentAddress, dummyPaymentAddress)\n\tuassert.Equal(t, expectRoyaltyAmount, dummyRoyaltyAmount)\n}\n"},{"name":"igrc721.gno","body":"package grc721\n\nimport \"std\"\n\ntype IGRC721 interface {\n\tBalanceOf(owner std.Address) (uint64, error)\n\tOwnerOf(tid TokenID) (std.Address, error)\n\tSetTokenURI(tid TokenID, tURI TokenURI) (bool, error)\n\tSafeTransferFrom(from, to std.Address, tid TokenID) error\n\tTransferFrom(from, to std.Address, tid TokenID) error\n\tApprove(approved std.Address, tid TokenID) error\n\tSetApprovalForAll(operator std.Address, approved bool) error\n\tGetApproved(tid TokenID) (std.Address, error)\n\tIsApprovedForAll(owner, operator std.Address) bool\n}\n\ntype (\n\tTokenID string\n\tTokenURI string\n)\n\ntype TransferEvent struct {\n\tFrom std.Address\n\tTo std.Address\n\tTokenID TokenID\n}\n\ntype ApprovalEvent struct {\n\tOwner std.Address\n\tApproved std.Address\n\tTokenID TokenID\n}\n\ntype ApprovalForAllEvent struct {\n\tOwner std.Address\n\tOperator std.Address\n\tApproved bool\n}\n"},{"name":"igrc721_metadata.gno","body":"package grc721\n\n// IGRC721CollectionMetadata describes basic information about an NFT collection.\ntype IGRC721CollectionMetadata interface {\n\tName() string // Name returns the name of the collection.\n\tSymbol() string // Symbol returns the symbol of the collection.\n}\n\n// IGRC721Metadata follows the Ethereum standard\ntype IGRC721Metadata interface {\n\tIGRC721CollectionMetadata\n\tTokenURI(tid TokenID) (string, error) // TokenURI returns the URI of a specific token.\n}\n\n// IGRC721Metadata follows the OpenSea metadata standard\ntype IGRC721MetadataOnchain interface {\n\tIGRC721CollectionMetadata\n\tTokenMetadata(tid TokenID) (Metadata, error)\n}\n\ntype Trait struct {\n\tDisplayType string\n\tTraitType string\n\tValue string\n}\n\n// see: https://docs.opensea.io/docs/metadata-standards\ntype Metadata struct {\n\tImage string // URL to the image of the item. Can be any type of image (including SVGs, which will be cached into PNGs by OpenSea), IPFS or Arweave URLs or paths. We recommend using a minimum 3000 x 3000 image.\n\tImageData string // Raw SVG image data, if you want to generate images on the fly (not recommended). Only use this if you're not including the image parameter.\n\tExternalURL string // URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site.\n\tDescription string // Human-readable description of the item. Markdown is supported.\n\tName string // Name of the item.\n\tAttributes []Trait // Attributes for the item, which will show up on the OpenSea page for the item.\n\tBackgroundColor string // Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #\n\tAnimationURL string // URL to a multimedia attachment for the item. Supported file extensions: GLTF, GLB, WEBM, MP4, M4V, OGV, OGG, MP3, WAV, OGA, HTML (for rich experiences and interactive NFTs using JavaScript canvas, WebGL, etc.). Scripts and relative paths within the HTML page are now supported. Access to browser extensions is not supported.\n\tYoutubeURL string // URL to a YouTube video (only used if animation_url is not provided).\n}\n"},{"name":"igrc721_royalty.gno","body":"package grc721\n\nimport \"std\"\n\n// IGRC2981 follows the Ethereum standard\ntype IGRC2981 interface {\n\t// RoyaltyInfo retrieves royalty information for a tokenID and salePrice.\n\t// It returns the payment address, royalty amount, and an error if any.\n\tRoyaltyInfo(tokenID TokenID, salePrice uint64) (std.Address, uint64, error)\n}\n\n// RoyaltyInfo represents royalty information for a token.\ntype RoyaltyInfo struct {\n\tPaymentAddress std.Address // PaymentAddress is the address where royalty payment should be sent.\n\tPercentage uint64 // Percentage is the royalty percentage. It indicates the percentage of royalty to be paid for each sale. For example : Percentage = 10 =\u003e 10%\n}\n"},{"name":"util.gno","body":"package grc721\n\nimport (\n\t\"std\"\n)\n\nvar zeroAddress = std.Address(\"\")\n\nfunc isValidAddress(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\treturn nil\n}\n\nfunc emit(event interface{}) {\n\t// TODO: setup a pubsub system here?\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc777","path":"gno.land/p/demo/grc/grc777","files":[{"name":"dummy_test.gno","body":"package grc777\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\ntype dummyImpl struct{}\n\n// FIXME: this should fail.\nvar _ IGRC777 = (*dummyImpl)(nil)\n\nfunc TestInterface(t *testing.T) {\n\tvar dummy IGRC777 = \u0026dummyImpl{}\n}\n\nfunc (impl *dummyImpl) GetName() string { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetSymbol() string { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetDecimals() uint { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Granularity() (granularity uint64) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) TotalSupply() (supply uint64) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) BalanceOf(address std.Address) uint64 { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Burn(amount uint64, data []byte) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) AuthorizeOperator(operator std.Address) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) RevokeOperator(operators std.Address) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) DefaultOperators() []std.Address { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Send(recipient std.Address, amount uint64, data []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) IsOperatorFor(operator, tokenHolder std.Address) bool {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorSend(sender, recipient std.Address, amount uint64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorBurn(account std.Address, amount uint64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n"},{"name":"igrc777.gno","body":"package grc777\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/exts\"\n)\n\n// TODO: use big.Int or a custom uint64 instead of uint64\n\ntype IGRC777 interface {\n\texts.TokenMetadata\n\n\t// Returns the smallest part of the token that is not divisible. This\n\t// means all token operations (creation, movement and destruction) must\n\t// have amounts that are a multiple of this number.\n\t//\n\t// For most token contracts, this value will equal 1.\n\tGranularity() (granularity uint64)\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() (supply uint64)\n\n\t// Returns the amount of tokens owned by an account (`owner`).\n\tBalanceOf(address std.Address) uint64\n\n\t// Moves `amount` tokens from the caller's account to `recipient`.\n\t//\n\t// If send or receive hooks are registered for the caller and `recipient`,\n\t// the corresponding functions will be called with `data` and empty\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tSend(recipient std.Address, amount uint64, data []byte)\n\n\t// Destroys `amount` tokens from the caller's account, reducing the\n\t// total supply.\n\t//\n\t// If a send hook is registered for the caller, the corresponding function\n\t// will be called with `data` and empty `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\tBurn(amount uint64, data []byte)\n\n\t// Returns true if an account is an operator of `tokenHolder`.\n\t// Operators can send and burn tokens on behalf of their owners. All\n\t// accounts are their own operator.\n\t//\n\t// See {operatorSend} and {operatorBurn}.\n\tIsOperatorFor(operator, tokenHolder std.Address) bool\n\n\t// Make an account an operator of the caller.\n\t//\n\t// See {isOperatorFor}.\n\t//\n\t// Emits an {AuthorizedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tAuthorizeOperator(operator std.Address)\n\n\t// Revoke an account's operator status for the caller.\n\t//\n\t// See {isOperatorFor} and {defaultOperators}.\n\t//\n\t// Emits a {RevokedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tRevokeOperator(operators std.Address)\n\n\t// Returns the list of default operators. These accounts are operators\n\t// for all token holders, even if {authorizeOperator} was never called on\n\t// them.\n\t//\n\t// This list is immutable, but individual holders may revoke these via\n\t// {revokeOperator}, in which case {isOperatorFor} will return false.\n\tDefaultOperators() []std.Address\n\n\t// Moves `amount` tokens from `sender` to `recipient`. The caller must\n\t// be an operator of `sender`.\n\t//\n\t// If send or receive hooks are registered for `sender` and `recipient`,\n\t// the corresponding functions will be called with `data` and\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - `sender` cannot be the zero address.\n\t// - `sender` must have at least `amount` tokens.\n\t// - the caller must be an operator for `sender`.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tOperatorSend(sender, recipient std.Address, amount uint64, data, operatorData []byte)\n\n\t// Destroys `amount` tokens from `account`, reducing the total supply.\n\t// The caller must be an operator of `account`.\n\t//\n\t// If a send hook is registered for `account`, the corresponding function\n\t// will be called with `data` and `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - `account` cannot be the zero address.\n\t// - `account` must have at least `amount` tokens.\n\t// - the caller must be an operator for `account`.\n\tOperatorBurn(account std.Address, amount uint64, data, operatorData []byte)\n}\n\n// Emitted when `amount` tokens are created by `operator` and assigned to `to`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype MintedEvent struct {\n\tOperator std.Address\n\tTo std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` destroys `amount` tokens from `account`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype BurnedEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` is made operator for `tokenHolder`\ntype AuthorizedOperatorEvent struct {\n\tOperator std.Address\n\tTokenHolder std.Address\n}\n\n// Emitted when `operator` is revoked its operator status for `tokenHolder`.\ntype RevokedOperatorEvent struct {\n\tOperator std.Address\n\tTokenHolder std.Address\n}\n\ntype SentEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"rat","path":"gno.land/p/demo/rat","files":[{"name":"maths.gno","body":"package rat\n\nconst (\n\tintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63) // 32 or 64\n\n\tMaxInt = 1\u003c\u003c(intSize-1) - 1\n\tMinInt = -1 \u003c\u003c (intSize - 1)\n\tMaxInt8 = 1\u003c\u003c7 - 1\n\tMinInt8 = -1 \u003c\u003c 7\n\tMaxInt16 = 1\u003c\u003c15 - 1\n\tMinInt16 = -1 \u003c\u003c 15\n\tMaxInt32 = 1\u003c\u003c31 - 1\n\tMinInt32 = -1 \u003c\u003c 31\n\tMaxInt64 = 1\u003c\u003c63 - 1\n\tMinInt64 = -1 \u003c\u003c 63\n\tMaxUint = 1\u003c\u003cintSize - 1\n\tMaxUint8 = 1\u003c\u003c8 - 1\n\tMaxUint16 = 1\u003c\u003c16 - 1\n\tMaxUint32 = 1\u003c\u003c32 - 1\n\tMaxUint64 = 1\u003c\u003c64 - 1\n)\n"},{"name":"rat.gno","body":"package rat\n\n//----------------------------------------\n// Rat fractions\n\n// represents a fraction.\ntype Rat struct {\n\tX int32\n\tY int32 // must be positive\n}\n\nfunc NewRat(x, y int32) Rat {\n\tif y \u003c= 0 {\n\t\tpanic(\"invalid std.Rat denominator\")\n\t}\n\treturn Rat{X: x, Y: y}\n}\n\nfunc (r1 Rat) IsValid() bool {\n\tif r1.Y \u003c= 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (r1 Rat) Cmp(r2 Rat) int {\n\tif !r1.IsValid() {\n\t\tpanic(\"invalid std.Rat left operand\")\n\t}\n\tif !r2.IsValid() {\n\t\tpanic(\"invalid std.Rat right operand\")\n\t}\n\tvar p1, p2 int64\n\tp1 = int64(r1.X) * int64(r2.Y)\n\tp2 = int64(r1.Y) * int64(r2.X)\n\tif p1 \u003c p2 {\n\t\treturn -1\n\t} else if p1 == p2 {\n\t\treturn 0\n\t} else {\n\t\treturn 1\n\t}\n}\n\n//func (r1 Rat) Plus(r2 Rat) Rat {\n// XXX\n//}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"txlink","path":"gno.land/p/moul/txlink","files":[{"name":"txlink.gno","body":"// Package txlink provides utilities for creating transaction-related links\n// compatible with Gnoweb, Gnobro, and other clients within the Gno ecosystem.\n//\n// This package is optimized for generating lightweight transaction links with\n// flexible arguments, allowing users to build dynamic links that integrate\n// seamlessly with various Gno clients.\n//\n// The primary function, URL, is designed to produce markdown links for\n// transaction functions in the current \"relative realm\". By specifying a custom\n// Realm, you can generate links that either use the current realm path or a\n// fully qualified path for another realm.\n//\n// This package is a streamlined alternative to helplink, providing similar\n// functionality for transaction links without the full feature set of helplink.\npackage txlink\n\nimport (\n\t\"std\"\n\t\"strings\"\n)\n\nconst chainDomain = \"gno.land\" // XXX: std.ChainDomain (#2911)\n\n// URL returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc URL(fn string, args ...string) string {\n\treturn Realm(\"\").URL(fn, args...)\n}\n\n// Realm represents a specific realm for generating tx links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\tcurPath := std.CurrentRealm().PkgPath()\n\t\treturn strings.TrimPrefix(curPath, chainDomain)\n\t}\n\n\t// local realm -\u003e /realm\n\trealm := string(r)\n\tif strings.Contains(realm, chainDomain) {\n\t\treturn strings.TrimPrefix(realm, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + string(r)\n}\n\n// URL returns a URL for the specified function with optional key-value\n// arguments.\nfunc (r Realm) URL(fn string, args ...string) string {\n\t// Start with the base query\n\turl := r.prefix() + \"$help\u0026func=\" + fn\n\n\t// Check if args length is even\n\tif len(args)%2 != 0 {\n\t\t// If not even, we can choose to handle the error here.\n\t\t// For example, we can just return the URL without appending\n\t\t// more args.\n\t\treturn url\n\t}\n\n\t// Append key-value pairs to the URL\n\tfor i := 0; i \u003c len(args); i += 2 {\n\t\tkey := args[i]\n\t\tvalue := args[i+1]\n\t\t// XXX: escape keys and args\n\t\turl += \"\u0026\" + key + \"=\" + value\n\t}\n\n\treturn url\n}\n"},{"name":"txlink_test.gno","body":"package txlink\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestURL(t *testing.T) {\n\ttests := []struct {\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"$help\u0026func=oddArgsFunc\", \"\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"https://gno.world/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tgot := tt.realm.URL(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"users","path":"gno.land/p/demo/users","files":[{"name":"types.gno","body":"package users\n\ntype AddressOrName string\n\nfunc (aon AddressOrName) IsName() bool {\n\treturn aon != \"\" \u0026\u0026 aon[0] == '@'\n}\n\nfunc (aon AddressOrName) GetName() (string, bool) {\n\tif len(aon) \u003e= 2 \u0026\u0026 aon[0] == '@' {\n\t\treturn string(aon[1:]), true\n\t}\n\treturn \"\", false\n}\n"},{"name":"users.gno","body":"package users\n\nimport (\n\t\"std\"\n\t\"strconv\"\n)\n\n//----------------------------------------\n// Types\n\ntype User struct {\n\tAddress std.Address\n\tName string\n\tProfile string\n\tNumber int\n\tInvites int\n\tInviter std.Address\n}\n\nfunc (u *User) Render() string {\n\tstr := \"## user \" + u.Name + \"\\n\" +\n\t\t\"\\n\" +\n\t\t\" * address = \" + string(u.Address) + \"\\n\" +\n\t\t\" * \" + strconv.Itoa(u.Invites) + \" invites\\n\"\n\tif u.Inviter != \"\" {\n\t\tstr = str + \" * invited by \" + string(u.Inviter) + \"\\n\"\n\t}\n\tstr = str + \"\\n\" +\n\t\tu.Profile + \"\\n\"\n\treturn str\n}\n"},{"name":"users_test.gno","body":"package users\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"users","path":"gno.land/r/demo/users","files":[{"name":"preregister.gno","body":"package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/users\"\n)\n\n// pre-restricted names\nvar preRestrictedNames = []string{\n\t\"bitcoin\", \"cosmos\", \"newtendermint\", \"ethereum\",\n}\n\n// pre-registered users\nvar preRegisteredUsers = []struct {\n\tName string\n\tAddress std.Address\n}{\n\t// system name\n\t{\"archives\", \"g1xlnyjrnf03ju82v0f98ruhpgnquk28knmjfe5k\"}, // -\u003e @r_archives\n\t{\"demo\", \"g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n\"}, // -\u003e @r_demo\n\t{\"gno\", \"g19602kd9tfxrfd60sgreadt9zvdyyuudcyxsz8a\"}, // -\u003e @r_gno\n\t{\"gnoland\", \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"}, // -\u003e @r_gnoland\n\t{\"gnolang\", \"g1yjlnm3z2630gg5mryjd79907e0zx658wxs9hnd\"}, // -\u003e @r_gnolang\n\t{\"gov\", \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"}, // -\u003e @r_gov\n\t{\"nt\", \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"}, // -\u003e @r_nt\n\t{\"sys\", \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"}, // -\u003e @r_sys\n\t{\"x\", \"g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz\"}, // -\u003e @r_x\n\n\t// test1 user\n\t{\"test1\", \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"}, // -\u003e @test1\n\n\t// Onbloc\n\t{\"gnoswap\", \"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c\"}, // -\u003e @r_gnoswap\n\t{\"onbloc\", \"g12vx7dn3dqq89mz550zwunvg4qw6epq73d9csay\"}, // -\u003e @r_onbloc\n\n\t// Dragos\n\t{\"flippando\", \"g1z82x8mxa0pz5s9u7csy6zya4x0ut9uw6p7d8dk\"}, // -\u003e @r_flippando\n\t{\"zentasktic\", \"g1paxgmwy2wzhx0l6qvav2p8thvphc5c030xz35c\"}, // -\u003e @r_zentasktic\n}\n\nfunc init() {\n\t// add pre-registered users\n\tfor _, res := range preRegisteredUsers {\n\t\t// assert not already registered.\n\t\t_, ok := name2User.Get(res.Name)\n\t\tif ok {\n\t\t\tpanic(\"name already registered\")\n\t\t}\n\n\t\t_, ok = addr2User.Get(res.Address.String())\n\t\tif ok {\n\t\t\tpanic(\"address already registered\")\n\t\t}\n\n\t\tcounter++\n\t\tuser := \u0026users.User{\n\t\t\tAddress: res.Address,\n\t\t\tName: res.Name,\n\t\t\tProfile: \"\",\n\t\t\tNumber: counter,\n\t\t\tInvites: int(0),\n\t\t\tInviter: admin,\n\t\t}\n\t\tname2User.Set(res.Name, user)\n\t\taddr2User.Set(res.Address.String(), user)\n\t}\n\n\t// add pre-restricted names\n\tfor _, name := range preRestrictedNames {\n\t\tif _, ok := name2User.Get(name); ok {\n\t\t\tpanic(\"name already registered\")\n\t\t}\n\n\t\trestricted.Set(name, true)\n\t}\n}\n"},{"name":"users.gno","body":"package users\n\nimport (\n\t\"regexp\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/avlhelpers\"\n\t\"gno.land/p/demo/users\"\n)\n\n//----------------------------------------\n// State\n\nvar (\n\tadmin std.Address = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\" // @moul\n\n\trestricted avl.Tree // Name -\u003e true - restricted name\n\tname2User avl.Tree // Name -\u003e *users.User\n\taddr2User avl.Tree // std.Address -\u003e *users.User\n\tinvites avl.Tree // string(inviter+\":\"+invited) -\u003e true\n\tcounter int // user id counter\n\tminFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register.\n\tmaxFeeMult int64 = 10 // maximum multiples of minFee accepted.\n)\n\n//----------------------------------------\n// Top-level functions\n\nfunc Register(inviter std.Address, name string, profile string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert invited or paid.\n\tcaller := std.GetCallerAt(2)\n\tif caller != std.GetOrigCaller() {\n\t\tpanic(\"should not happen\") // because std.AssertOrigCall().\n\t}\n\n\tsentCoins := std.GetOrigSend()\n\tminCoin := std.NewCoin(\"ugnot\", minFee)\n\n\tif inviter == \"\" {\n\t\t// banker := std.GetBanker(std.BankerTypeOrigSend)\n\t\tif len(sentCoins) == 1 \u0026\u0026 sentCoins[0].IsGTE(minCoin) {\n\t\t\tif sentCoins[0].Amount \u003e minFee*maxFeeMult {\n\t\t\t\tpanic(\"payment must not be greater than \" + strconv.Itoa(int(minFee*maxFeeMult)))\n\t\t\t} else {\n\t\t\t\t// ok\n\t\t\t}\n\t\t} else {\n\t\t\tpanic(\"payment must not be less than \" + strconv.Itoa(int(minFee)))\n\t\t}\n\t} else {\n\t\tinvitekey := inviter.String() + \":\" + caller.String()\n\t\t_, ok := invites.Get(invitekey)\n\t\tif !ok {\n\t\t\tpanic(\"invalid invitation\")\n\t\t}\n\t\tinvites.Remove(invitekey)\n\t}\n\n\t// assert not already registered.\n\t_, ok := name2User.Get(name)\n\tif ok {\n\t\tpanic(\"name already registered: \" + name)\n\t}\n\t_, ok = addr2User.Get(caller.String())\n\tif ok {\n\t\tpanic(\"address already registered: \" + caller.String())\n\t}\n\n\tisInviterAdmin := inviter == admin\n\n\t// check for restricted name\n\tif _, isRestricted := restricted.Get(name); isRestricted {\n\t\t// only address invite by the admin can register restricted name\n\t\tif !isInviterAdmin {\n\t\t\tpanic(\"restricted name: \" + name)\n\t\t}\n\n\t\trestricted.Remove(name)\n\t}\n\n\t// assert name is valid.\n\t// admin inviter can bypass name restriction\n\tif !isInviterAdmin \u0026\u0026 !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name + \" (must be at least 6 characters, lowercase alphanumeric with underscore)\")\n\t}\n\n\t// remainder of fees go toward invites.\n\tinvites := int(0)\n\tif len(sentCoins) == 1 {\n\t\tif sentCoins[0].Denom == \"ugnot\" \u0026\u0026 sentCoins[0].Amount \u003e= minFee {\n\t\t\tinvites = int(sentCoins[0].Amount / minFee)\n\t\t\tif inviter == \"\" \u0026\u0026 invites \u003e 0 {\n\t\t\t\tinvites -= 1\n\t\t\t}\n\t\t}\n\t}\n\t// register.\n\tcounter++\n\tuser := \u0026users.User{\n\t\tAddress: caller,\n\t\tName: name,\n\t\tProfile: profile,\n\t\tNumber: counter,\n\t\tInvites: invites,\n\t\tInviter: inviter,\n\t}\n\tname2User.Set(name, user)\n\taddr2User.Set(caller.String(), user)\n}\n\nfunc Invite(invitee string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// get caller/inviter.\n\tcaller := std.GetCallerAt(2)\n\tif caller != std.GetOrigCaller() {\n\t\tpanic(\"should not happen\") // because std.AssertOrigCall().\n\t}\n\tlines := strings.Split(invitee, \"\\n\")\n\tif caller == admin {\n\t\t// nothing to do, all good\n\t} else {\n\t\t// ensure has invites.\n\t\tuserI, ok := addr2User.Get(caller.String())\n\t\tif !ok {\n\t\t\tpanic(\"user unknown\")\n\t\t}\n\t\tuser := userI.(*users.User)\n\t\tif user.Invites \u003c= 0 {\n\t\t\tpanic(\"user has no invite tokens\")\n\t\t}\n\t\tuser.Invites -= len(lines)\n\t\tif user.Invites \u003c 0 {\n\t\t\tpanic(\"user has insufficient invite tokens\")\n\t\t}\n\t}\n\t// for each line...\n\tfor _, line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue // file bodies have a trailing newline.\n\t\t} else if strings.HasPrefix(line, `//`) {\n\t\t\tcontinue // comment\n\t\t}\n\t\t// record invite.\n\t\tinvitekey := string(caller) + \":\" + string(line)\n\t\tinvites.Set(invitekey, true)\n\t}\n}\n\nfunc GrantInvites(invites string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert admin.\n\tcaller := std.GetCallerAt(2)\n\tif caller != std.GetOrigCaller() {\n\t\tpanic(\"should not happen\") // because std.AssertOrigCall().\n\t}\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\t// for each line...\n\tlines := strings.Split(invites, \"\\n\")\n\tfor _, line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue // file bodies have a trailing newline.\n\t\t} else if strings.HasPrefix(line, `//`) {\n\t\t\tcontinue // comment\n\t\t}\n\t\t// parse name and invites.\n\t\tvar name string\n\t\tvar invites int\n\t\tparts := strings.Split(line, \":\")\n\t\tif len(parts) == 1 { // short for :1.\n\t\t\tname = parts[0]\n\t\t\tinvites = 1\n\t\t} else if len(parts) == 2 {\n\t\t\tname = parts[0]\n\t\t\tinvites_, err := strconv.Atoi(parts[1])\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tinvites = int(invites_)\n\t\t} else {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t\t// give invites.\n\t\tuserI, ok := name2User.Get(name)\n\t\tif !ok {\n\t\t\t// maybe address.\n\t\t\tuserI, ok = addr2User.Get(name)\n\t\t\tif !ok {\n\t\t\t\tpanic(\"invalid user \" + name)\n\t\t\t}\n\t\t}\n\t\tuser := userI.(*users.User)\n\t\tuser.Invites += invites\n\t}\n}\n\n// Any leftover fees go toward invitations.\nfunc SetMinFee(newMinFee int64) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert admin caller.\n\tcaller := std.GetCallerAt(2)\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\t// update global variables.\n\tminFee = newMinFee\n}\n\n// This helps prevent fat finger accidents.\nfunc SetMaxFeeMultiple(newMaxFeeMult int64) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// assert admin caller.\n\tcaller := std.GetCallerAt(2)\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\t// update global variables.\n\tmaxFeeMult = newMaxFeeMult\n}\n\n//----------------------------------------\n// Exposed public functions\n\nfunc GetUserByName(name string) *users.User {\n\tuserI, ok := name2User.Get(name)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn userI.(*users.User)\n}\n\nfunc GetUserByAddress(addr std.Address) *users.User {\n\tuserI, ok := addr2User.Get(addr.String())\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn userI.(*users.User)\n}\n\n// unlike GetUserByName, input must be \"@\" prefixed for names.\nfunc GetUserByAddressOrName(input users.AddressOrName) *users.User {\n\tname, isName := input.GetName()\n\tif isName {\n\t\treturn GetUserByName(name)\n\t}\n\treturn GetUserByAddress(std.Address(input))\n}\n\n// Get a list of user names starting from the given prefix. Limit the\n// number of results to maxResults. (This can be used for a name search tool.)\nfunc ListUsersByPrefix(prefix string, maxResults int) []string {\n\treturn avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults)\n}\n\nfunc Resolve(input users.AddressOrName) std.Address {\n\tname, isName := input.GetName()\n\tif !isName {\n\t\treturn std.Address(input) // TODO check validity\n\t}\n\n\tuser := GetUserByName(name)\n\treturn user.Address\n}\n\n// Add restricted name to the list\nfunc AdminAddRestrictedName(name string) {\n\t// assert CallTx call.\n\tstd.AssertOriginCall()\n\t// get caller\n\tcaller := std.GetOrigCaller()\n\t// assert admin\n\tif caller != admin {\n\t\tpanic(\"unauthorized\")\n\t}\n\n\tif user := GetUserByName(name); user != nil {\n\t\tpanic(\"already registered name\")\n\t}\n\n\t// register restricted name\n\n\trestricted.Set(name, true)\n}\n\n//----------------------------------------\n// Constants\n\n// NOTE: name length must be clearly distinguishable from a bech32 address.\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`)\n\n//----------------------------------------\n// Render main page\n\nfunc Render(fullPath string) string {\n\tpath, _ := splitPathAndQuery(fullPath)\n\tif path == \"\" {\n\t\treturn renderHome(fullPath)\n\t} else if len(path) \u003e= 38 { // 39? 40?\n\t\tif path[:2] != \"g1\" {\n\t\t\treturn \"invalid address \" + path\n\t\t}\n\t\tuser := GetUserByAddress(std.Address(path))\n\t\tif user == nil {\n\t\t\t// TODO: display basic information about account.\n\t\t\treturn \"unknown address \" + path\n\t\t}\n\t\treturn user.Render()\n\t} else {\n\t\tuser := GetUserByName(path)\n\t\tif user == nil {\n\t\t\treturn \"unknown username \" + path\n\t\t}\n\t\treturn user.Render()\n\t}\n}\n\nfunc renderHome(path string) string {\n\tdoc := \"\"\n\n\tpage := pager.NewPager(\u0026name2User, 50).MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\tuser := item.Value.(*users.User)\n\t\tdoc += \" * [\" + user.Name + \"](/r/demo/users:\" + user.Name + \")\\n\"\n\t}\n\tdoc += \"\\n\"\n\tdoc += page.Selector()\n\treturn doc\n}\n\nfunc splitPathAndQuery(fullPath string) (string, string) {\n\tparts := strings.SplitN(fullPath, \"?\", 2)\n\tpath := parts[0]\n\tqueryString := \"\"\n\tif len(parts) \u003e 1 {\n\t\tqueryString = \"?\" + parts[1]\n\t}\n\treturn path, queryString\n}\n"},{"name":"users_test.gno","body":"package users\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPreRegisteredTest1(t *testing.T) {\n\tnames := ListUsersByPrefix(\"test1\", 1)\n\tuassert.Equal(t, len(names), 1)\n\tuassert.Equal(t, names[0], \"test1\")\n}\n"},{"name":"z_0_b_filetest.gno","body":"package main\n\n// SEND: 19900000ugnot\n\nimport (\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// payment must not be less than 20000000\n"},{"name":"z_0_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tstd.TestSetOrigSend(std.Coins{std.NewCoin(\"dontcare\", 1)}, nil)\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// incompatible coin denominations: dontcare, ugnot\n"},{"name":"z_10_filetest.gno","body":"// PKGPATH: gno.land/r/demo/users_test\npackage users_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc init() {\n\tcaller := std.GetOrigCaller() // main\n\ttest2 := testutils.TestAddress(\"test2\")\n\t// as admin, invite gnouser and test2\n\tstd.TestSetOrigCaller(admin)\n\tusers.Invite(caller.String() + \"\\n\" + test2.String())\n\t// register as caller\n\tstd.TestSetOrigCaller(caller)\n\tusers.Register(admin, \"gnouser\", \"my profile\")\n}\n\nfunc main() {\n\t// register as test2\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(admin, \"test222\", \"my profile 2\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_11_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tstd.TestSetOrigCaller(admin)\n\tusers.AdminAddRestrictedName(\"superrestricted\")\n\n\t// test restricted name\n\tstd.TestSetOrigCaller(caller)\n\tusers.Register(\"\", \"superrestricted\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// restricted name: superrestricted\n"},{"name":"z_11b_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tstd.TestSetOrigCaller(admin)\n\t// add restricted name\n\tusers.AdminAddRestrictedName(\"superrestricted\")\n\t// grant invite to caller\n\tusers.Invite(caller.String())\n\t// set back caller\n\tstd.TestSetOrigCaller(caller)\n\t// register restricted name with admin invite\n\tusers.Register(admin, \"superrestricted\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_12_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"alicia\", \"my profile\")\n\n\t{\n\t\t// Normal usage\n\t\tnames := users.ListUsersByPrefix(\"a\", 1)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t\tprintln(\"name: \" + names[0])\n\t}\n\n\t{\n\t\t// Empty prefix: match all\n\t\tnames := users.ListUsersByPrefix(\"\", 1)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t\tprintln(\"name: \" + names[0])\n\t}\n\n\t{\n\t\t// The prefix is before \"alicia\"\n\t\tnames := users.ListUsersByPrefix(\"alich\", 1)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t}\n\n\t{\n\t\t// The prefix is after the last name\n\t\tnames := users.ListUsersByPrefix(\"y\", 10)\n\t\tprintln(\"# names: \" + strconv.Itoa(len(names)))\n\t}\n\n\t// More tests are in p/demo/avlhelpers\n}\n\n// Output:\n// # names: 1\n// name: alicia\n// # names: 1\n// name: alicia\n// # names: 0\n// # names: 0\n"},{"name":"z_1_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_2_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_3_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_4_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\ttest2 := testutils.TestAddress(\"test2\")\n\tusers.Invite(test1.String())\n\t// switch to test2 (not test1)\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(\"done\")\n}\n\n// Error:\n// invalid invitation\n"},{"name":"z_5_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\tprintln(users.Render(\"\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"?page=2\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"gnouser\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"satoshi\"))\n\tprintln(\"========================================\")\n\tprintln(users.Render(\"badname\"))\n}\n\n// Output:\n// * [archives](/r/demo/users:archives)\n// * [demo](/r/demo/users:demo)\n// * [gno](/r/demo/users:gno)\n// * [gnoland](/r/demo/users:gnoland)\n// * [gnolang](/r/demo/users:gnolang)\n// * [gnouser](/r/demo/users:gnouser)\n// * [gov](/r/demo/users:gov)\n// * [nt](/r/demo/users:nt)\n// * [satoshi](/r/demo/users:satoshi)\n// * [sys](/r/demo/users:sys)\n// * [test1](/r/demo/users:test1)\n// * [x](/r/demo/users:x)\n//\n//\n// ========================================\n//\n//\n// ========================================\n// ## user gnouser\n//\n// * address = g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// * 9 invites\n//\n// my profile\n//\n// ========================================\n// ## user satoshi\n//\n// * address = g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7\n// * 0 invites\n// * invited by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// my other profile\n//\n// ========================================\n// unknown username badname\n"},{"name":"z_6_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller()\n\t// as admin, grant invites to unregistered user.\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Error:\n// invalid user g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n"},{"name":"z_7_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\t// as admin, grant invites to gnouser(again) and satoshi.\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\\n\" + test1.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_7b_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\\n\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\t// as admin, grant invites to gnouser(again) and satoshi.\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\\n\" + test1.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"},{"name":"z_8_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// as admin, grant invites to gnouser\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"test1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tstd.TestSetOrigSend(std.Coins{{\"dontcare\", 1}}, nil)\n\tusers.Register(caller, \"satoshi\", \"my other profile\")\n\t// as admin, grant invites to gnouser(again) and nonexistent user.\n\tstd.TestSetOrigCaller(admin)\n\ttest2 := testutils.TestAddress(\"test2\")\n\tusers.GrantInvites(caller.String() + \":1\\n\" + test2.String() + \":1\")\n\tprintln(\"done\")\n}\n\n// Error:\n// invalid user g1w3jhxapjta047h6lta047h6lta047h6laqcyu4\n"},{"name":"z_9_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\ttest2 := testutils.TestAddress(\"test2\")\n\t// as admin, invite gnouser and test2\n\tstd.TestSetOrigCaller(admin)\n\tusers.Invite(caller.String() + \"\\n\" + test2.String())\n\t// register as caller\n\tstd.TestSetOrigCaller(caller)\n\tusers.Register(admin, \"gnouser\", \"my profile\")\n\t// register as test2\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(admin, \"test222\", \"my profile 2\")\n\tprintln(\"done\")\n}\n\n// Output:\n// done\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"boards","path":"gno.land/r/demo/boards","files":[{"name":"README.md","body":"This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `-remote localhost:26657` here, that flag can be replaced\nwith `-remote gno.land:26657` if you have $GNOT on the testnet.\n(To use the testnet, also replace `-chainid dev` with `-chainid portal-loop` .)\n\n### Build `gnokey` (and other tools).\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd gno/gno.land\nmake build\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add -recover KEYNAME\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\nTake note of your `addr` which looks something like `g17sphqax3kasjptdkmuqvn740u8dhtx4kxl6ljf` .\nYou will use this as your `ACCOUNT_ADDR`.\n\n## Interact with the blockchain.\n\n### Add $GNOT for your account.\n\nBefore starting the `gnoland` node for the first time, your new account can be given $GNOT in the node genesis.\nEdit the file `gno.land/genesis/genesis_balances.txt` and add the following line (simlar to the others), using\nyour `ACCOUNT_ADDR` and `KEYNAME`\n\n`ACCOUNT_ADDR=10000000000ugnot # @KEYNAME`\n\n### Alternative: Run a faucet to add $GNOT.\n\nInstead of editing `gno.land/genesis/genesis_balances.txt`, a more general solution (with more steps)\nis to run a local \"faucet\" and use the web browser to add $GNOT. (This can be done at any time.)\nSee this page: https://github.com/gnolang/gno/blob/master/contribs/gnofaucet/README.md\n\n\n### Start the `gnoland` node.\n\n```bash\n./build/gnoland start\n```\n\nNOTE: The node already has the \"boards\" realm.\n\nLeave this running in the terminal. In a new terminal, cd to the same folder `gno/gno.land` .\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR -remote localhost:26657\n```\n\n### Register a board username with a smart contract call.\n\nThe `USERNAME` for posting can different than your `KEYNAME`. It is internally linked to your `ACCOUNT_ADDR`. It must be at least 6 characters, lowercase alphanumeric with underscore.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/users\" -func \"Register\" -args \"\" -args \"USERNAME\" -args \"Profile description\" -gas-fee \"10000000ugnot\" -gas-wanted \"2000000\" -send \"200000000ugnot\" -broadcast -chainid dev -remote 127.0.0.1:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/users$help\u0026func=Register\n\n### Create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/boards\" -func \"CreateBoard\" -args \"BOARDNAME\" -gas-fee \"1000000ugnot\" -gas-wanted \"10000000\" -broadcast -chainid dev -remote localhost:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" -data 'gno.land/r/demo/boards.GetBoardIDFromName(\"BOARDNAME\")' -remote localhost:26657\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/boards\" -func \"CreateThread\" -args BOARD_ID -args \"Hello gno.land\" -args \"Text of the post\" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call -pkgpath \"gno.land/r/demo/boards\" -func \"CreateReply\" -args BOARD_ID -args \"1\" -args \"1\" -args \"Nice to meet you too.\" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" -data \"gno.land/r/demo/boards:BOARDNAME/1\" -remote localhost:26657\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" -data \"gno.land/r/demo/boards:gnolang\"\n```\n## View the board in the browser.\n\n### Start the web server.\n\n```bash\n./build/gnoweb\n```\n\nThis should print something like `Running on http://127.0.0.1:8888` . Leave this running in the terminal.\n\n### View in the browser\n\nIn your browser, navigate to the printed address http://127.0.0.1:8888 .\nTo see you post, click on the package `/r/demo/boards` .\n"},{"name":"board.gno","body":"package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/txlink\"\n)\n\n//----------------------------------------\n// Board\n\ntype BoardID uint64\n\nfunc (bid BoardID) String() string {\n\treturn strconv.Itoa(int(bid))\n}\n\ntype Board struct {\n\tid BoardID // only set for public boards.\n\turl string\n\tname string\n\tcreator std.Address\n\tthreads avl.Tree // Post.id -\u003e *Post\n\tpostsCtr uint64 // increments Post.id\n\tcreatedAt time.Time\n\tdeleted avl.Tree // TODO reserved for fast-delete.\n}\n\nfunc newBoard(id BoardID, url string, name string, creator std.Address) *Board {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\texists := gBoardsByName.Has(name)\n\tif exists {\n\t\tpanic(\"board already exists\")\n\t}\n\treturn \u0026Board{\n\t\tid: id,\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\tthreads: avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t\tdeleted: avl.Tree{},\n\t}\n}\n\n/* TODO support this once we figure out how to ensure URL correctness.\n// A private board is not tracked by gBoards*,\n// but must be persisted by the caller's realm.\n// Private boards have 0 id and does not ping\n// back the remote board on reposts.\nfunc NewPrivateBoard(url string, name string, creator std.Address) *Board {\n\treturn newBoard(0, url, name, creator)\n}\n*/\n\nfunc (board *Board) IsPrivate() bool {\n\treturn board.id == 0\n}\n\nfunc (board *Board) GetThread(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpostI, exists := board.threads.Get(pidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn postI.(*Post)\n}\n\nfunc (board *Board) AddThread(creator std.Address, title string, body string) *Post {\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\tthread := newPost(board, pid, creator, title, body, pid, 0, 0)\n\tboard.threads.Set(pidkey, thread)\n\treturn thread\n}\n\n// NOTE: this can be potentially very expensive for threads with many replies.\n// TODO: implement optional fast-delete where thread is simply moved.\nfunc (board *Board) DeleteThread(pid PostID) {\n\tpidkey := postIDKey(pid)\n\t_, removed := board.threads.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"thread does not exist with id \" + pid.String())\n\t}\n}\n\nfunc (board *Board) HasPermission(addr std.Address, perm Permission) bool {\n\tif board.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// Renders the board for display suitable as plaintext in\n// console. This is suitable for demonstration or tests,\n// but not for prod.\nfunc (board *Board) RenderBoard() string {\n\tstr := \"\"\n\tstr += \"\\\\[[post](\" + board.GetPostFormURL() + \")]\\n\\n\"\n\tif board.threads.Size() \u003e 0 {\n\t\tboard.threads.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tif str != \"\" {\n\t\t\t\tstr += \"----------------------------------------\\n\"\n\t\t\t}\n\t\t\tstr += value.(*Post).RenderSummary() + \"\\n\"\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\nfunc (board *Board) incGetPostID() PostID {\n\tboard.postsCtr++\n\treturn PostID(board.postsCtr)\n}\n\nfunc (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string {\n\tif replyID == 0 {\n\t\treturn board.url + \"/\" + threadID.String()\n\t} else {\n\t\treturn board.url + \"/\" + threadID.String() + \"/\" + replyID.String()\n\t}\n}\n\nfunc (board *Board) GetPostFormURL() string {\n\treturn txlink.URL(\"CreateThread\", \"bid\", board.id.String())\n}\n"},{"name":"boards.gno","body":"package boards\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgBoards avl.Tree // id -\u003e *Board\n\tgBoardsCtr int // increments Board.id\n\tgBoardsByName avl.Tree // name -\u003e *Board\n\tgDefaultAnonFee = 100000000 // minimum fee required if anonymous\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n"},{"name":"misc.gno","body":"package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/demo/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getBoard(bid BoardID) *Board {\n\tbidkey := boardIDKey(bid)\n\tboard_, exists := gBoards.Get(bidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\tboard := board_.(*Board)\n\treturn board\n}\n\nfunc incGetBoardID() BoardID {\n\tgBoardsCtr++\n\treturn BoardID(gBoardsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\" \", length-len(str)) + str\n\t}\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc boardIDKey(bid BoardID) string {\n\treturn padZero(uint64(bid), 10)\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/r/demo/users:\" + addr.String() + \")\"\n\t} else {\n\t\treturn \"[@\" + user.Name + \"](/r/demo/users:\" + user.Name + \")\"\n\t}\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t}\n\treturn user.Name\n}\n"},{"name":"post.gno","body":"package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/txlink\"\n)\n\n//----------------------------------------\n// Post\n\n// NOTE: a PostID is relative to the board.\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\n// A Post is a \"thread\" or a \"reply\" depending on context.\n// A thread is a Post of a Board that holds other replies.\ntype Post struct {\n\tboard *Board\n\tid PostID\n\tcreator std.Address\n\ttitle string // optional\n\tbody string\n\treplies avl.Tree // Post.id -\u003e *Post\n\trepliesAll avl.Tree // Post.id -\u003e *Post (all replies, for top-level posts)\n\treposts avl.Tree // Board.id -\u003e Post.id\n\tthreadID PostID // original Post.id\n\tparentID PostID // parent Post.id (if reply or repost)\n\trepostBoard BoardID // original Board.id (if repost)\n\tcreatedAt time.Time\n\tupdatedAt time.Time\n}\n\nfunc newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {\n\treturn \u0026Post{\n\t\tboard: board,\n\t\tid: id,\n\t\tcreator: creator,\n\t\ttitle: title,\n\t\tbody: body,\n\t\treplies: avl.Tree{},\n\t\trepliesAll: avl.Tree{},\n\t\treposts: avl.Tree{},\n\t\tthreadID: threadID,\n\t\tparentID: parentID,\n\t\trepostBoard: repostBoard,\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (post *Post) IsThread() bool {\n\treturn post.parentID == 0\n}\n\nfunc (post *Post) GetPostID() PostID {\n\treturn post.id\n}\n\nfunc (post *Post) AddReply(creator std.Address, body string) *Post {\n\tboard := post.board\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\treply := newPost(board, pid, creator, \"\", body, post.threadID, post.id, 0)\n\tpost.replies.Set(pidkey, reply)\n\tif post.threadID == post.id {\n\t\tpost.repliesAll.Set(pidkey, reply)\n\t} else {\n\t\tthread := board.GetThread(post.threadID)\n\t\tthread.repliesAll.Set(pidkey, reply)\n\t}\n\treturn reply\n}\n\nfunc (post *Post) Update(title string, body string) {\n\tpost.title = title\n\tpost.body = body\n\tpost.updatedAt = time.Now()\n}\n\nfunc (thread *Post) GetReply(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\treplyI, ok := thread.repliesAll.Get(pidkey)\n\tif !ok {\n\t\treturn nil\n\t} else {\n\t\treturn replyI.(*Post)\n\t}\n}\n\nfunc (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post {\n\tif !post.IsThread() {\n\t\tpanic(\"cannot repost non-thread post\")\n\t}\n\tpid := dst.incGetPostID()\n\tpidkey := postIDKey(pid)\n\trepost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)\n\tdst.threads.Set(pidkey, repost)\n\tif !dst.IsPrivate() {\n\t\tbidkey := boardIDKey(dst.id)\n\t\tpost.reposts.Set(bidkey, pid)\n\t}\n\treturn repost\n}\n\nfunc (thread *Post) DeletePost(pid PostID) {\n\tif thread.id == pid {\n\t\tpanic(\"should not happen\")\n\t}\n\tpidkey := postIDKey(pid)\n\tpostI, removed := thread.repliesAll.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"post not found in thread\")\n\t}\n\tpost := postI.(*Post)\n\tif post.parentID != thread.id {\n\t\tparent := thread.GetReply(post.parentID)\n\t\tparent.replies.Remove(pidkey)\n\t} else {\n\t\tthread.replies.Remove(pidkey)\n\t}\n}\n\nfunc (post *Post) HasPermission(addr std.Address, perm Permission) bool {\n\tif post.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\t// post notes inherit permissions of the board.\n\treturn post.board.HasPermission(addr, perm)\n}\n\nfunc (post *Post) GetSummary() string {\n\treturn summaryOf(post.body, 80)\n}\n\nfunc (post *Post) GetURL() string {\n\tif post.IsThread() {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.id, 0)\n\t} else {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.threadID, post.id)\n\t}\n}\n\nfunc (post *Post) GetReplyFormURL() string {\n\treturn txlink.URL(\"CreateReply\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetRepostFormURL() string {\n\treturn txlink.URL(\"CreateRepost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetDeleteFormURL() string {\n\treturn txlink.URL(\"DeletePost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) RenderSummary() string {\n\tif post.repostBoard != 0 {\n\t\tdstBoard := getBoard(post.repostBoard)\n\t\tif dstBoard == nil {\n\t\t\tpanic(\"repostBoard does not exist\")\n\t\t}\n\t\tthread := dstBoard.GetThread(PostID(post.parentID))\n\t\tif thread == nil {\n\t\t\treturn \"reposted post does not exist\"\n\t\t}\n\t\treturn \"Repost: \" + post.GetSummary() + \"\\n\" + thread.RenderSummary()\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += \"## [\" + summaryOf(post.title, 80) + \"](\" + post.GetURL() + \")\\n\"\n\t\tstr += \"\\n\"\n\t}\n\tstr += post.GetSummary() + \"\\n\"\n\tstr += \"\\\\- \" + displayAddressMD(post.creator) + \",\"\n\tstr += \" [\" + post.createdAt.Format(\"2006-01-02 3:04pm MST\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\"\n\tstr += \" (\" + strconv.Itoa(post.replies.Size()) + \" replies)\"\n\tstr += \" (\" + strconv.Itoa(post.reposts.Size()) + \" reposts)\" + \"\\n\"\n\treturn str\n}\n\nfunc (post *Post) RenderPost(indent string, levels int) string {\n\tif post == nil {\n\t\treturn \"nil post\"\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += indent + \"# \" + post.title + \"\\n\"\n\t\tstr += indent + \"\\n\"\n\t}\n\tstr += indentBody(indent, post.body) + \"\\n\" // TODO: indent body lines.\n\tstr += indent + \"\\\\- \" + displayAddressMD(post.creator) + \", \"\n\tstr += \"[\" + post.createdAt.Format(\"2006-01-02 3:04pm (MST)\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[reply](\" + post.GetReplyFormURL() + \")]\"\n\tif post.IsThread() {\n\t\tstr += \" \\\\[[repost](\" + post.GetRepostFormURL() + \")]\"\n\t}\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\\n\"\n\tif levels \u003e 0 {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tpost.replies.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\t\tstr += indent + \"\\n\"\n\t\t\t\tstr += value.(*Post).RenderPost(indent+\"\u003e \", levels-1)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t} else {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tstr += indent + \"\\n\"\n\t\t\tstr += indent + \"_[see all \" + strconv.Itoa(post.replies.Size()) + \" replies](\" + post.GetURL() + \")_\\n\"\n\t\t}\n\t}\n\treturn str\n}\n\n// render reply and link to context thread\nfunc (post *Post) RenderInner() string {\n\tif post.IsThread() {\n\t\tpanic(\"unexpected thread\")\n\t}\n\tthreadID := post.threadID\n\t// replyID := post.id\n\tparentID := post.parentID\n\tstr := \"\"\n\tstr += \"_[see thread](\" + post.board.GetURLFromThreadAndReplyID(\n\t\tthreadID, 0) + \")_\\n\\n\"\n\tthread := post.board.GetThread(post.threadID)\n\tvar parent *Post\n\tif thread.id == parentID {\n\t\tparent = thread\n\t} else {\n\t\tparent = thread.GetReply(parentID)\n\t}\n\tstr += parent.RenderPost(\"\", 0)\n\tstr += \"\\n\"\n\tstr += post.RenderPost(\"\u003e \", 5)\n\treturn str\n}\n"},{"name":"public.gno","body":"package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetBoardIDFromName(name string) (BoardID, bool) {\n\tboardI, exists := gBoardsByName.Get(name)\n\tif !exists {\n\t\treturn 0, false\n\t}\n\treturn boardI.(*Board).id, true\n}\n\nfunc CreateBoard(name string) BoardID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tbid := incGetBoardID()\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tpanic(\"unauthorized\")\n\t}\n\turl := \"/r/demo/boards:\" + name\n\tboard := newBoard(bid, url, name, caller)\n\tbidkey := boardIDKey(bid)\n\tgBoards.Set(bidkey, board)\n\tgBoardsByName.Set(name, board)\n\treturn board.id\n}\n\nfunc checkAnonFee() bool {\n\tsent := std.GetOrigSend()\n\tanonFeeCoin := std.NewCoin(\"ugnot\", int64(gDefaultAnonFee))\n\tif len(sent) == 1 \u0026\u0026 sent[0].IsGTE(anonFeeCoin) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc CreateThread(bid BoardID, title string, body string) PostID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.AddThread(caller, title, body)\n\treturn thread.id\n}\n\nfunc CreateReply(bid BoardID, threadid, postid PostID, body string) PostID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\treply := thread.AddReply(caller, body)\n\t\treturn reply.id\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\treply := post.AddReply(caller, body)\n\t\treturn reply.id\n\t}\n}\n\n// If dstBoard is private, does not ping back.\n// If board specified by bid is private, panics.\nfunc CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tif usernameOf(caller) == \"\" {\n\t\t// TODO: allow with gDefaultAnonFee payment.\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"src board not exist\")\n\t}\n\tif board.IsPrivate() {\n\t\tpanic(\"cannot repost from a private board\")\n\t}\n\tdst := getBoard(dstBoardID)\n\tif dst == nil {\n\t\tpanic(\"dst board not exist\")\n\t}\n\tthread := board.GetThread(postid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\trepost := thread.AddRepostTo(caller, title, body, dst)\n\treturn repost.id\n}\n\nfunc DeletePost(bid BoardID, threadid, postid PostID, reason string) {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// delete thread\n\t\tif !thread.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tboard.DeleteThread(threadid)\n\t} else {\n\t\t// delete thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.DeletePost(postid)\n\t}\n}\n\nfunc EditPost(bid BoardID, threadid, postid PostID, title, body string) {\n\tif !(std.IsOriginCall() || std.PrevRealm().IsUser()) {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.GetOrigCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// edit thread\n\t\tif !thread.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.Update(title, body)\n\t} else {\n\t\t// edit thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tpost.Update(title, body)\n\t}\n}\n"},{"name":"render.gno","body":"package boards\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderBoard(bid BoardID) string {\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\treturn \"missing board\"\n\t}\n\treturn board.RenderBoard()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"These are all the boards of this realm:\\n\\n\"\n\t\tgBoards.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tboard := value.(*Board)\n\t\t\tstr += \" * [\" + board.url + \"](\" + board.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/demo/boards:BOARD_NAME\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\treturn boardI.(*Board).RenderBoard()\n\t} else if len(parts) == 2 {\n\t\t// /r/demo/boards:BOARD_NAME/THREAD_ID\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\treturn thread.RenderPost(\"\", 5)\n\t} else if len(parts) == 3 {\n\t\t// /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\trid, err := strconv.Atoi(parts[2])\n\t\tif err != nil {\n\t\t\treturn \"invalid reply id: \" + parts[2]\n\t\t}\n\t\treply := thread.GetReply(PostID(rid))\n\t\tif reply == nil {\n\t\t\treturn \"reply does not exist with id: \" + parts[2]\n\t\t}\n\t\treturn reply.RenderInner()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n"},{"name":"role.gno","body":"package boards\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission Permission = \"role:edit\"\n)\n"},{"name":"z_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\nimport (\n\t\"gno.land/r/demo/boards\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// unauthorized\n"},{"name":"z_0_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 19900000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid = boards.CreateBoard(\"test_board\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// payment must not be less than 20000000\n"},{"name":"z_0_c_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tboards.CreateThread(1, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n"},{"name":"z_0_d_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateReply(bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// thread not exist\n"},{"name":"z_0_e_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tboards.CreateReply(bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 20000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/demo/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)] (0 replies) (0 reposts)\n//\n// ----------------------------------------\n// ## [Second Post (title)](/r/demo/boards:test_board/2)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)] (1 replies) (0 reposts)\n"},{"name":"z_10_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// boardId 2 not exist\n\tboards.DeletePost(2, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// board not exist\n"},{"name":"z_10_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// pid of 2 not exist\n\tboards.DeletePost(bid, 2, 2, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// thread not exist\n"},{"name":"z_10_c_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.DeletePost(bid, pid, rid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n"},{"name":"z_10_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.DeletePost(bid, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// ----------------------------------------------------\n// thread does not exist with id: 1\n"},{"name":"z_11_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// board 2 not exist\n\tboards.EditPost(2, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// board not exist\n"},{"name":"z_11_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// thread 2 not exist\n\tboards.EditPost(bid, 2, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// thread not exist\n"},{"name":"z_11_c_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// post 2 not exist\n\tboards.EditPost(bid, pid, 2, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// post not exist\n"},{"name":"z_11_d_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.EditPost(bid, pid, rid, \"\", \"Edited: First reply of the First post\\n\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e Edited: First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n"},{"name":"z_11_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\tboards.EditPost(bid, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// ----------------------------------------------------\n// # Edited: First Post in (title)\n//\n// Edited: Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n"},{"name":"z_12_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create a post via registered user\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n"},{"name":"z_12_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing board\n\trid := boards.CreateRepost(5, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// src board not exist\n"},{"name":"z_12_c_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tboards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing thread\n\trid := boards.CreateRepost(bid1, 5, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// thread not exist\n"},{"name":"z_12_d_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing destination board\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", 5)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// dst board not exist\n"},{"name":"z_12_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid1 boards.BoardID\n\tbid2 boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid1 = boards.CreateBoard(\"test_board1\")\n\tpid = boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 = boards.CreateBoard(\"test_board2\")\n}\n\nfunc main() {\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board2\"))\n}\n\n// Output:\n// 1\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=2)]\n//\n// ----------------------------------------\n// Repost: Check this out\n// ## [First Post (title)](/r/demo/boards:test_board1/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)] (0 replies) (1 reposts)\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar board *boards.Board\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\t_ = boards.CreateBoard(\"test_board_1\")\n\t_ = boards.CreateBoard(\"test_board_2\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"\"))\n}\n\n// Output:\n// These are all the boards of this realm:\n//\n// * [/r/demo/boards:test_board_1](/r/demo/boards:test_board_1)\n// * [/r/demo/boards:test_board_2](/r/demo/boards:test_board_2)\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n"},{"name":"z_3_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n}\n\nfunc main() {\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n"},{"name":"z_4_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n}\n\nfunc main() {\n\trid2 := boards.CreateReply(bid, pid, pid, \"Second reply of the second post\")\n\tprintln(rid2)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// 4\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n//\n// \u003e Second reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=4)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=4)]\n\n// Realm:\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/boards\"]\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\",\n// \"ModTime\": \"123\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"68663c8895d37d479e417c11e21badfe21345c61\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:112\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"3f34ac77289aa1d5f9a2f8b6d083138325816fb0\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"94a6665a44bac6ede7f3e3b87173e537b12f9532\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"bc8e5b4e782a0bbc4ac9689681f119beb7b34d59\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9957eadbc91dd32f33b0d815e041a32dbdea0671\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131]={\n// \"Fields\": [\n// {\n// \"N\": \"AAAAgJSeXbo=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"N\": \"AbSNdvQQIhE=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"1024\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Location\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"336074805fc853987abe6f7fe3ad97a6a6f3077a:2\"\n// },\n// \"Index\": \"182\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"1024\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Location\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Board\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"N\": \"BAAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"Second reply of the second post\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"f91e355bd19240f0f3350a7fa0e6a82b72225916\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9ee9c4117be283fc51ffcc5ecd65b75ecef5a9dd\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"eb768b0140a5fe95f9c58747f0960d647dacfd42\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.BoardID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"0fd3352422af0a56a77ef2c9e88f479054e3d51f\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"bed4afa8ffdbbf775451c947fc68b27a345ce32a\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"0\",\n// \"RefCount\": \"2\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c45bbd47a46681a63af973db0ec2180922e4a8ae\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\"\n// }\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\",\n// \"ModTime\": \"134\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"dc1f011553dc53e7a846049e08cc77fa35ea6a51\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:121\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"96b86b4585c7f1075d7794180a5581f72733a7ab\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"32274e1f28fb2b97d67a1262afd362d370de7faa\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c2cfd6aec36a462f35bf02e5bf4a127aa1bb7ac2\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"5cb875179e86d32c517322af7a323b2a5f3e6cc5\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\"\n// }\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85]={\n// \"Fields\": [\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.BoardID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"/r/demo/boards:test_board\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"test_board\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"a416a751c3a45a1e5cba11e737c51340b081e372\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:86\"\n// }\n// },\n// {\n// \"N\": \"BAAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"36299fccbc13f2a84c4629fad4cb940f0bd4b1c6\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:87\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"af6ed0268f99b7f369329094eb6dfaea7812708b\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:88\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85\",\n// \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\",\n// \"RefCount\": \"1\"\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9809329dc1ddc5d3556f7a8fa3c2cebcbf65560b\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:105\",\n// \"RefCount\": \"1\"\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ceae9a1c4ed28bb51062e6ccdccfad0caafd1c4f\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:105\",\n// \"RefCount\": \"1\"\n// }\n// }\n// switchrealm[\"gno.land/r/demo/boards\"]\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/users\"]\n// switchrealm[\"gno.land/r/demo/boards\"]\n// switchrealm[\"gno.land/r/demo/boards_test\"]\n"},{"name":"z_5_b_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n"},{"name":"z_5_c_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 101000000}}, nil)\n\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post (title)\n//\n// Body of the first post. (body)\n// \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)]\n//\n// \u003e Reply of the first post\n// \u003e \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=2)]\n"},{"name":"z_5_d_filetest.gno","body":"package main\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\n\t// create reply via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\tboards.CreateReply(bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n"},{"name":"z_5_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\trid2 := boards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=4)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=4)]\n"},{"name":"z_6_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tboards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\tboards.CreateReply(bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n// \u003e\n// \u003e \u003e First reply of the first reply\n// \u003e \u003e\n// \u003e \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=5)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=5)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=4)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=4)]\n"},{"name":"z_7_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc init() {\n\t// register\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\t// create board and post\n\tbid := boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/demo/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=1\u0026postid=1)] (0 replies) (0 reposts)\n"},{"name":"z_8_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tboards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\trid2 := boards.CreateReply(bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid)) + \"/\" + strconv.Itoa(int(rid2))))\n}\n\n// Output:\n// _[see thread](/r/demo/boards:test_board/2)_\n//\n// Reply of the second post\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=3)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=3)]\n//\n// _[see all 1 replies](/r/demo/boards:test_board/2/3)_\n//\n// \u003e First reply of the first reply\n// \u003e\n// \u003e \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026threadid=2\u0026postid=5)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026threadid=2\u0026postid=5)]\n"},{"name":"z_9_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar dstBoard boards.BoardID\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tdstBoard = boards.CreateBoard(\"dst_board\")\n\n\tboards.CreateRepost(0, 0, \"First Post in (title)\", \"Body of the first post. (body)\", dstBoard)\n}\n\nfunc main() {\n}\n\n// Error:\n// src board not exist\n"},{"name":"z_9_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tsrcBoard boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tsrcBoard = boards.CreateBoard(\"first_board\")\n\tpid = boards.CreateThread(srcBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(srcBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", 0)\n}\n\nfunc main() {\n}\n\n// Error:\n// dst board not exist\n"},{"name":"z_9_filetest.gno","body":"// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/demo/boards\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tfirstBoard boards.BoardID\n\tsecondBoard boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\n\tfirstBoard = boards.CreateBoard(\"first_board\")\n\tsecondBoard = boards.CreateBoard(\"second_board\")\n\tpid = boards.CreateThread(firstBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(firstBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", secondBoard)\n}\n\nfunc main() {\n\tprintln(boards.Render(\"second_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=2\u0026threadid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=2\u0026threadid=1\u0026postid=1)]\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"groups","path":"gno.land/p/demo/groups","files":[{"name":"groups.gno","body":"package groups\n\nimport \"gno.land/r/demo/boards\"\n\n// TODO implement something and test.\ntype Group struct {\n\tBoard *boards.Board\n}\n"},{"name":"vote_set.gno","body":"package groups\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/rat\"\n)\n\n//----------------------------------------\n// VoteSet\n\ntype VoteSet interface {\n\t// number of present votes in set.\n\tSize() int\n\t// add or update vote for voter.\n\tSetVote(voter std.Address, value string) error\n\t// count the number of votes for value.\n\tCountVotes(value string) int\n}\n\n//----------------------------------------\n// VoteList\n\ntype Vote struct {\n\tVoter std.Address\n\tValue string\n}\n\ntype VoteList []Vote\n\nfunc NewVoteList() *VoteList {\n\treturn \u0026VoteList{}\n}\n\nfunc (vlist *VoteList) Size() int {\n\treturn len(*vlist)\n}\n\nfunc (vlist *VoteList) SetVote(voter std.Address, value string) error {\n\t// TODO optimize with binary algorithm\n\tfor i, vote := range *vlist {\n\t\tif vote.Voter == voter {\n\t\t\t// update vote\n\t\t\t(*vlist)[i] = Vote{\n\t\t\t\tVoter: voter,\n\t\t\t\tValue: value,\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\t*vlist = append(*vlist, Vote{\n\t\tVoter: voter,\n\t\tValue: value,\n\t})\n\treturn nil\n}\n\nfunc (vlist *VoteList) CountVotes(target string) int {\n\t// TODO optimize with binary algorithm\n\tvar count int\n\tfor _, vote := range *vlist {\n\t\tif vote.Value == target {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\n//----------------------------------------\n// Committee\n\ntype Committee struct {\n\tQuorum rat.Rat\n\tThreshold rat.Rat\n\tAddresses std.AddressSet\n}\n\n//----------------------------------------\n// VoteSession\n// NOTE: this seems a bit too formal and\n// complicated vs what might be possible;\n// something simpler, more informal.\n\ntype SessionStatus int\n\nconst (\n\tSessionNew SessionStatus = iota\n\tSessionStarted\n\tSessionCompleted\n\tSessionCanceled\n)\n\ntype VoteSession struct {\n\tName string\n\tCreator std.Address\n\tBody string\n\tStart time.Time\n\tDeadline time.Time\n\tStatus SessionStatus\n\tCommittee *Committee\n\tVotes VoteSet\n\tChoices []string\n\tResult string\n}\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/p/demo/groups\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nvar vset groups.VoteSet\n\nfunc init() {\n\taddr1 := testutils.TestAddress(\"test1\")\n\taddr2 := testutils.TestAddress(\"test2\")\n\tvset = groups.NewVoteList()\n\tvset.SetVote(addr1, \"yes\")\n\tvset.SetVote(addr2, \"yes\")\n}\n\nfunc main() {\n\tprintln(vset.Size())\n\tprintln(\"yes:\", vset.CountVotes(\"yes\"))\n\tprintln(\"no:\", vset.CountVotes(\"no\"))\n}\n\n// Output:\n// 2\n// yes: 2\n// no: 0\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"uint256","path":"gno.land/p/demo/uint256","files":[{"name":"LICENSE","body":"BSD 3-Clause License\n\nCopyright 2020 uint256 Authors\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"},{"name":"README.md","body":"# Fixed size 256-bit math library\n\nThis is a library specialized at replacing the `big.Int` library for math based on 256-bit types.\n\noriginal repository: [uint256](\u003chttps://github.com/holiman/uint256/tree/master\u003e)\n"},{"name":"arithmetic.gno","body":"// arithmetic provides arithmetic operations for Uint objects.\n// This includes basic binary operations such as addition, subtraction, multiplication, division, and modulo operations\n// as well as overflow checks, and negation. These functions are essential for numeric\n// calculations using 256-bit unsigned integers.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Add sets z to the sum x+y\nfunc (z *Uint) Add(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// AddOverflow sets z to the sum x+y, and returns z and whether overflow occurred\nfunc (z *Uint) AddOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Sub sets z to the difference x-y\nfunc (z *Uint) Sub(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// SubOverflow sets z to the difference x-y and returns z and true if the operation underflowed\nfunc (z *Uint) SubOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Neg returns -x mod 2^256.\nfunc (z *Uint) Neg(x *Uint) *Uint {\n\treturn z.Sub(new(Uint), x)\n}\n\n// commented out for possible overflow\n// Mul sets z to the product x*y\nfunc (z *Uint) Mul(x, y *Uint) *Uint {\n\tvar (\n\t\tres Uint\n\t\tcarry uint64\n\t\tres1, res2, res3 uint64\n\t)\n\n\tcarry, res.arr[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tres3 = x.arr[3]*y.arr[0] + carry\n\n\tcarry, res.arr[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tres3 = res3 + x.arr[2]*y.arr[1] + carry\n\n\tcarry, res.arr[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tres3 = res3 + x.arr[1]*y.arr[2] + carry\n\n\tres.arr[3] = res3 + x.arr[0]*y.arr[3]\n\n\treturn z.Set(\u0026res)\n}\n\n// MulOverflow sets z to the product x*y, and returns z and whether overflow occurred\nfunc (z *Uint) MulOverflow(x, y *Uint) (*Uint, bool) {\n\tp := umul(x, y)\n\tcopy(z.arr[:], p[:4])\n\treturn z, (p[4] | p[5] | p[6] | p[7]) != 0\n}\n\n// commented out for possible overflow\n// Div sets z to the quotient x/y for returns z.\n// If y == 0, z is set to 0\nfunc (z *Uint) Div(x, y *Uint) *Uint {\n\tif y.IsZero() || y.Gt(x) {\n\t\treturn z.Clear()\n\t}\n\tif x.Eq(y) {\n\t\treturn z.SetOne()\n\t}\n\t// Shortcut some cases\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() / y.Uint64())\n\t}\n\n\t// At this point, we know\n\t// x/y ; x \u003e y \u003e 0\n\n\tvar quot Uint\n\tudivrem(quot.arr[:], x.arr[:], y)\n\treturn z.Set(\u0026quot)\n}\n\n// MulMod calculates the modulo-m multiplication of x and y and\n// returns z.\n// If m == 0, z is set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) MulMod(x, y, m *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() || m.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tp := umul(x, y)\n\n\tif m.arr[3] != 0 {\n\t\tmu := Reciprocal(m)\n\t\tr := reduce4(p, m, mu)\n\t\treturn z.Set(\u0026r)\n\t}\n\n\tvar (\n\t\tpl Uint\n\t\tph Uint\n\t)\n\n\tpl = Uint{arr: [4]uint64{p[0], p[1], p[2], p[3]}}\n\tph = Uint{arr: [4]uint64{p[4], p[5], p[6], p[7]}}\n\n\t// If the multiplication is within 256 bits use Mod().\n\tif ph.IsZero() {\n\t\treturn z.Mod(\u0026pl, m)\n\t}\n\n\tvar quot [8]uint64\n\trem := udivrem(quot[:], p[:], m)\n\treturn z.Set(\u0026rem)\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// If y == 0, z is set to 0 (OBS: differs from the big.Uint)\nfunc (z *Uint) Mod(x, y *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tswitch x.Cmp(y) {\n\tcase -1:\n\t\t// x \u003c y\n\t\tcopy(z.arr[:], x.arr[:])\n\t\treturn z\n\tcase 0:\n\t\t// x == y\n\t\treturn z.Clear() // They are equal\n\t}\n\n\t// At this point:\n\t// x != 0\n\t// y != 0\n\t// x \u003e y\n\n\t// Shortcut trivial case\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() % y.Uint64())\n\t}\n\n\tvar quot Uint\n\t*z = udivrem(quot.arr[:], x.arr[:], y)\n\treturn z\n}\n\n// DivMod sets z to the quotient x div y and m to the modulus x mod y and returns the pair (z, m) for y != 0.\n// If y == 0, both z and m are set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) DivMod(x, y, m *Uint) (*Uint, *Uint) {\n\tif y.IsZero() {\n\t\treturn z.Clear(), m.Clear()\n\t}\n\tvar quot Uint\n\t*m = udivrem(quot.arr[:], x.arr[:], y)\n\t*z = quot\n\treturn z, m\n}\n\n// Exp sets z = base**exponent mod 2**256, and returns z.\nfunc (z *Uint) Exp(base, exponent *Uint) *Uint {\n\tres := Uint{arr: [4]uint64{1, 0, 0, 0}}\n\tmultiplier := *base\n\texpBitLen := exponent.BitLen()\n\n\tcurBit := 0\n\tword := exponent.arr[0]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 64; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[1]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 128; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[2]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 192; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[3]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 256; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\treturn z.Set(\u0026res)\n}\n\nfunc (z *Uint) squared() {\n\tvar (\n\t\tres Uint\n\t\tcarry0, carry1, carry2 uint64\n\t\tres1, res2 uint64\n\t)\n\n\tcarry0, res.arr[0] = bits.Mul64(z.arr[0], z.arr[0])\n\tcarry0, res1 = umulHop(carry0, z.arr[0], z.arr[1])\n\tcarry0, res2 = umulHop(carry0, z.arr[0], z.arr[2])\n\n\tcarry1, res.arr[1] = umulHop(res1, z.arr[0], z.arr[1])\n\tcarry1, res2 = umulStep(res2, z.arr[1], z.arr[1], carry1)\n\n\tcarry2, res.arr[2] = umulHop(res2, z.arr[0], z.arr[2])\n\n\tres.arr[3] = 2*(z.arr[0]*z.arr[3]+z.arr[1]*z.arr[2]) + carry0 + carry1 + carry2\n\n\tz.Set(\u0026res)\n}\n\n// udivrem divides u by d and produces both quotient and remainder.\n// The quotient is stored in provided quot - len(u)-len(d)+1 words.\n// It loosely follows the Knuth's division algorithm (sometimes referenced as \"schoolbook\" division) using 64-bit words.\n// See Knuth, Volume 2, section 4.3.1, Algorithm D.\nfunc udivrem(quot, u []uint64, d *Uint) (rem Uint) {\n\tvar dLen int\n\tfor i := len(d.arr) - 1; i \u003e= 0; i-- {\n\t\tif d.arr[i] != 0 {\n\t\t\tdLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tshift := uint(bits.LeadingZeros64(d.arr[dLen-1]))\n\n\tvar dnStorage Uint\n\tdn := dnStorage.arr[:dLen]\n\tfor i := dLen - 1; i \u003e 0; i-- {\n\t\tdn[i] = (d.arr[i] \u003c\u003c shift) | (d.arr[i-1] \u003e\u003e (64 - shift))\n\t}\n\tdn[0] = d.arr[0] \u003c\u003c shift\n\n\tvar uLen int\n\tfor i := len(u) - 1; i \u003e= 0; i-- {\n\t\tif u[i] != 0 {\n\t\t\tuLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif uLen \u003c dLen {\n\t\tcopy(rem.arr[:], u)\n\t\treturn rem\n\t}\n\n\tvar unStorage [9]uint64\n\tun := unStorage[:uLen+1]\n\tun[uLen] = u[uLen-1] \u003e\u003e (64 - shift)\n\tfor i := uLen - 1; i \u003e 0; i-- {\n\t\tun[i] = (u[i] \u003c\u003c shift) | (u[i-1] \u003e\u003e (64 - shift))\n\t}\n\tun[0] = u[0] \u003c\u003c shift\n\n\t// TODO: Skip the highest word of numerator if not significant.\n\n\tif dLen == 1 {\n\t\tr := udivremBy1(quot, un, dn[0])\n\t\trem.SetUint64(r \u003e\u003e shift)\n\t\treturn rem\n\t}\n\n\tudivremKnuth(quot, un, dn)\n\n\tfor i := 0; i \u003c dLen-1; i++ {\n\t\trem.arr[i] = (un[i] \u003e\u003e shift) | (un[i+1] \u003c\u003c (64 - shift))\n\t}\n\trem.arr[dLen-1] = un[dLen-1] \u003e\u003e shift\n\n\treturn rem\n}\n\n// umul computes full 256 x 256 -\u003e 512 multiplication.\nfunc umul(x, y *Uint) [8]uint64 {\n\tvar (\n\t\tres [8]uint64\n\t\tcarry, carry4, carry5, carry6 uint64\n\t\tres1, res2, res3, res4, res5 uint64\n\t)\n\n\tcarry, res[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tcarry4, res3 = umulHop(carry, x.arr[3], y.arr[0])\n\n\tcarry, res[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tcarry, res3 = umulStep(res3, x.arr[2], y.arr[1], carry)\n\tcarry5, res4 = umulStep(carry4, x.arr[3], y.arr[1], carry)\n\n\tcarry, res[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tcarry, res3 = umulStep(res3, x.arr[1], y.arr[2], carry)\n\tcarry, res4 = umulStep(res4, x.arr[2], y.arr[2], carry)\n\tcarry6, res5 = umulStep(carry5, x.arr[3], y.arr[2], carry)\n\n\tcarry, res[3] = umulHop(res3, x.arr[0], y.arr[3])\n\tcarry, res[4] = umulStep(res4, x.arr[1], y.arr[3], carry)\n\tcarry, res[5] = umulStep(res5, x.arr[2], y.arr[3], carry)\n\tres[7], res[6] = umulStep(carry6, x.arr[3], y.arr[3], carry)\n\n\treturn res\n}\n\n// umulStep computes (hi * 2^64 + lo) = z + (x * y) + carry.\nfunc umulStep(z, x, y, carry uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry = bits.Add64(lo, carry, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\tlo, carry = bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// umulHop computes (hi * 2^64 + lo) = z + (x * y)\nfunc umulHop(z, x, y uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry := bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// udivremBy1 divides u by single normalized word d and produces both quotient and remainder.\n// The quotient is stored in provided quot.\nfunc udivremBy1(quot, u []uint64, d uint64) (rem uint64) {\n\treciprocal := reciprocal2by1(d)\n\trem = u[len(u)-1] // Set the top word as remainder.\n\tfor j := len(u) - 2; j \u003e= 0; j-- {\n\t\tquot[j], rem = udivrem2by1(rem, u[j], d, reciprocal)\n\t}\n\treturn rem\n}\n\n// udivremKnuth implements the division of u by normalized multiple word d from the Knuth's division algorithm.\n// The quotient is stored in provided quot - len(u)-len(d) words.\n// Updates u to contain the remainder - len(d) words.\nfunc udivremKnuth(quot, u, d []uint64) {\n\tdh := d[len(d)-1]\n\tdl := d[len(d)-2]\n\treciprocal := reciprocal2by1(dh)\n\n\tfor j := len(u) - len(d) - 1; j \u003e= 0; j-- {\n\t\tu2 := u[j+len(d)]\n\t\tu1 := u[j+len(d)-1]\n\t\tu0 := u[j+len(d)-2]\n\n\t\tvar qhat, rhat uint64\n\t\tif u2 \u003e= dh { // Division overflows.\n\t\t\tqhat = ^uint64(0)\n\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t} else {\n\t\t\tqhat, rhat = udivrem2by1(u2, u1, dh, reciprocal)\n\t\t\tph, pl := bits.Mul64(qhat, dl)\n\t\t\tif ph \u003e rhat || (ph == rhat \u0026\u0026 pl \u003e u0) {\n\t\t\t\tqhat--\n\t\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t\t}\n\t\t}\n\n\t\t// Multiply and subtract.\n\t\tborrow := subMulTo(u[j:], d, qhat)\n\t\tu[j+len(d)] = u2 - borrow\n\t\tif u2 \u003c borrow { // Too much subtracted, add back.\n\t\t\tqhat--\n\t\t\tu[j+len(d)] += addTo(u[j:], d)\n\t\t}\n\n\t\tquot[j] = qhat // Store quotient digit.\n\t}\n}\n\n// isBitSet returns true if bit n-th is set, where n = 0 is LSB.\n// The n must be \u003c= 255.\nfunc (z *Uint) isBitSet(n uint) bool {\n\treturn (z.arr[n/64] \u0026 (1 \u003c\u003c (n % 64))) != 0\n}\n\n// addTo computes x += y.\n// Requires len(x) \u003e= len(y).\nfunc addTo(x, y []uint64) uint64 {\n\tvar carry uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\tx[i], carry = bits.Add64(x[i], y[i], carry)\n\t}\n\treturn carry\n}\n\n// subMulTo computes x -= y * multiplier.\n// Requires len(x) \u003e= len(y).\nfunc subMulTo(x, y []uint64, multiplier uint64) uint64 {\n\tvar borrow uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\ts, carry1 := bits.Sub64(x[i], borrow, 0)\n\t\tph, pl := bits.Mul64(y[i], multiplier)\n\t\tt, carry2 := bits.Sub64(s, pl, 0)\n\t\tx[i] = t\n\t\tborrow = ph + carry1 + carry2\n\t}\n\treturn borrow\n}\n\n// reciprocal2by1 computes \u003c^d, ^0\u003e / d.\nfunc reciprocal2by1(d uint64) uint64 {\n\treciprocal, _ := bits.Div64(^d, ^uint64(0), d)\n\treturn reciprocal\n}\n\n// udivrem2by1 divides \u003cuh, ul\u003e / d and produces both quotient and remainder.\n// It uses the provided d's reciprocal.\n// Implementation ported from https://github.com/chfast/intx and is based on\n// \"Improved division by invariant integers\", Algorithm 4.\nfunc udivrem2by1(uh, ul, d, reciprocal uint64) (quot, rem uint64) {\n\tqh, ql := bits.Mul64(reciprocal, uh)\n\tql, carry := bits.Add64(ql, ul, 0)\n\tqh, _ = bits.Add64(qh, uh, carry)\n\tqh++\n\n\tr := ul - qh*d\n\n\tif r \u003e ql {\n\t\tqh--\n\t\tr += d\n\t}\n\n\tif r \u003e= d {\n\t\tqh++\n\t\tr -= d\n\t}\n\n\treturn qh, r\n}\n"},{"name":"arithmetic_test.gno","body":"package uint256\n\nimport (\n\t\"testing\"\n)\n\ntype binOp2Test struct {\n\tx, y, want string\n}\n\nfunc TestAdd(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"3\", \"4\"},\n\t\t{\"10\", \"10\", \"20\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\", \"36893488147419103230\"}, // uint64 overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Add(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Add(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant string\n\t\toverflow bool\n\t}{\n\t\t{\"0\", \"1\", \"1\", false},\n\t\t{\"1\", \"0\", \"1\", false},\n\t\t{\"1\", \"1\", \"2\", false},\n\t\t{\"10\", \"10\", \"20\", false},\n\t\t{\"18446744073709551615\", \"18446744073709551615\", \"36893488147419103230\", false}, // uint64 overflow, but not Uint256 overflow\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"0\", true}, // 2^256 - 1 + 1, should overflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819967\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", false}, // (2^255 - 1) + 2^255, no overflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819967\", \"57896044618658097711785492504343953926634992332820282019728792003956564819969\", \"0\", true}, // (2^255 - 1) + (2^255 + 1), should overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant, _ := FromDecimal(tt.want)\n\n\t\tgot, overflow := new(Uint).AddOverflow(x, y)\n\n\t\tif got.Cmp(want) != 0 || overflow != tt.overflow {\n\t\t\tt.Errorf(\"AddOverflow(%s, %s) = (%s, %v), want (%s, %v)\",\n\t\t\t\ttt.x, tt.y, got.ToString(), overflow, tt.want, tt.overflow)\n\t\t}\n\t}\n}\n\nfunc TestSub(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"10\", \"10\", \"0\"},\n\t\t{\"31337\", \"1337\", \"30000\"},\n\t\t{\"2\", \"3\", twoPow256Sub1}, // underflow\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\n\t\twant := MustFromDecimal(tc.want)\n\n\t\tgot := new(Uint).Sub(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Sub(%s, %s) = %v, want %v\",\n\t\t\t\ttc.x, tc.y, got.ToString(), want.ToString(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestSubOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant string\n\t\toverflow bool\n\t}{\n\t\t{\"1\", \"0\", \"1\", false},\n\t\t{\"1\", \"1\", \"0\", false},\n\t\t{\"10\", \"10\", \"0\", false},\n\t\t{\"31337\", \"1337\", \"30000\", false},\n\t\t{\"0\", \"1\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", true}, // 0 - 1, should underflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"1\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\", false}, // 2^255 - 1, no underflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"57896044618658097711785492504343953926634992332820282019728792003956564819969\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", true}, // 2^255 - (2^255 + 1), should underflow\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\t\twant := MustFromDecimal(tc.want)\n\n\t\tgot, overflow := new(Uint).SubOverflow(x, y)\n\n\t\tif got.Cmp(want) != 0 || overflow != tc.overflow {\n\t\t\tt.Errorf(\n\t\t\t\t\"SubOverflow(%s, %s) = (%s, %v), want (%s, %v)\",\n\t\t\t\ttc.x, tc.y, got.ToString(), overflow, tc.want, tc.overflow,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestMul(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"10\", \"10\", \"100\"},\n\t\t{\"18446744073709551615\", \"2\", \"36893488147419103230\"}, // uint64 overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Mul(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mul(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMulOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twantZ string\n\t\twantOver bool\n\t}{\n\t\t{\"0x1\", \"0x1\", \"0x1\", false},\n\t\t{\"0x0\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x0\", false},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x2\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", true},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x1\", true},\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000000\", \"0x2\", \"0x0\", true},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x2\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", false},\n\t\t{\"0x100000000000000000\", \"0x100000000000000000\", \"0x10000000000000000000000000000000000\", false},\n\t\t{\"0x10000000000000000000000000000000\", \"0x10000000000000000000000000000000\", \"0x100000000000000000000000000000000000000000000000000000000000000\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\ty := MustFromHex(tt.y)\n\t\twantZ := MustFromHex(tt.wantZ)\n\n\t\tgotZ, gotOver := new(Uint).MulOverflow(x, y)\n\n\t\tif gotZ.Neq(wantZ) {\n\t\t\tt.Errorf(\n\t\t\t\t\"MulOverflow(%s, %s) = %s, want %s\",\n\t\t\t\ttt.x, tt.y, gotZ.ToString(), wantZ.ToString(),\n\t\t\t)\n\t\t}\n\t\tif gotOver != tt.wantOver {\n\t\t\tt.Errorf(\"MulOverflow(%s, %s) = %v, want %v\", tt.x, tt.y, gotOver, tt.wantOver)\n\t\t}\n\t}\n}\n\nfunc TestDiv(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"10445\"},\n\t\t{\"31337\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1000000000000000000\", \"3\", \"333333333333333333\"},\n\t\t{twoPow256Sub1, \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Div(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Div(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMod(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"2\"},\n\t\t{\"31337\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"2\", \"31337\", \"2\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"1\"}, // 2^256 - 1 mod 2\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"3\", \"0\"}, // 2^256 - 1 mod 3\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"}, // 2^256 - 1 mod 2^255\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Mod(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMulMod(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\tm string\n\t\twant string\n\t}{\n\t\t{\"0x1\", \"0x1\", \"0x2\", \"0x1\"},\n\t\t{\"0x10\", \"0x10\", \"0x7\", \"0x4\"},\n\t\t{\"0x100\", \"0x100\", \"0x17\", \"0x9\"},\n\t\t{\"0x31337\", \"0x31337\", \"0x31338\", \"0x1\"},\n\t\t{\"0x0\", \"0x31337\", \"0x31338\", \"0x0\"},\n\t\t{\"0x31337\", \"0x0\", \"0x31338\", \"0x0\"},\n\t\t{\"0x2\", \"0x3\", \"0x5\", \"0x1\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x0\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", \"0x1\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffff\", \"0x0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\ty := MustFromHex(tt.y)\n\t\tm := MustFromHex(tt.m)\n\t\twant := MustFromHex(tt.want)\n\n\t\tgot := new(Uint).MulMod(x, y, m)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"MulMod(%s, %s, %s) = %s, want %s\",\n\t\t\t\ttt.x, tt.y, tt.m, got.ToString(), want.ToString(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestDivMod(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twantDiv string\n\t\twantMod string\n\t}{\n\t\t{\"1\", \"1\", \"1\", \"0\"},\n\t\t{\"10\", \"10\", \"1\", \"0\"},\n\t\t{\"100\", \"10\", \"10\", \"0\"},\n\t\t{\"31337\", \"3\", \"10445\", \"2\"},\n\t\t{\"31337\", \"0\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\", \"0\"},\n\t\t{\"2\", \"31337\", \"0\", \"2\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twantDiv := MustFromDecimal(tt.wantDiv)\n\t\twantMod := MustFromDecimal(tt.wantMod)\n\n\t\tgotDiv := new(Uint)\n\t\tgotMod := new(Uint)\n\t\tgotDiv.DivMod(x, y, gotMod)\n\n\t\tfor i := range gotDiv.arr {\n\t\t\tif gotDiv.arr[i] != wantDiv.arr[i] {\n\t\t\t\tt.Errorf(\"DivMod(%s, %s) got Div %v, want Div %v\", tt.x, tt.y, gotDiv, wantDiv)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor i := range gotMod.arr {\n\t\t\tif gotMod.arr[i] != wantMod.arr[i] {\n\t\t\t\tt.Errorf(\"DivMod(%s, %s) got Mod %v, want Mod %v\", tt.x, tt.y, gotMod, wantMod)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"31337\", \"115792089237316195423570985008687907853269984665640564039457584007913129608599\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129608599\", \"31337\"},\n\t\t{\"0\", \"0\"},\n\t\t{\"2\", \"115792089237316195423570985008687907853269984665640564039457584007913129639934\"},\n\t\t{\"1\", twoPow256Sub1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Neg(x)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Neg(%s) = %v, want %v\", tt.x, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestExp(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"30773171189753\"},\n\t\t{\"31337\", \"0\", \"1\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"2\", \"3\", \"8\"},\n\t\t{\"2\", \"64\", \"18446744073709551616\"},\n\t\t{\"2\", \"128\", \"340282366920938463463374607431768211456\"},\n\t\t{\"2\", \"255\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"2\", \"256\", \"0\"}, // overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Exp(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Exp(%s, %s) = %v, want %v\",\n\t\t\t\ttt.x, tt.y, got.ToString(), want.ToString(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestExp_LargeExponent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbase string\n\t\texponent string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"2^129\",\n\t\t\tbase: \"2\",\n\t\t\texponent: \"680564733841876926926749214863536422912\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"2^193\",\n\t\t\tbase: \"2\",\n\t\t\texponent: \"12379400392853802746563808384000000000000000000\",\n\t\t\texpected: \"0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbase := MustFromDecimal(tt.base)\n\t\t\texponent := MustFromDecimal(tt.exponent)\n\t\t\texpected := MustFromDecimal(tt.expected)\n\n\t\t\tresult := new(Uint).Exp(base, exponent)\n\n\t\t\tif result.Neq(expected) {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Test %s failed. Expected %s, got %s\",\n\t\t\t\t\ttt.name, expected.ToString(), result.ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"bits_table.gno","body":"// Copyright 2017 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Code generated by go run make_tables.go. DO NOT EDIT.\n\npackage uint256\n\nconst ntz8tab = \"\" +\n\t\"\\x08\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x07\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\"\n\nconst pop8tab = \"\" +\n\t\"\\x00\\x01\\x01\\x02\\x01\\x02\\x02\\x03\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\\x05\\x06\\x06\\x07\\x06\\x07\\x07\\x08\"\n\nconst rev8tab = \"\" +\n\t\"\\x00\\x80\\x40\\xc0\\x20\\xa0\\x60\\xe0\\x10\\x90\\x50\\xd0\\x30\\xb0\\x70\\xf0\" +\n\t\"\\x08\\x88\\x48\\xc8\\x28\\xa8\\x68\\xe8\\x18\\x98\\x58\\xd8\\x38\\xb8\\x78\\xf8\" +\n\t\"\\x04\\x84\\x44\\xc4\\x24\\xa4\\x64\\xe4\\x14\\x94\\x54\\xd4\\x34\\xb4\\x74\\xf4\" +\n\t\"\\x0c\\x8c\\x4c\\xcc\\x2c\\xac\\x6c\\xec\\x1c\\x9c\\x5c\\xdc\\x3c\\xbc\\x7c\\xfc\" +\n\t\"\\x02\\x82\\x42\\xc2\\x22\\xa2\\x62\\xe2\\x12\\x92\\x52\\xd2\\x32\\xb2\\x72\\xf2\" +\n\t\"\\x0a\\x8a\\x4a\\xca\\x2a\\xaa\\x6a\\xea\\x1a\\x9a\\x5a\\xda\\x3a\\xba\\x7a\\xfa\" +\n\t\"\\x06\\x86\\x46\\xc6\\x26\\xa6\\x66\\xe6\\x16\\x96\\x56\\xd6\\x36\\xb6\\x76\\xf6\" +\n\t\"\\x0e\\x8e\\x4e\\xce\\x2e\\xae\\x6e\\xee\\x1e\\x9e\\x5e\\xde\\x3e\\xbe\\x7e\\xfe\" +\n\t\"\\x01\\x81\\x41\\xc1\\x21\\xa1\\x61\\xe1\\x11\\x91\\x51\\xd1\\x31\\xb1\\x71\\xf1\" +\n\t\"\\x09\\x89\\x49\\xc9\\x29\\xa9\\x69\\xe9\\x19\\x99\\x59\\xd9\\x39\\xb9\\x79\\xf9\" +\n\t\"\\x05\\x85\\x45\\xc5\\x25\\xa5\\x65\\xe5\\x15\\x95\\x55\\xd5\\x35\\xb5\\x75\\xf5\" +\n\t\"\\x0d\\x8d\\x4d\\xcd\\x2d\\xad\\x6d\\xed\\x1d\\x9d\\x5d\\xdd\\x3d\\xbd\\x7d\\xfd\" +\n\t\"\\x03\\x83\\x43\\xc3\\x23\\xa3\\x63\\xe3\\x13\\x93\\x53\\xd3\\x33\\xb3\\x73\\xf3\" +\n\t\"\\x0b\\x8b\\x4b\\xcb\\x2b\\xab\\x6b\\xeb\\x1b\\x9b\\x5b\\xdb\\x3b\\xbb\\x7b\\xfb\" +\n\t\"\\x07\\x87\\x47\\xc7\\x27\\xa7\\x67\\xe7\\x17\\x97\\x57\\xd7\\x37\\xb7\\x77\\xf7\" +\n\t\"\\x0f\\x8f\\x4f\\xcf\\x2f\\xaf\\x6f\\xef\\x1f\\x9f\\x5f\\xdf\\x3f\\xbf\\x7f\\xff\"\n\nconst len8tab = \"\" +\n\t\"\\x00\\x01\\x02\\x02\\x03\\x03\\x03\\x03\\x04\\x04\\x04\\x04\\x04\\x04\\x04\\x04\" +\n\t\"\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\"\n"},{"name":"bitwise.gno","body":"// bitwise contains bitwise operations for Uint instances.\n// This file includes functions to perform bitwise AND, OR, XOR, and NOT operations, as well as bit shifting.\n// These operations are crucial for manipulating individual bits within a 256-bit unsigned integer.\npackage uint256\n\n// Or sets z = x | y and returns z.\nfunc (z *Uint) Or(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] | y.arr[0]\n\tz.arr[1] = x.arr[1] | y.arr[1]\n\tz.arr[2] = x.arr[2] | y.arr[2]\n\tz.arr[3] = x.arr[3] | y.arr[3]\n\treturn z\n}\n\n// And sets z = x \u0026 y and returns z.\nfunc (z *Uint) And(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026 y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026 y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026 y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026 y.arr[3]\n\treturn z\n}\n\n// Not sets z = ^x and returns z.\nfunc (z *Uint) Not(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = ^x.arr[3], ^x.arr[2], ^x.arr[1], ^x.arr[0]\n\treturn z\n}\n\n// AndNot sets z = x \u0026^ y and returns z.\nfunc (z *Uint) AndNot(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026^ y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026^ y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026^ y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026^ y.arr[3]\n\treturn z\n}\n\n// Xor sets z = x ^ y and returns z.\nfunc (z *Uint) Xor(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] ^ y.arr[0]\n\tz.arr[1] = x.arr[1] ^ y.arr[1]\n\tz.arr[2] = x.arr[2] ^ y.arr[2]\n\tz.arr[3] = x.arr[3] ^ y.arr[3]\n\treturn z\n}\n\n// Lsh sets z = x \u003c\u003c n and returns z.\nfunc (z *Uint) Lsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.lsh64(x)\n\t\tcase 128:\n\t\t\treturn z.lsh128(x)\n\t\tcase 192:\n\t\t\treturn z.lsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.lsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.lsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.lsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[0] \u003e\u003e (64 - n)\n\tz.arr[0] = z.arr[0] \u003c\u003c n\n\nsh64:\n\tb = z.arr[1] \u003e\u003e (64 - n)\n\tz.arr[1] = (z.arr[1] \u003c\u003c n) | a\n\nsh128:\n\ta = z.arr[2] \u003e\u003e (64 - n)\n\tz.arr[2] = (z.arr[2] \u003c\u003c n) | b\n\nsh192:\n\tz.arr[3] = (z.arr[3] \u003c\u003c n) | a\n\n\treturn z\n}\n\n// Rsh sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) Rsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.rsh64(x)\n\t\tcase 128:\n\t\t\treturn z.rsh128(x)\n\t\tcase 192:\n\t\t\treturn z.rsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.rsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.rsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.rsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[3] \u003c\u003c (64 - n)\n\tz.arr[3] = z.arr[3] \u003e\u003e n\n\nsh64:\n\tb = z.arr[2] \u003c\u003c (64 - n)\n\tz.arr[2] = (z.arr[2] \u003e\u003e n) | a\n\nsh128:\n\ta = z.arr[1] \u003c\u003c (64 - n)\n\tz.arr[1] = (z.arr[1] \u003e\u003e n) | b\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\n// SRsh (Signed/Arithmetic right shift)\n// considers z to be a signed integer, during right-shift\n// and sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) SRsh(x *Uint, n uint) *Uint {\n\t// If the MSB is 0, SRsh is same as Rsh.\n\tif !x.isBitSet(255) {\n\t\treturn z.Rsh(x, n)\n\t}\n\tif n%64 == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.srsh64(x)\n\t\tcase 128:\n\t\t\treturn z.srsh128(x)\n\t\tcase 192:\n\t\t\treturn z.srsh192(x)\n\t\tdefault:\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t}\n\tvar a uint64 = MaxUint64 \u003c\u003c (64 - n%64)\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t\tz.srsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.srsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.srsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\tz.arr[3], a = (z.arr[3]\u003e\u003en)|a, z.arr[3]\u003c\u003c(64-n)\n\nsh64:\n\tz.arr[2], a = (z.arr[2]\u003e\u003en)|a, z.arr[2]\u003c\u003c(64-n)\n\nsh128:\n\tz.arr[1], a = (z.arr[1]\u003e\u003en)|a, z.arr[1]\u003c\u003c(64-n)\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\nfunc (z *Uint) lsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[2], x.arr[1], x.arr[0], 0\n\treturn z\n}\n\nfunc (z *Uint) lsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[1], x.arr[0], 0, 0\n\treturn z\n}\n\nfunc (z *Uint) lsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[0], 0, 0, 0\n\treturn z\n}\n\nfunc (z *Uint) rsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) rsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) rsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x.arr[3]\n\treturn z\n}\n\nfunc (z *Uint) srsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) srsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) srsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, x.arr[3]\n\treturn z\n}\n"},{"name":"bitwise_test.gno","body":"package uint256\n\nimport \"testing\"\n\ntype logicOpTest struct {\n\tname string\n\tx Uint\n\ty Uint\n\twant Uint\n}\n\nfunc TestOr(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).Or(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Or(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAnd(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).And(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"And(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNot(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx Uint\n\t\twant Uint\n\t}{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).Not(\u0026tt.x)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Not(%s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAndNot(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).AndNot(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"AndNot(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestXor(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).Xor(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Xor(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t{\"0\", 0, \"0\"},\n\t\t{\"0\", 1, \"0\"},\n\t\t{\"0\", 64, \"0\"},\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"2\"},\n\t\t{\"1\", 64, \"18446744073709551616\"},\n\t\t{\"1\", 128, \"340282366920938463463374607431768211456\"},\n\t\t{\"1\", 192, \"6277101735386680763835789423207666416102355444464034512896\"},\n\t\t{\"1\", 255, \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"1\", 256, \"0\"},\n\t\t{\"31337\", 0, \"31337\"},\n\t\t{\"31337\", 1, \"62674\"},\n\t\t{\"31337\", 64, \"578065619037836218990592\"},\n\t\t{\"31337\", 128, \"10663428532201448629551770073089320442396672\"},\n\t\t{\"31337\", 192, \"196705537081812415096322133155058642481399512563169449530621952\"},\n\t\t{\"31337\", 193, \"393411074163624830192644266310117284962799025126338899061243904\"},\n\t\t{\"31337\", 255, \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"31337\", 256, \"0\"},\n\t\t// 64 \u003c n \u003c 128\n\t\t{\"1\", 65, \"36893488147419103232\"},\n\t\t{\"31337\", 100, \"39724366859352024754702188346867712\"},\n\n\t\t// 128 \u003c n \u003c 192\n\t\t{\"1\", 129, \"680564733841876926926749214863536422912\"},\n\t\t{\"31337\", 150, \"44725660946326664792723507424638829088826130956288\"},\n\n\t\t// 192 \u003c n \u003c 256\n\t\t{\"1\", 193, \"12554203470773361527671578846415332832204710888928069025792\"},\n\t\t{\"31337\", 200, \"50356617492943978264658466087695012475238275216171379079839219712\"},\n\n\t\t// n \u003e 256\n\t\t{\"1\", 257, \"0\"},\n\t\t{\"31337\", 300, \"0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Lsh(x, tt.y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Lsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t{\"0\", 0, \"0\"},\n\t\t{\"0\", 1, \"0\"},\n\t\t{\"0\", 64, \"0\"},\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"0\"},\n\t\t{\"1\", 64, \"0\"},\n\t\t{\"1\", 128, \"0\"},\n\t\t{\"1\", 192, \"0\"},\n\t\t{\"1\", 255, \"0\"},\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", 255, \"1\"},\n\t\t{\"6277101735386680763835789423207666416102355444464034512896\", 192, \"1\"},\n\t\t{\"340282366920938463463374607431768211456\", 128, \"1\"},\n\t\t{\"18446744073709551616\", 64, \"1\"},\n\t\t{\"393411074163624830192644266310117284962799025126338899061243904\", 193, \"31337\"},\n\t\t{\"196705537081812415096322133155058642481399512563169449530621952\", 192, \"31337\"},\n\t\t{\"10663428532201448629551770073089320442396672\", 128, \"31337\"},\n\t\t{\"578065619037836218990592\", 64, \"31337\"},\n\t\t{twoPow256Sub1, 256, \"0\"},\n\t\t// outliers\n\t\t{\"340282366920938463463374607431768211455\", 129, \"0\"},\n\t\t{\"18446744073709551615\", 65, \"0\"},\n\t\t{twoPow256Sub1, 1, \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\n\t\t// n \u003e 256\n\t\t{\"1\", 257, \"0\"},\n\t\t{\"31337\", 300, \"0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Rsh(x, tt.y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestSRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t// Positive numbers (behaves like Rsh)\n\t\t{\"0x0\", 0, \"0x0\"},\n\t\t{\"0x0\", 1, \"0x0\"},\n\t\t{\"0x1\", 0, \"0x1\"},\n\t\t{\"0x1\", 1, \"0x0\"},\n\t\t{\"0x31337\", 0, \"0x31337\"},\n\t\t{\"0x31337\", 4, \"0x3133\"},\n\t\t{\"0x31337\", 8, \"0x313\"},\n\t\t{\"0x31337\", 16, \"0x3\"},\n\t\t{\"0x10000000000000000\", 64, \"0x1\"}, // 2^64 \u003e\u003e 64\n\n\t\t// // Numbers with MSB set (negative numbers in two's complement)\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 0, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 1, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 4, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 64, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 128, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 192, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 255, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\n\t\t// Large positive number close to max value\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 1, \"0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 2, \"0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 64, \"0x7fffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 128, \"0x7fffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 192, \"0x7fffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 255, \"0x0\"},\n\n\t\t// Specific cases\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000000\", 1, \"0xc000000000000000000000000000000000000000000000000000000000000000\"},\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000001\", 1, \"0xc000000000000000000000000000000000000000000000000000000000000000\"},\n\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 65, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 127, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 129, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 193, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\n\t\t// n \u003e 256\n\t\t{\"0x1\", 257, \"0x0\"},\n\t\t{\"0x31337\", 300, \"0x0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\twant := MustFromHex(tt.want)\n\n\t\tgot := new(Uint).SRsh(x, tt.y)\n\n\t\tif !got.Eq(want) {\n\t\t\tt.Errorf(\"SRsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n"},{"name":"cmp.gno","body":"// cmp (or, comparisons) includes methods for comparing Uint instances.\n// These comparison functions cover a range of operations including equality checks, less than/greater than\n// evaluations, and specialized comparisons such as signed greater than. These are fundamental for logical\n// decision making based on Uint values.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Cmp compares z and x and returns:\n//\n//\t-1 if z \u003c x\n//\t 0 if z == x\n//\t+1 if z \u003e x\nfunc (z *Uint) Cmp(x *Uint) (r int) {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\td0, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\td1, carry := bits.Sub64(z.arr[1], x.arr[1], carry)\n\td2, carry := bits.Sub64(z.arr[2], x.arr[2], carry)\n\td3, carry := bits.Sub64(z.arr[3], x.arr[3], carry)\n\tif carry == 1 {\n\t\treturn -1\n\t}\n\tif d0|d1|d2|d3 == 0 {\n\t\treturn 0\n\t}\n\treturn 1\n}\n\n// IsZero returns true if z == 0\nfunc (z *Uint) IsZero() bool {\n\treturn (z.arr[0] | z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Sign returns:\n//\n//\t-1 if z \u003c 0\n//\t 0 if z == 0\n//\t+1 if z \u003e 0\n//\n// Where z is interpreted as a two's complement signed number\nfunc (z *Uint) Sign() int {\n\tif z.IsZero() {\n\t\treturn 0\n\t}\n\tif z.arr[3] \u003c 0x8000000000000000 {\n\t\treturn 1\n\t}\n\treturn -1\n}\n\n// LtUint64 returns true if z is smaller than n\nfunc (z *Uint) LtUint64(n uint64) bool {\n\treturn z.arr[0] \u003c n \u0026\u0026 (z.arr[1]|z.arr[2]|z.arr[3]) == 0\n}\n\n// GtUint64 returns true if z is larger than n\nfunc (z *Uint) GtUint64(n uint64) bool {\n\treturn z.arr[0] \u003e n || (z.arr[1]|z.arr[2]|z.arr[3]) != 0\n}\n\n// Lt returns true if z \u003c x\nfunc (z *Uint) Lt(x *Uint) bool {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\t_, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\t_, carry = bits.Sub64(z.arr[1], x.arr[1], carry)\n\t_, carry = bits.Sub64(z.arr[2], x.arr[2], carry)\n\t_, carry = bits.Sub64(z.arr[3], x.arr[3], carry)\n\n\treturn carry != 0\n}\n\n// Gt returns true if z \u003e x\nfunc (z *Uint) Gt(x *Uint) bool {\n\treturn x.Lt(z)\n}\n\n// Lte returns true if z \u003c= x\nfunc (z *Uint) Lte(x *Uint) bool {\n\tcond1 := z.Lt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Gte returns true if z \u003e= x\nfunc (z *Uint) Gte(x *Uint) bool {\n\tcond1 := z.Gt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Eq returns true if z == x\nfunc (z *Uint) Eq(x *Uint) bool {\n\treturn (z.arr[0] == x.arr[0]) \u0026\u0026 (z.arr[1] == x.arr[1]) \u0026\u0026 (z.arr[2] == x.arr[2]) \u0026\u0026 (z.arr[3] == x.arr[3])\n}\n\n// Neq returns true if z != x\nfunc (z *Uint) Neq(x *Uint) bool {\n\treturn !z.Eq(x)\n}\n\n// Sgt interprets z and x as signed integers, and returns\n// true if z \u003e x\nfunc (z *Uint) Sgt(x *Uint) bool {\n\tzSign := z.Sign()\n\txSign := x.Sign()\n\n\tswitch {\n\tcase zSign \u003e= 0 \u0026\u0026 xSign \u003c 0:\n\t\treturn true\n\tcase zSign \u003c 0 \u0026\u0026 xSign \u003e= 0:\n\t\treturn false\n\tdefault:\n\t\treturn z.Gt(x)\n\t}\n}\n"},{"name":"cmp_test.gno","body":"package uint256\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestSign(t *testing.T) {\n\ttests := []struct {\n\t\tinput *Uint\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tinput: NewUint(0),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(1),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(0x7fffffffffffffff),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(0x8000000000000000),\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input.ToString(), func(t *testing.T) {\n\t\t\tresult := tt.input.Sign()\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Sign() = %d; want %d\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCmp(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant int\n\t}{\n\t\t{\"0\", \"0\", 0},\n\t\t{\"0\", \"1\", -1},\n\t\t{\"1\", \"0\", 1},\n\t\t{\"1\", \"1\", 0},\n\t\t{\"10\", \"10\", 0},\n\t\t{\"10\", \"11\", -1},\n\t\t{\"11\", \"10\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\n\t\tgot := x.Cmp(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Cmp(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", true},\n\t\t{\"1\", false},\n\t\t{\"10\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\n\t\tgot := x.IsZero()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"IsZero(%s) = %v, want %v\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestLtUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint64\n\t\twant bool\n\t}{\n\t\t{\"0\", 1, true},\n\t\t{\"1\", 0, false},\n\t\t{\"10\", 10, false},\n\t\t{\"0xffffffffffffffff\", 0, false},\n\t\t{\"0x10000000000000000\", 10000000000000000, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := parseTestString(t, tc.x)\n\n\t\tgot := x.LtUint64(tc.y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"LtUint64(%s, %d) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_GtUint64(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tz string\n\t\tn uint64\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"z \u003e n\",\n\t\t\tz: \"1\",\n\t\t\tn: 0,\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"z \u003c n\",\n\t\t\tz: \"18446744073709551615\",\n\t\t\tn: 0xFFFFFFFFFFFFFFFF,\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"z == n\",\n\t\t\tz: \"18446744073709551615\",\n\t\t\tn: 0xFFFFFFFFFFFFFFFF,\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := MustFromDecimal(tt.z)\n\n\t\t\tif got := z.GtUint64(tt.n); got != tt.want {\n\t\t\t\tt.Errorf(\"Uint.GtUint64() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSGT(t *testing.T) {\n\tx := MustFromHex(\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\")\n\ty := MustFromHex(\"0x0\")\n\tactual := x.Sgt(y)\n\tif actual {\n\t\tt.Fatalf(\"Expected %v false\", actual)\n\t}\n\n\tx = MustFromHex(\"0x0\")\n\ty = MustFromHex(\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\")\n\tactual = x.Sgt(y)\n\tif !actual {\n\t\tt.Fatalf(\"Expected %v true\", actual)\n\t}\n}\n\nfunc TestEq(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twant bool\n\t}{\n\t\t{\"0xffffffffffffffff\", \"18446744073709551615\", true},\n\t\t{\"0x10000000000000000\", \"18446744073709551616\", true},\n\t\t{\"0\", \"0\", true},\n\t\t{twoPow256Sub1, twoPow256Sub1, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := parseTestString(t, tt.x)\n\n\t\ty, err := FromDecimal(tt.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Eq(y)\n\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Eq(%s, %s) = %v, want %v\", tt.x, tt.y, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_Lte(t *testing.T) {\n\ttests := []struct {\n\t\tz, x string\n\t\twant bool\n\t}{\n\t\t{\"10\", \"20\", true},\n\t\t{\"20\", \"10\", false},\n\t\t{\"10\", \"10\", true},\n\t\t{\"0\", \"0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, err := FromDecimal(tt.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tx, err := FromDecimal(tt.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tif got := z.Lte(x); got != tt.want {\n\t\t\tt.Errorf(\"Uint.Lte(%v, %v) = %v, want %v\", tt.z, tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_Gte(t *testing.T) {\n\ttests := []struct {\n\t\tz, x string\n\t\twant bool\n\t}{\n\t\t{\"20\", \"10\", true},\n\t\t{\"10\", \"20\", false},\n\t\t{\"10\", \"10\", true},\n\t\t{\"0\", \"0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := parseTestString(t, tt.z)\n\t\tx := parseTestString(t, tt.x)\n\n\t\tif got := z.Gte(x); got != tt.want {\n\t\t\tt.Errorf(\"Uint.Gte(%v, %v) = %v, want %v\", tt.z, tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc parseTestString(_ *testing.T, s string) *Uint {\n\tvar x *Uint\n\n\tif strings.HasPrefix(s, \"0x\") {\n\t\tx = MustFromHex(s)\n\t} else {\n\t\tx = MustFromDecimal(s)\n\t}\n\n\treturn x\n}\n"},{"name":"conversion.gno","body":"// conversions contains methods for converting Uint instances to other types and vice versa.\n// This includes conversions to and from basic types such as uint64 and int32, as well as string representations\n// and byte slices. Additionally, it covers marshaling and unmarshaling for JSON and other text formats.\npackage uint256\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Uint) Uint64() uint64 {\n\treturn z.arr[0]\n}\n\n// Uint64WithOverflow returns the lower 64-bits of z and bool whether overflow occurred\nfunc (z *Uint) Uint64WithOverflow() (uint64, bool) {\n\treturn z.arr[0], (z.arr[1] | z.arr[2] | z.arr[3]) != 0\n}\n\n// SetUint64 sets z to the value x\nfunc (z *Uint) SetUint64(x uint64) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x\n\treturn z\n}\n\n// IsUint64 reports whether z can be represented as a uint64.\nfunc (z *Uint) IsUint64() bool {\n\treturn (z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Dec returns the decimal representation of z.\nfunc (z *Uint) Dec() string {\n\tif z.IsZero() {\n\t\treturn \"0\"\n\t}\n\tif z.IsUint64() {\n\t\treturn strconv.FormatUint(z.Uint64(), 10)\n\t}\n\n\t// The max uint64 value being 18446744073709551615, the largest\n\t// power-of-ten below that is 10000000000000000000.\n\t// When we do a DivMod using that number, the remainder that we\n\t// get back is the lower part of the output.\n\t//\n\t// The ascii-output of remainder will never exceed 19 bytes (since it will be\n\t// below 10000000000000000000).\n\t//\n\t// Algorithm example using 100 as divisor\n\t//\n\t// 12345 % 100 = 45 (rem)\n\t// 12345 / 100 = 123 (quo)\n\t// -\u003e output '45', continue iterate on 123\n\tvar (\n\t\t// out is 98 bytes long: 78 (max size of a string without leading zeroes,\n\t\t// plus slack so we can copy 19 bytes every iteration).\n\t\t// We init it with zeroes, because when strconv appends the ascii representations,\n\t\t// it will omit leading zeroes.\n\t\tout = []byte(\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\")\n\t\tdivisor = NewUint(10000000000000000000) // 20 digits\n\t\ty = new(Uint).Set(z) // copy to avoid modifying z\n\t\tpos = len(out) // position to write to\n\t\tbuf = make([]byte, 0, 19) // buffer to write uint64:s to\n\t)\n\tfor {\n\t\t// Obtain Q and R for divisor\n\t\tvar quot Uint\n\t\trem := udivrem(quot.arr[:], y.arr[:], divisor)\n\t\ty.Set(\u0026quot) // Set Q for next loop\n\t\t// Convert the R to ascii representation\n\t\tbuf = strconv.AppendUint(buf[:0], rem.Uint64(), 10)\n\t\t// Copy in the ascii digits\n\t\tcopy(out[pos-len(buf):], buf)\n\t\tif y.IsZero() {\n\t\t\tbreak\n\t\t}\n\t\t// Move 19 digits left\n\t\tpos -= 19\n\t}\n\t// skip leading zeroes by only using the 'used size' of buf\n\treturn string(out[pos-len(buf):])\n}\n\nfunc (z *Uint) Scan(src interface{}) error {\n\tif src == nil {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tswitch src := src.(type) {\n\tcase string:\n\t\treturn z.scanScientificFromString(src)\n\tcase []byte:\n\t\treturn z.scanScientificFromString(string(src))\n\t}\n\treturn errors.New(\"default // unsupported type: can't convert to uint256.Uint\")\n}\n\nfunc (z *Uint) scanScientificFromString(src string) error {\n\tif len(src) == 0 {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tidx := strings.IndexByte(src, 'e')\n\tif idx == -1 {\n\t\treturn z.SetFromDecimal(src)\n\t}\n\tif err := z.SetFromDecimal(src[:idx]); err != nil {\n\t\treturn err\n\t}\n\tif src[(idx+1):] == \"0\" {\n\t\treturn nil\n\t}\n\texp := new(Uint)\n\tif err := exp.SetFromDecimal(src[(idx + 1):]); err != nil {\n\t\treturn err\n\t}\n\tif exp.GtUint64(77) { // 10**78 is larger than 2**256\n\t\treturn ErrBig256Range\n\t}\n\texp.Exp(NewUint(10), exp)\n\tif _, overflow := z.MulOverflow(z, exp); overflow {\n\t\treturn ErrBig256Range\n\t}\n\treturn nil\n}\n\n// ToString returns the decimal string representation of z. It returns an empty string if z is nil.\n// OBS: doesn't exist from holiman's uint256\nfunc (z *Uint) ToString() string {\n\tif z == nil {\n\t\treturn \"\"\n\t}\n\n\treturn z.Dec()\n}\n\n// MarshalJSON implements json.Marshaler.\n// MarshalJSON marshals using the 'decimal string' representation. This is _not_ compatible\n// with big.Uint: big.Uint marshals into JSON 'native' numeric format.\n//\n// The JSON native format is, on some platforms, (e.g. javascript), limited to 53-bit large\n// integer space. Thus, U256 uses string-format, which is not compatible with\n// big.int (big.Uint refuses to unmarshal a string representation).\nfunc (z *Uint) MarshalJSON() ([]byte, error) {\n\treturn []byte(`\"` + z.Dec() + `\"`), nil\n}\n\n// UnmarshalJSON implements json.Unmarshaler. UnmarshalJSON accepts either\n// - Quoted string: either hexadecimal OR decimal\n// - Not quoted string: only decimal\nfunc (z *Uint) UnmarshalJSON(input []byte) error {\n\tif len(input) \u003c 2 || input[0] != '\"' || input[len(input)-1] != '\"' {\n\t\t// if not quoted, it must be decimal\n\t\treturn z.fromDecimal(string(input))\n\t}\n\treturn z.UnmarshalText(input[1 : len(input)-1])\n}\n\n// MarshalText implements encoding.TextMarshaler\n// MarshalText marshals using the decimal representation (compatible with big.Uint)\nfunc (z *Uint) MarshalText() ([]byte, error) {\n\treturn []byte(z.Dec()), nil\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler. This method\n// can unmarshal either hexadecimal or decimal.\n// - For hexadecimal, the input _must_ be prefixed with 0x or 0X\nfunc (z *Uint) UnmarshalText(input []byte) error {\n\tif len(input) \u003e= 2 \u0026\u0026 input[0] == '0' \u0026\u0026 (input[1] == 'x' || input[1] == 'X') {\n\t\treturn z.fromHex(string(input))\n\t}\n\treturn z.fromDecimal(string(input))\n}\n\n// SetBytes interprets buf as the bytes of a big-endian unsigned\n// integer, sets z to that value, and returns z.\n// If buf is larger than 32 bytes, the last 32 bytes is used.\nfunc (z *Uint) SetBytes(buf []byte) *Uint {\n\tswitch l := len(buf); l {\n\tcase 0:\n\t\tz.Clear()\n\tcase 1:\n\t\tz.SetBytes1(buf)\n\tcase 2:\n\t\tz.SetBytes2(buf)\n\tcase 3:\n\t\tz.SetBytes3(buf)\n\tcase 4:\n\t\tz.SetBytes4(buf)\n\tcase 5:\n\t\tz.SetBytes5(buf)\n\tcase 6:\n\t\tz.SetBytes6(buf)\n\tcase 7:\n\t\tz.SetBytes7(buf)\n\tcase 8:\n\t\tz.SetBytes8(buf)\n\tcase 9:\n\t\tz.SetBytes9(buf)\n\tcase 10:\n\t\tz.SetBytes10(buf)\n\tcase 11:\n\t\tz.SetBytes11(buf)\n\tcase 12:\n\t\tz.SetBytes12(buf)\n\tcase 13:\n\t\tz.SetBytes13(buf)\n\tcase 14:\n\t\tz.SetBytes14(buf)\n\tcase 15:\n\t\tz.SetBytes15(buf)\n\tcase 16:\n\t\tz.SetBytes16(buf)\n\tcase 17:\n\t\tz.SetBytes17(buf)\n\tcase 18:\n\t\tz.SetBytes18(buf)\n\tcase 19:\n\t\tz.SetBytes19(buf)\n\tcase 20:\n\t\tz.SetBytes20(buf)\n\tcase 21:\n\t\tz.SetBytes21(buf)\n\tcase 22:\n\t\tz.SetBytes22(buf)\n\tcase 23:\n\t\tz.SetBytes23(buf)\n\tcase 24:\n\t\tz.SetBytes24(buf)\n\tcase 25:\n\t\tz.SetBytes25(buf)\n\tcase 26:\n\t\tz.SetBytes26(buf)\n\tcase 27:\n\t\tz.SetBytes27(buf)\n\tcase 28:\n\t\tz.SetBytes28(buf)\n\tcase 29:\n\t\tz.SetBytes29(buf)\n\tcase 30:\n\t\tz.SetBytes30(buf)\n\tcase 31:\n\t\tz.SetBytes31(buf)\n\tdefault:\n\t\tz.SetBytes32(buf[l-32:])\n\t}\n\treturn z\n}\n\n// SetBytes1 is identical to SetBytes(in[:1]), but panics is input is too short\nfunc (z *Uint) SetBytes1(in []byte) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(in[0])\n\treturn z\n}\n\n// SetBytes2 is identical to SetBytes(in[:2]), but panics is input is too short\nfunc (z *Uint) SetBytes2(in []byte) *Uint {\n\t_ = in[1] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\treturn z\n}\n\n// SetBytes3 is identical to SetBytes(in[:3]), but panics is input is too short\nfunc (z *Uint) SetBytes3(in []byte) *Uint {\n\t_ = in[2] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\treturn z\n}\n\n// SetBytes4 is identical to SetBytes(in[:4]), but panics is input is too short\nfunc (z *Uint) SetBytes4(in []byte) *Uint {\n\t_ = in[3] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\treturn z\n}\n\n// SetBytes5 is identical to SetBytes(in[:5]), but panics is input is too short\nfunc (z *Uint) SetBytes5(in []byte) *Uint {\n\t_ = in[4] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint40(in[0:5])\n\treturn z\n}\n\n// SetBytes6 is identical to SetBytes(in[:6]), but panics is input is too short\nfunc (z *Uint) SetBytes6(in []byte) *Uint {\n\t_ = in[5] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint48(in[0:6])\n\treturn z\n}\n\n// SetBytes7 is identical to SetBytes(in[:7]), but panics is input is too short\nfunc (z *Uint) SetBytes7(in []byte) *Uint {\n\t_ = in[6] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint56(in[0:7])\n\treturn z\n}\n\n// SetBytes8 is identical to SetBytes(in[:8]), but panics is input is too short\nfunc (z *Uint) SetBytes8(in []byte) *Uint {\n\t_ = in[7] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = binary.BigEndian.Uint64(in[0:8])\n\treturn z\n}\n\n// SetBytes9 is identical to SetBytes(in[:9]), but panics is input is too short\nfunc (z *Uint) SetBytes9(in []byte) *Uint {\n\t_ = in[8] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(in[0])\n\tz.arr[0] = binary.BigEndian.Uint64(in[1:9])\n\treturn z\n}\n\n// SetBytes10 is identical to SetBytes(in[:10]), but panics is input is too short\nfunc (z *Uint) SetBytes10(in []byte) *Uint {\n\t_ = in[9] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[2:10])\n\treturn z\n}\n\n// SetBytes11 is identical to SetBytes(in[:11]), but panics is input is too short\nfunc (z *Uint) SetBytes11(in []byte) *Uint {\n\t_ = in[10] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[0] = binary.BigEndian.Uint64(in[3:11])\n\treturn z\n}\n\n// SetBytes12 is identical to SetBytes(in[:12]), but panics is input is too short\nfunc (z *Uint) SetBytes12(in []byte) *Uint {\n\t_ = in[11] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[4:12])\n\treturn z\n}\n\n// SetBytes13 is identical to SetBytes(in[:13]), but panics is input is too short\nfunc (z *Uint) SetBytes13(in []byte) *Uint {\n\t_ = in[12] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint40(in[0:5])\n\tz.arr[0] = binary.BigEndian.Uint64(in[5:13])\n\treturn z\n}\n\n// SetBytes14 is identical to SetBytes(in[:14]), but panics is input is too short\nfunc (z *Uint) SetBytes14(in []byte) *Uint {\n\t_ = in[13] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint48(in[0:6])\n\tz.arr[0] = binary.BigEndian.Uint64(in[6:14])\n\treturn z\n}\n\n// SetBytes15 is identical to SetBytes(in[:15]), but panics is input is too short\nfunc (z *Uint) SetBytes15(in []byte) *Uint {\n\t_ = in[14] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint56(in[0:7])\n\tz.arr[0] = binary.BigEndian.Uint64(in[7:15])\n\treturn z\n}\n\n// SetBytes16 is identical to SetBytes(in[:16]), but panics is input is too short\nfunc (z *Uint) SetBytes16(in []byte) *Uint {\n\t_ = in[15] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[0] = binary.BigEndian.Uint64(in[8:16])\n\treturn z\n}\n\n// SetBytes17 is identical to SetBytes(in[:17]), but panics is input is too short\nfunc (z *Uint) SetBytes17(in []byte) *Uint {\n\t_ = in[16] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(in[0])\n\tz.arr[1] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[0] = binary.BigEndian.Uint64(in[9:17])\n\treturn z\n}\n\n// SetBytes18 is identical to SetBytes(in[:18]), but panics is input is too short\nfunc (z *Uint) SetBytes18(in []byte) *Uint {\n\t_ = in[17] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[0] = binary.BigEndian.Uint64(in[10:18])\n\treturn z\n}\n\n// SetBytes19 is identical to SetBytes(in[:19]), but panics is input is too short\nfunc (z *Uint) SetBytes19(in []byte) *Uint {\n\t_ = in[18] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[1] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[0] = binary.BigEndian.Uint64(in[11:19])\n\treturn z\n}\n\n// SetBytes20 is identical to SetBytes(in[:20]), but panics is input is too short\nfunc (z *Uint) SetBytes20(in []byte) *Uint {\n\t_ = in[19] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[0] = binary.BigEndian.Uint64(in[12:20])\n\treturn z\n}\n\n// SetBytes21 is identical to SetBytes(in[:21]), but panics is input is too short\nfunc (z *Uint) SetBytes21(in []byte) *Uint {\n\t_ = in[20] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint40(in[0:5])\n\tz.arr[1] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[0] = binary.BigEndian.Uint64(in[13:21])\n\treturn z\n}\n\n// SetBytes22 is identical to SetBytes(in[:22]), but panics is input is too short\nfunc (z *Uint) SetBytes22(in []byte) *Uint {\n\t_ = in[21] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint48(in[0:6])\n\tz.arr[1] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[0] = binary.BigEndian.Uint64(in[14:22])\n\treturn z\n}\n\n// SetBytes23 is identical to SetBytes(in[:23]), but panics is input is too short\nfunc (z *Uint) SetBytes23(in []byte) *Uint {\n\t_ = in[22] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint56(in[0:7])\n\tz.arr[1] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[0] = binary.BigEndian.Uint64(in[15:23])\n\treturn z\n}\n\n// SetBytes24 is identical to SetBytes(in[:24]), but panics is input is too short\nfunc (z *Uint) SetBytes24(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[1] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[0] = binary.BigEndian.Uint64(in[16:24])\n\treturn z\n}\n\n// SetBytes25 is identical to SetBytes(in[:25]), but panics is input is too short\nfunc (z *Uint) SetBytes25(in []byte) *Uint {\n\t_ = in[24] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(in[0])\n\tz.arr[2] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[1] = binary.BigEndian.Uint64(in[9:17])\n\tz.arr[0] = binary.BigEndian.Uint64(in[17:25])\n\treturn z\n}\n\n// SetBytes26 is identical to SetBytes(in[:26]), but panics is input is too short\nfunc (z *Uint) SetBytes26(in []byte) *Uint {\n\t_ = in[25] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[1] = binary.BigEndian.Uint64(in[10:18])\n\tz.arr[0] = binary.BigEndian.Uint64(in[18:26])\n\treturn z\n}\n\n// SetBytes27 is identical to SetBytes(in[:27]), but panics is input is too short\nfunc (z *Uint) SetBytes27(in []byte) *Uint {\n\t_ = in[26] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[2] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[1] = binary.BigEndian.Uint64(in[11:19])\n\tz.arr[0] = binary.BigEndian.Uint64(in[19:27])\n\treturn z\n}\n\n// SetBytes28 is identical to SetBytes(in[:28]), but panics is input is too short\nfunc (z *Uint) SetBytes28(in []byte) *Uint {\n\t_ = in[27] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[1] = binary.BigEndian.Uint64(in[12:20])\n\tz.arr[0] = binary.BigEndian.Uint64(in[20:28])\n\treturn z\n}\n\n// SetBytes29 is identical to SetBytes(in[:29]), but panics is input is too short\nfunc (z *Uint) SetBytes29(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint40(in[0:5])\n\tz.arr[2] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[1] = binary.BigEndian.Uint64(in[13:21])\n\tz.arr[0] = binary.BigEndian.Uint64(in[21:29])\n\treturn z\n}\n\n// SetBytes30 is identical to SetBytes(in[:30]), but panics is input is too short\nfunc (z *Uint) SetBytes30(in []byte) *Uint {\n\t_ = in[29] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint48(in[0:6])\n\tz.arr[2] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[1] = binary.BigEndian.Uint64(in[14:22])\n\tz.arr[0] = binary.BigEndian.Uint64(in[22:30])\n\treturn z\n}\n\n// SetBytes31 is identical to SetBytes(in[:31]), but panics is input is too short\nfunc (z *Uint) SetBytes31(in []byte) *Uint {\n\t_ = in[30] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint56(in[0:7])\n\tz.arr[2] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[1] = binary.BigEndian.Uint64(in[15:23])\n\tz.arr[0] = binary.BigEndian.Uint64(in[23:31])\n\treturn z\n}\n\n// SetBytes32 sets z to the value of the big-endian 256-bit unsigned integer in.\nfunc (z *Uint) SetBytes32(in []byte) *Uint {\n\t_ = in[31] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[2] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[1] = binary.BigEndian.Uint64(in[16:24])\n\tz.arr[0] = binary.BigEndian.Uint64(in[24:32])\n\treturn z\n}\n\n// Utility methods that are \"missing\" among the bigEndian.UintXX methods.\n\n// bigEndianUint40 returns the uint64 value represented by the 5 bytes in big-endian order.\nfunc bigEndianUint40(b []byte) uint64 {\n\t_ = b[4] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[4]) | uint64(b[3])\u003c\u003c8 | uint64(b[2])\u003c\u003c16 | uint64(b[1])\u003c\u003c24 |\n\t\tuint64(b[0])\u003c\u003c32\n}\n\n// bigEndianUint56 returns the uint64 value represented by the 7 bytes in big-endian order.\nfunc bigEndianUint56(b []byte) uint64 {\n\t_ = b[6] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[6]) | uint64(b[5])\u003c\u003c8 | uint64(b[4])\u003c\u003c16 | uint64(b[3])\u003c\u003c24 |\n\t\tuint64(b[2])\u003c\u003c32 | uint64(b[1])\u003c\u003c40 | uint64(b[0])\u003c\u003c48\n}\n\n// bigEndianUint48 returns the uint64 value represented by the 6 bytes in big-endian order.\nfunc bigEndianUint48(b []byte) uint64 {\n\t_ = b[5] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[5]) | uint64(b[4])\u003c\u003c8 | uint64(b[3])\u003c\u003c16 | uint64(b[2])\u003c\u003c24 |\n\t\tuint64(b[1])\u003c\u003c32 | uint64(b[0])\u003c\u003c40\n}\n"},{"name":"conversion_test.gno","body":"package uint256\n\nimport \"testing\"\n\nfunc TestIsUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0x0\", true},\n\t\t{\"0x1\", true},\n\t\t{\"0x10\", true},\n\t\t{\"0xffffffffffffffff\", true},\n\t\t{\"0x10000000000000000\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\tgot := x.IsUint64()\n\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"IsUint64(%s) = %v, want %v\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestDec(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tz Uint\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"zero\",\n\t\t\tz: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"less than 20 digits\",\n\t\t\tz: Uint{arr: [4]uint64{1234567890, 0, 0, 0}},\n\t\t\twant: \"1234567890\",\n\t\t},\n\t\t{\n\t\t\tname: \"max possible value\",\n\t\t\tz: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: twoPow256Sub1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.z.Dec()\n\t\t\tif result != tt.want {\n\t\t\t\tt.Errorf(\"Dec(%v) = %s, want %s\", tt.z, result, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUint_Scan(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput interface{}\n\t\twant *Uint\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tinput: nil,\n\t\t\twant: NewUint(0),\n\t\t},\n\t\t{\n\t\t\tname: \"valid scientific notation\",\n\t\t\tinput: \"1e4\",\n\t\t\twant: NewUint(10000),\n\t\t},\n\t\t{\n\t\t\tname: \"valid decimal string\",\n\t\t\tinput: \"12345\",\n\t\t\twant: NewUint(12345),\n\t\t},\n\t\t{\n\t\t\tname: \"valid byte slice\",\n\t\t\tinput: []byte(\"12345\"),\n\t\t\twant: NewUint(12345),\n\t\t},\n\t\t{\n\t\t\tname: \"invalid string\",\n\t\t\tinput: \"invalid\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"out of range\",\n\t\t\tinput: \"115792089237316195423570985008687907853269984665640564039457584007913129639936\", // 2^256\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"unsupported type\",\n\t\t\tinput: 123,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := new(Uint)\n\t\t\terr := z.Scan(tt.input)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Scan() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Scan() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\t}\n\t\t\t\tif !z.Eq(tt.want) {\n\t\t\t\t\tt.Errorf(\"Scan() = %v, want %v\", z, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpected string\n\t}{\n\t\t{[]byte{}, \"0\"},\n\t\t{[]byte{0x01}, \"1\"},\n\t\t{[]byte{0x12, 0x34}, \"4660\"},\n\t\t{[]byte{0x12, 0x34, 0x56}, \"1193046\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78}, \"305419896\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a}, \"78187493530\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"20015998343868\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"5124095576030430\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"1311768467463790320\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"335812727670730321938\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"85968058283706962416180\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"22007822920628982378542166\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"5634002667681019488906794616\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"1442304682926340989160139421850\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"369229998829143293224995691993788\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"94522879700260683065598897150409950\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"24197857203266734864793317670504947440\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"6194651444036284125387089323649266544658\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"1585830769673288736099094866854212235432500\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"405972677036361916441368285914678332270720086\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"103929005321308650608990281194157653061304342136\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"26605825362255014555901511985704359183693911586970\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"6811091292737283726310787068340315951025641366264508\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"1743639370940744633935561489495120883462564189763714270\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"446371678960830626287503741310750946166416432579510853360\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"114271149813972640329600957775552242218602606740354778460178\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"29253414352376995924377845190541374007962267325530823285805620\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"7488874074208510956640728368778591746038340435335890761166238806\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"1917151762997378804900026462407319486985815151445988034858557134456\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"490790851327328974054406774376273788668368678770172936923790626420890\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"125642457939796217357928134240326089899102381765164271852490400363748028\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"32164469232587831643629602365523479014170209731882053594237542493119495390\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"8234104123542484900769178205574010627627573691361805720124810878238590820080\"},\n\t\t// over 32 bytes (last 32 bytes are used)\n\t\t{append([]byte{0xff}, []byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}...), \"8234104123542484900769178205574010627627573691361805720124810878238590820080\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tz := new(Uint)\n\t\tz.SetBytes(test.input)\n\t\texpected := MustFromDecimal(test.expected)\n\t\tif z.Cmp(expected) != 0 {\n\t\t\tt.Errorf(\"SetBytes(%x) = %s, expected %s\", test.input, z.ToString(), test.expected)\n\t\t}\n\t}\n}\n"},{"name":"error.gno","body":"package uint256\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrEmptyString = errors.New(\"empty hex string\")\n\tErrSyntax = errors.New(\"invalid hex string\")\n\tErrRange = errors.New(\"number out of range\")\n\tErrMissingPrefix = errors.New(\"hex string without 0x prefix\")\n\tErrEmptyNumber = errors.New(\"hex string \\\"0x\\\"\")\n\tErrLeadingZero = errors.New(\"hex number with leading zero digits\")\n\tErrBig256Range = errors.New(\"hex number \u003e 256 bits\")\n\tErrBadBufferLength = errors.New(\"bad ssz buffer length\")\n\tErrBadEncodedLength = errors.New(\"bad ssz encoded length\")\n\tErrInvalidBase = errors.New(\"invalid base\")\n\tErrInvalidBitSize = errors.New(\"invalid bit size\")\n)\n\ntype u256Error struct {\n\tfn string // function name\n\tinput string\n\terr error\n}\n\nfunc (e *u256Error) Error() string {\n\treturn e.fn + \": \" + e.input + \": \" + e.err.Error()\n}\n\nfunc (e *u256Error) Unwrap() error {\n\treturn e.err\n}\n\nfunc errEmptyString(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyString}\n}\n\nfunc errSyntax(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrSyntax}\n}\n\nfunc errMissingPrefix(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrMissingPrefix}\n}\n\nfunc errEmptyNumber(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyNumber}\n}\n\nfunc errLeadingZero(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrLeadingZero}\n}\n\nfunc errRange(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrRange}\n}\n\nfunc errBig256Range(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBig256Range}\n}\n\nfunc errBadBufferLength(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBadBufferLength}\n}\n\nfunc errInvalidBase(fn string, base int) error {\n\treturn \u0026u256Error{fn: fn, input: string(base), err: ErrInvalidBase}\n}\n\nfunc errInvalidBitSize(fn string, bitSize int) error {\n\treturn \u0026u256Error{fn: fn, input: string(bitSize), err: ErrInvalidBitSize}\n}\n"},{"name":"mod.gno","body":"package uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Some utility functions\n\n// Reciprocal computes a 320-bit value representing 1/m\n//\n// Notes:\n// - specialized for m.arr[3] != 0, hence limited to 2^192 \u003c= m \u003c 2^256\n// - returns zero if m.arr[3] == 0\n// - starts with a 32-bit division, refines with newton-raphson iterations\nfunc Reciprocal(m *Uint) (mu [5]uint64) {\n\tif m.arr[3] == 0 {\n\t\treturn mu\n\t}\n\n\ts := bits.LeadingZeros64(m.arr[3]) // Replace with leadingZeros(m) for general case\n\tp := 255 - s // floor(log_2(m)), m\u003e0\n\n\t// 0 or a power of 2?\n\n\t// Check if at least one bit is set in m.arr[2], m.arr[1] or m.arr[0],\n\t// or at least two bits in m.arr[3]\n\n\tif m.arr[0]|m.arr[1]|m.arr[2]|(m.arr[3]\u0026(m.arr[3]-1)) == 0 {\n\n\t\tmu[4] = ^uint64(0) \u003e\u003e uint(p\u002663)\n\t\tmu[3] = ^uint64(0)\n\t\tmu[2] = ^uint64(0)\n\t\tmu[1] = ^uint64(0)\n\t\tmu[0] = ^uint64(0)\n\n\t\treturn mu\n\t}\n\n\t// Maximise division precision by left-aligning divisor\n\n\tvar (\n\t\ty Uint // left-aligned copy of m\n\t\tr0 uint32 // estimate of 2^31/y\n\t)\n\n\ty.Lsh(m, uint(s)) // 1/2 \u003c y \u003c 1\n\n\t// Extract most significant 32 bits\n\n\tyh := uint32(y.arr[3] \u003e\u003e 32)\n\n\tif yh == 0x80000000 { // Avoid overflow in division\n\t\tr0 = 0xffffffff\n\t} else {\n\t\tr0, _ = bits.Div32(0x80000000, 0, yh)\n\t}\n\n\t// First iteration: 32 -\u003e 64\n\n\tt1 := uint64(r0) // 2^31/y\n\tt1 *= t1 // 2^62/y^2\n\tt1, _ = bits.Mul64(t1, y.arr[3]) // 2^62/y^2 * 2^64/y / 2^64 = 2^62/y\n\n\tr1 := uint64(r0) \u003c\u003c 32 // 2^63/y\n\tr1 -= t1 // 2^63/y - 2^62/y = 2^62/y\n\tr1 *= 2 // 2^63/y\n\n\tif (r1 | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr1 = ^uint64(0)\n\t}\n\n\t// Second iteration: 64 -\u003e 128\n\n\t// square: 2^126/y^2\n\ta2h, a2l := bits.Mul64(r1, r1)\n\n\t// multiply by y: e2h:e2l:b2h = 2^126/y^2 * 2^128/y / 2^128 = 2^126/y\n\tb2h, _ := bits.Mul64(a2l, y.arr[2])\n\tc2h, c2l := bits.Mul64(a2l, y.arr[3])\n\td2h, d2l := bits.Mul64(a2h, y.arr[2])\n\te2h, e2l := bits.Mul64(a2h, y.arr[3])\n\n\tb2h, c := bits.Add64(b2h, c2l, 0)\n\te2l, c = bits.Add64(e2l, c2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t_, c = bits.Add64(b2h, d2l, 0)\n\te2l, c = bits.Add64(e2l, d2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t// subtract: t2h:t2l = 2^127/y - 2^126/y = 2^126/y\n\tt2l, b := bits.Sub64(0, e2l, 0)\n\tt2h, _ := bits.Sub64(r1, e2h, b)\n\n\t// double: r2h:r2l = 2^127/y\n\tr2l, c := bits.Add64(t2l, t2l, 0)\n\tr2h, _ := bits.Add64(t2h, t2h, c)\n\n\tif (r2h | r2l | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr2h = ^uint64(0)\n\t\tr2l = ^uint64(0)\n\t}\n\n\t// Third iteration: 128 -\u003e 192\n\n\t// square r2 (keep 256 bits): 2^190/y^2\n\ta3h, a3l := bits.Mul64(r2l, r2l)\n\tb3h, b3l := bits.Mul64(r2l, r2h)\n\tc3h, c3l := bits.Mul64(r2h, r2h)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\t// multiply by y: q = 2^190/y^2 * 2^192/y / 2^192 = 2^190/y\n\n\tx0 := a3l\n\tx1 := a3h\n\tx2 := c3l\n\tx3 := c3h\n\n\tvar q0, q1, q2, q3, q4, t0 uint64\n\n\tq0, _ = bits.Mul64(x2, y.arr[0])\n\tq1, t0 = bits.Mul64(x3, y.arr[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x1, y.arr[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x3, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x3, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, y.arr[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x3, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\t// subtract: t3 = 2^191/y - 2^190/y = 2^190/y\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\tt3l, b := bits.Sub64(0, q2, b)\n\tt3m, b := bits.Sub64(r2l, q3, b)\n\tt3h, _ := bits.Sub64(r2h, q4, b)\n\n\t// double: r3 = 2^191/y\n\tr3l, c := bits.Add64(t3l, t3l, 0)\n\tr3m, c := bits.Add64(t3m, t3m, c)\n\tr3h, _ := bits.Add64(t3h, t3h, c)\n\n\t// Fourth iteration: 192 -\u003e 320\n\n\t// square r3\n\n\ta4h, a4l := bits.Mul64(r3l, r3l)\n\tb4h, b4l := bits.Mul64(r3l, r3m)\n\tc4h, c4l := bits.Mul64(r3l, r3h)\n\td4h, d4l := bits.Mul64(r3m, r3m)\n\te4h, e4l := bits.Mul64(r3m, r3h)\n\tf4h, f4l := bits.Mul64(r3h, r3h)\n\n\tb4h, c = bits.Add64(b4h, c4l, 0)\n\te4l, c = bits.Add64(e4l, c4h, c)\n\te4h, _ = bits.Add64(e4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\t// multiply by y\n\n\tx1, x0 = bits.Mul64(d4h, y.arr[0])\n\tx3, x2 = bits.Mul64(f4h, y.arr[0])\n\tt1, t0 = bits.Mul64(f4l, y.arr[0])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx3, _ = bits.Add64(x3, 0, c)\n\n\tt1, t0 = bits.Mul64(d4h, y.arr[1])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx4, t0 := bits.Mul64(f4h, y.arr[1])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[1])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[1])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[2])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[2])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx5, t0 := bits.Mul64(f4h, y.arr[2])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[2])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[2])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[3])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[3])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx6, t0 := bits.Mul64(f4h, y.arr[3])\n\tx5, c = bits.Add64(x5, t0, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\tt1, t0 = bits.Mul64(a4l, y.arr[3])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[3])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[3])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, c = bits.Add64(x5, t1, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\n\t// subtract\n\t_, b = bits.Sub64(0, x0, 0)\n\t_, b = bits.Sub64(0, x1, b)\n\tr4l, b := bits.Sub64(0, x2, b)\n\tr4k, b := bits.Sub64(0, x3, b)\n\tr4j, b := bits.Sub64(r3l, x4, b)\n\tr4i, b := bits.Sub64(r3m, x5, b)\n\tr4h, _ := bits.Sub64(r3h, x6, b)\n\n\t// Multiply candidate for 1/4y by y, with full precision\n\n\tx0 = r4l\n\tx1 = r4k\n\tx2 = r4j\n\tx3 = r4i\n\tx4 = r4h\n\n\tq1, q0 = bits.Mul64(x0, y.arr[0])\n\tq3, q2 = bits.Mul64(x2, y.arr[0])\n\tq5, q4 := bits.Mul64(x4, y.arr[0])\n\n\tt1, t0 = bits.Mul64(x1, y.arr[0])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[0])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq6, t0 := bits.Mul64(x4, y.arr[1])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[1])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[1])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq7, t0 := bits.Mul64(x4, y.arr[2])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[2])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq8, t0 := bits.Mul64(x4, y.arr[3])\n\tq7, c = bits.Add64(q7, t0, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq4, c = bits.Add64(q4, t0, 0)\n\tq5, c = bits.Add64(q5, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[3])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, c = bits.Add64(q7, t1, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\t// Final adjustment\n\n\t// subtract q from 1/4\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\t_, b = bits.Sub64(0, q2, b)\n\t_, b = bits.Sub64(0, q3, b)\n\t_, b = bits.Sub64(0, q4, b)\n\t_, b = bits.Sub64(0, q5, b)\n\t_, b = bits.Sub64(0, q6, b)\n\t_, b = bits.Sub64(0, q7, b)\n\t_, b = bits.Sub64(uint64(1)\u003c\u003c62, q8, b)\n\n\t// decrement the result\n\tx0, t := bits.Sub64(r4l, 1, 0)\n\tx1, t = bits.Sub64(r4k, 0, t)\n\tx2, t = bits.Sub64(r4j, 0, t)\n\tx3, t = bits.Sub64(r4i, 0, t)\n\tx4, _ = bits.Sub64(r4h, 0, t)\n\n\t// commit the decrement if the subtraction underflowed (reciprocal was too large)\n\tif b != 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\t// Shift to correct bit alignment, truncating excess bits\n\n\tp = (p \u0026 63) - 1\n\n\tx0, c = bits.Add64(r4l, r4l, 0)\n\tx1, c = bits.Add64(r4k, r4k, c)\n\tx2, c = bits.Add64(r4j, r4j, c)\n\tx3, c = bits.Add64(r4i, r4i, c)\n\tx4, _ = bits.Add64(r4h, r4h, c)\n\n\tif p \u003c 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t\tp = 0 // avoid negative shift below\n\t}\n\n\t{\n\t\tr := uint(p) // right shift\n\t\tl := uint(64 - r) // left shift\n\n\t\tx0 = (r4l \u003e\u003e r) | (r4k \u003c\u003c l)\n\t\tx1 = (r4k \u003e\u003e r) | (r4j \u003c\u003c l)\n\t\tx2 = (r4j \u003e\u003e r) | (r4i \u003c\u003c l)\n\t\tx3 = (r4i \u003e\u003e r) | (r4h \u003c\u003c l)\n\t\tx4 = (r4h \u003e\u003e r)\n\t}\n\n\tif p \u003e 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\tmu[0] = r4l\n\tmu[1] = r4k\n\tmu[2] = r4j\n\tmu[3] = r4i\n\tmu[4] = r4h\n\n\treturn mu\n}\n\n// reduce4 computes the least non-negative residue of x modulo m\n//\n// requires a four-word modulus (m.arr[3] \u003e 1) and its inverse (mu)\nfunc reduce4(x [8]uint64, m *Uint, mu [5]uint64) (z Uint) {\n\t// NB: Most variable names in the comments match the pseudocode for\n\t// \tBarrett reduction in the Handbook of Applied Cryptography.\n\n\t// q1 = x/2^192\n\n\tx0 := x[3]\n\tx1 := x[4]\n\tx2 := x[5]\n\tx3 := x[6]\n\tx4 := x[7]\n\n\t// q2 = q1 * mu; q3 = q2 / 2^320\n\n\tvar q0, q1, q2, q3, q4, q5, t0, t1, c uint64\n\n\tq0, _ = bits.Mul64(x3, mu[0])\n\tq1, t0 = bits.Mul64(x4, mu[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x2, mu[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x4, mu[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x3, mu[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, mu[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x4, mu[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x1, mu[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x3, mu[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, mu[3])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, mu[3])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x4, mu[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, mu[4])\n\t_, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, mu[4])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq5, t0 = bits.Mul64(x4, mu[4])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[4])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[4])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\t// Drop the fractional part of q3\n\n\tq0 = q1\n\tq1 = q2\n\tq2 = q3\n\tq3 = q4\n\tq4 = q5\n\n\t// r1 = x mod 2^320\n\n\tx0 = x[0]\n\tx1 = x[1]\n\tx2 = x[2]\n\tx3 = x[3]\n\tx4 = x[4]\n\n\t// r2 = q3 * m mod 2^320\n\n\tvar r0, r1, r2, r3, r4 uint64\n\n\tr4, r3 = bits.Mul64(q0, m.arr[3])\n\t_, t0 = bits.Mul64(q1, m.arr[3])\n\tr4, _ = bits.Add64(r4, t0, 0)\n\n\tt1, r2 = bits.Mul64(q0, m.arr[2])\n\tr3, c = bits.Add64(r3, t1, 0)\n\t_, t0 = bits.Mul64(q2, m.arr[2])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[2])\n\tr3, c = bits.Add64(r3, t0, 0)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, r1 = bits.Mul64(q0, m.arr[1])\n\tr2, c = bits.Add64(r2, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[1])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[1])\n\tr2, c = bits.Add64(r2, t0, 0)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q3, m.arr[1])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, r0 = bits.Mul64(q0, m.arr[0])\n\tr1, c = bits.Add64(r1, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[0])\n\tr2, c = bits.Add64(r2, t0, c)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q4, m.arr[0])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[0])\n\tr1, c = bits.Add64(r1, t0, 0)\n\tr2, c = bits.Add64(r2, t1, c)\n\tt1, t0 = bits.Mul64(q3, m.arr[0])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\t// r = r1 - r2\n\n\tvar b uint64\n\n\tr0, b = bits.Sub64(x0, r0, 0)\n\tr1, b = bits.Sub64(x1, r1, b)\n\tr2, b = bits.Sub64(x2, r2, b)\n\tr3, b = bits.Sub64(x3, r3, b)\n\tr4, b = bits.Sub64(x4, r4, b)\n\n\t// if r\u003c0 then r+=m\n\n\tif b != 0 {\n\t\tr0, c = bits.Add64(r0, m.arr[0], 0)\n\t\tr1, c = bits.Add64(r1, m.arr[1], c)\n\t\tr2, c = bits.Add64(r2, m.arr[2], c)\n\t\tr3, c = bits.Add64(r3, m.arr[3], c)\n\t\tr4, _ = bits.Add64(r4, 0, c)\n\t}\n\n\t// while (r\u003e=m) r-=m\n\n\tfor {\n\t\t// q = r - m\n\t\tq0, b = bits.Sub64(r0, m.arr[0], 0)\n\t\tq1, b = bits.Sub64(r1, m.arr[1], b)\n\t\tq2, b = bits.Sub64(r2, m.arr[2], b)\n\t\tq3, b = bits.Sub64(r3, m.arr[3], b)\n\t\tq4, b = bits.Sub64(r4, 0, b)\n\n\t\t// if borrow break\n\t\tif b != 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// r = q\n\t\tr4, r3, r2, r1, r0 = q4, q3, q2, q1, q0\n\t}\n\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = r3, r2, r1, r0\n\n\treturn z\n}\n"},{"name":"uint256.gno","body":"// Ported from https://github.com/holiman/uint256\n// This package provides a 256-bit unsigned integer type, Uint256, and associated functions.\npackage uint256\n\nimport (\n\t\"errors\"\n\t\"math/bits\"\n\t\"strconv\"\n)\n\nconst (\n\tMaxUint64 = 1\u003c\u003c64 - 1\n\tuintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63)\n)\n\n// Uint is represented as an array of 4 uint64, in little-endian order,\n// so that Uint[3] is the most significant, and Uint[0] is the least significant\ntype Uint struct {\n\tarr [4]uint64\n}\n\n// NewUint returns a new initialized Uint.\nfunc NewUint(val uint64) *Uint {\n\tz := \u0026Uint{arr: [4]uint64{val, 0, 0, 0}}\n\treturn z\n}\n\n// Zero returns a new Uint initialized to zero.\nfunc Zero() *Uint {\n\treturn NewUint(0)\n}\n\n// One returns a new Uint initialized to one.\nfunc One() *Uint {\n\treturn NewUint(1)\n}\n\n// SetAllOne sets all the bits of z to 1\nfunc (z *Uint) SetAllOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, MaxUint64\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Uint) Set(x *Uint) *Uint {\n\t*z = *x\n\n\treturn z\n}\n\n// SetOne sets z to 1\nfunc (z *Uint) SetOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 1\n\treturn z\n}\n\nconst twoPow256Sub1 = \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n\n// SetFromDecimal sets z from the given string, interpreted as a decimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Uint).SetString(..., 10) method.\n// Notable differences:\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0\",\n// - (this method does not accept any negative input as valid))\nfunc (z *Uint) SetFromDecimal(s string) (err error) {\n\t// Remove max one leading +\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '+' {\n\t\ts = s[1:]\n\t}\n\t// Remove any number of leading zeroes\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '0' {\n\t\tvar i int\n\t\tvar c rune\n\t\tfor i, c = range s {\n\t\t\tif c != '0' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ts = s[i:]\n\t}\n\tif len(s) \u003c len(twoPow256Sub1) {\n\t\treturn z.fromDecimal(s)\n\t}\n\tif len(s) == len(twoPow256Sub1) {\n\t\tif s \u003e twoPow256Sub1 {\n\t\t\treturn ErrBig256Range\n\t\t}\n\t\treturn z.fromDecimal(s)\n\t}\n\treturn ErrBig256Range\n}\n\n// FromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string. Numbers larger than 256 bits are not accepted.\nfunc FromDecimal(decimal string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromDecimal(decimal string) *Uint {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// multipliers holds the values that are needed for fromDecimal\nvar multipliers = [5]*Uint{\n\tnil, // represents first round, no multiplication needed\n\t{[4]uint64{10000000000000000000, 0, 0, 0}}, // 10 ^ 19\n\t{[4]uint64{687399551400673280, 5421010862427522170, 0, 0}}, // 10 ^ 38\n\t{[4]uint64{5332261958806667264, 17004971331911604867, 2938735877055718769, 0}}, // 10 ^ 57\n\t{[4]uint64{0, 8607968719199866880, 532749306367912313, 1593091911132452277}}, // 10 ^ 76\n}\n\n// fromDecimal is a helper function to only ever be called via SetFromDecimal\n// this function takes a string and chunks it up, calling ParseUint on it up to 5 times\n// these chunks are then multiplied by the proper power of 10, then added together.\nfunc (z *Uint) fromDecimal(bs string) error {\n\t// first clear the input\n\tz.Clear()\n\t// the maximum value of uint64 is 18446744073709551615, which is 20 characters\n\t// one less means that a string of 19 9's is always within the uint64 limit\n\tvar (\n\t\tnum uint64\n\t\terr error\n\t\tremaining = len(bs)\n\t)\n\tif remaining == 0 {\n\t\treturn errors.New(\"EOF\")\n\t}\n\t// We proceed in steps of 19 characters (nibbles), from least significant to most significant.\n\t// This means that the first (up to) 19 characters do not need to be multiplied.\n\t// In the second iteration, our slice of 19 characters needs to be multipleied\n\t// by a factor of 10^19. Et cetera.\n\tfor i, mult := range multipliers {\n\t\tif remaining \u003c= 0 {\n\t\t\treturn nil // Done\n\t\t} else if remaining \u003e 19 {\n\t\t\tnum, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64)\n\t\t} else {\n\t\t\t// Final round\n\t\t\tnum, err = strconv.ParseUint(bs, 10, 64)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// add that number to our running total\n\t\tif i == 0 {\n\t\t\tz.SetUint64(num)\n\t\t} else {\n\t\t\tbase := NewUint(num)\n\t\t\tz.Add(z, base.Mul(base, mult))\n\t\t}\n\t\t// Chop off another 19 characters\n\t\tif remaining \u003e 19 {\n\t\t\tbs = bs[0 : remaining-19]\n\t\t}\n\t\tremaining -= 19\n\t}\n\treturn nil\n}\n\n// Byte sets z to the value of the byte at position n,\n// with 'z' considered as a big-endian 32-byte integer\n// if 'n' \u003e 32, f is set to 0\n// Example: f = '5', n=31 =\u003e 5\nfunc (z *Uint) Byte(n *Uint) *Uint {\n\t// in z, z.arr[0] is the least significant\n\tif number, overflow := n.Uint64WithOverflow(); !overflow {\n\t\tif number \u003c 32 {\n\t\t\tnumber := z.arr[4-1-number/8]\n\t\t\toffset := (n.arr[0] \u0026 0x7) \u003c\u003c 3 // 8*(n.d % 8)\n\t\t\tz.arr[0] = (number \u0026 (0xff00000000000000 \u003e\u003e offset)) \u003e\u003e (56 - offset)\n\t\t\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\t\t\treturn z\n\t\t}\n\t}\n\n\treturn z.Clear()\n}\n\n// BitLen returns the number of bits required to represent z\nfunc (z *Uint) BitLen() int {\n\tswitch {\n\tcase z.arr[3] != 0:\n\t\treturn 192 + bits.Len64(z.arr[3])\n\tcase z.arr[2] != 0:\n\t\treturn 128 + bits.Len64(z.arr[2])\n\tcase z.arr[1] != 0:\n\t\treturn 64 + bits.Len64(z.arr[1])\n\tdefault:\n\t\treturn bits.Len64(z.arr[0])\n\t}\n}\n\n// ByteLen returns the number of bytes required to represent z\nfunc (z *Uint) ByteLen() int {\n\treturn (z.BitLen() + 7) / 8\n}\n\n// Clear sets z to 0\nfunc (z *Uint) Clear() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 0\n\treturn z\n}\n\nconst (\n\t// hextable = \"0123456789abcdef\"\n\tbintable = \"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\a\\b\\t\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n\tbadNibble = 0xff\n)\n\n// SetFromHex sets z from the given string, interpreted as a hexadecimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Int).SetString(..., 16) method.\n// Notable differences:\n// - This method _require_ \"0x\" or \"0X\" prefix.\n// - This method does not accept zero-prefixed hex, e.g. \"0x0001\"\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0x0\",\n// - (this method does not accept any negative input as valid)\nfunc (z *Uint) SetFromHex(hex string) error {\n\treturn z.fromHex(hex)\n}\n\n// fromHex is the internal implementation of parsing a hex-string.\nfunc (z *Uint) fromHex(hex string) error {\n\tif err := checkNumberS(hex); err != nil {\n\t\treturn err\n\t}\n\tif len(hex) \u003e 66 {\n\t\treturn ErrBig256Range\n\t}\n\tz.Clear()\n\tend := len(hex)\n\tfor i := 0; i \u003c 4; i++ {\n\t\tstart := end - 16\n\t\tif start \u003c 2 {\n\t\t\tstart = 2\n\t\t}\n\t\tfor ri := start; ri \u003c end; ri++ {\n\t\t\tnib := bintable[hex[ri]]\n\t\t\tif nib == badNibble {\n\t\t\t\treturn ErrSyntax\n\t\t\t}\n\t\t\tz.arr[i] = z.arr[i] \u003c\u003c 4\n\t\t\tz.arr[i] += uint64(nib)\n\t\t}\n\t\tend = start\n\t}\n\treturn nil\n}\n\n// FromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string. The string is required to be '0x'-prefixed\n// Numbers larger than 256 bits are not accepted.\nfunc FromHex(hex string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromHex(hex string) *Uint {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// Clone creates a new Uint identical to z\nfunc (z *Uint) Clone() *Uint {\n\tvar x Uint\n\tx.arr[0] = z.arr[0]\n\tx.arr[1] = z.arr[1]\n\tx.arr[2] = z.arr[2]\n\tx.arr[3] = z.arr[3]\n\n\treturn \u0026x\n}\n"},{"name":"uint256_test.gno","body":"package uint256\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSetAllOne(t *testing.T) {\n\tz := Zero()\n\tz.SetAllOne()\n\tif z.ToString() != twoPow256Sub1 {\n\t\tt.Errorf(\"Expected all ones, got %s\", z.ToString())\n\t}\n}\n\nfunc TestByte(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\tposition uint64\n\t\texpected byte\n\t}{\n\t\t{\"0x1000000000000000000000000000000000000000000000000000000000000000\", 0, 16},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 0, 255},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 31, 255},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tn := NewUint(tt.position)\n\t\tresult := z.Byte(n)\n\n\t\tif result.arr[0] != uint64(tt.expected) {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Position: %d, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.position, tt.expected, result.arr[0])\n\t\t}\n\n\t\t// check other array elements are 0\n\t\tif result.arr[1] != 0 || result.arr[2] != 0 || result.arr[3] != 0 {\n\t\t\tt.Errorf(\"Test case %d failed. Non-zero values in upper bytes\", i)\n\t\t}\n\t}\n\n\t// overflow\n\tz, _ := FromHex(\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\")\n\tn := NewUint(32)\n\tresult := z.Byte(n)\n\n\tif !result.IsZero() {\n\t\tt.Errorf(\"Expected zero for position \u003e= 32, got %v\", result)\n\t}\n}\n\nfunc TestBitLen(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"0x0\", 0},\n\t\t{\"0x1\", 1},\n\t\t{\"0xff\", 8},\n\t\t{\"0x100\", 9},\n\t\t{\"0xffff\", 16},\n\t\t{\"0x10000\", 17},\n\t\t{\"0xffffffffffffffff\", 64},\n\t\t{\"0x10000000000000000\", 65},\n\t\t{\"0xffffffffffffffffffffffffffffffff\", 128},\n\t\t{\"0x100000000000000000000000000000000\", 129},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 256},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.BitLen()\n\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.expected, result)\n\t\t}\n\t}\n}\n\nfunc TestByteLen(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"0x0\", 0},\n\t\t{\"0x1\", 1},\n\t\t{\"0xff\", 1},\n\t\t{\"0x100\", 2},\n\t\t{\"0xffff\", 2},\n\t\t{\"0x10000\", 3},\n\t\t{\"0xffffffffffffffff\", 8},\n\t\t{\"0x10000000000000000\", 9},\n\t\t{\"0xffffffffffffffffffffffffffffffff\", 16},\n\t\t{\"0x100000000000000000000000000000000\", 17},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 32},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.ByteLen()\n\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.expected, result)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t}{\n\t\t{\"0x1\", \"1\"},\n\t\t{\"0x100\", \"256\"},\n\t\t{\"0x10000000000000000\", \"18446744073709551616\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.Clone()\n\t\tif result.ToString() != tt.expected {\n\t\t\tt.Errorf(\"Test %s failed. Expected %s, got %s\", tt.input, tt.expected, result.ToString())\n\t\t}\n\t}\n}\n"},{"name":"utils.gno","body":"package uint256\n\nfunc checkNumberS(input string) error {\n\tconst fn = \"UnmarshalText\"\n\tl := len(input)\n\tif l == 0 {\n\t\treturn errEmptyString(fn, input)\n\t}\n\tif l \u003c 2 || input[0] != '0' ||\n\t\t(input[1] != 'x' \u0026\u0026 input[1] != 'X') {\n\t\treturn errMissingPrefix(fn, input)\n\t}\n\tif l == 2 {\n\t\treturn errEmptyNumber(fn, input)\n\t}\n\tif len(input) \u003e 3 \u0026\u0026 input[2] == '0' {\n\t\treturn errLeadingZero(fn, input)\n\t}\n\treturn nil\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"int256","path":"gno.land/p/demo/int256","files":[{"name":"LICENSE","body":"MIT License\n\nCopyright (c) 2023 Trịnh Đức Bảo Linh(Kevin)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."},{"name":"README.md","body":"# Fixed size signed 256-bit math library\n\n1. This is a library specialized at replacing the big.Int library for math based on signed 256-bit types.\n2. It uses [uint256](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/uint256) as the underlying type.\n\nported from [mempooler/int256](https://github.com/mempooler/int256)\n"},{"name":"absolute.gno","body":"package int256\n\nimport \"gno.land/p/demo/uint256\"\n\n// Abs returns |z|\nfunc (z *Int) Abs() *uint256.Uint {\n\treturn z.abs.Clone()\n}\n\n// AbsGt returns true if |z| \u003e x, where x is a uint256\nfunc (z *Int) AbsGt(x *uint256.Uint) bool {\n\treturn z.abs.Gt(x)\n}\n\n// AbsLt returns true if |z| \u003c x, where x is a uint256\nfunc (z *Int) AbsLt(x *uint256.Uint) bool {\n\treturn z.abs.Lt(x)\n}\n"},{"name":"absolute_test.gno","body":"package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestAbs(t *testing.T) {\n\ttests := []struct {\n\t\tx, want string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"1\"},\n\t\t{\"-2\", \"2\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Abs()\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"Abs(%s) = %v, want %v\", tc.x, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n\nfunc TestAbsGt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"false\"},\n\t\t{\"1\", \"0\", \"true\"},\n\t\t{\"-1\", \"0\", \"true\"},\n\t\t{\"-1\", \"1\", \"false\"},\n\t\t{\"-2\", \"1\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"false\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.AbsGt(y)\n\n\t\tif got != (tc.want == \"true\") {\n\t\t\tt.Errorf(\"AbsGt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestAbsLt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"false\"},\n\t\t{\"1\", \"0\", \"false\"},\n\t\t{\"-1\", \"0\", \"false\"},\n\t\t{\"-1\", \"1\", \"false\"},\n\t\t{\"-2\", \"1\", \"false\"},\n\t\t{\"-5\", \"10\", \"true\"},\n\t\t{\"31330\", \"31337\", \"true\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\", \"false\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"false\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"false\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.AbsLt(y)\n\n\t\tif got != (tc.want == \"true\") {\n\t\t\tt.Errorf(\"AbsLt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n"},{"name":"arithmetic.gno","body":"package int256\n\nimport \"gno.land/p/demo/uint256\"\n\nfunc (z *Int) Add(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tif x.neg == y.neg {\n\t\t// If both numbers have the same sign, add their absolute values\n\t\tz.abs.Add(x.abs, y.abs)\n\t\tz.neg = x.neg\n\t} else {\n\t\tswitch x.abs.Cmp(y.abs) {\n\t\tcase 1: // x \u003e y\n\t\t\tz.abs.Sub(x.abs, y.abs)\n\t\t\tz.neg = x.neg\n\t\tcase -1: // x \u003c y\n\t\t\tz.abs.Sub(y.abs, x.abs)\n\t\t\tz.neg = y.neg\n\t\tcase 0: // x == y\n\t\t\tz.abs = uint256.NewUint(0)\n\t\t}\n\t}\n\n\treturn z\n}\n\n// AddUint256 set z to the sum x + y, where y is a uint256, and returns z\nfunc (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int {\n\tif x.neg {\n\t\tif x.abs.Gt(y) {\n\t\t\tz.abs.Sub(x.abs, y)\n\t\t\tz.neg = true\n\t\t} else {\n\t\t\tz.abs.Sub(y, x.abs)\n\t\t\tz.neg = false\n\t\t}\n\t} else {\n\t\tz.abs.Add(x.abs, y)\n\t\tz.neg = false\n\t}\n\treturn z\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\nfunc AddDelta(z, x *uint256.Uint, y *Int) {\n\tif y.neg {\n\t\tz.Sub(x, y.abs)\n\t} else {\n\t\tz.Add(x, y.abs)\n\t}\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\nfunc AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool {\n\tvar overflow bool\n\tif y.neg {\n\t\t_, overflow = z.SubOverflow(x, y.abs)\n\t} else {\n\t\t_, overflow = z.AddOverflow(x, y.abs)\n\t}\n\treturn overflow\n}\n\n// Sub sets z to the difference x-y and returns z.\nfunc (z *Int) Sub(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tif x.neg != y.neg {\n\t\t// If sign are different, add the absolute values\n\t\tz.abs.Add(x.abs, y.abs)\n\t\tz.neg = x.neg\n\t} else {\n\t\tswitch x.abs.Cmp(y.abs) {\n\t\tcase 1: // x \u003e y\n\t\t\tz.abs.Sub(x.abs, y.abs)\n\t\t\tz.neg = x.neg\n\t\tcase -1: // x \u003c y\n\t\t\tz.abs.Sub(y.abs, x.abs)\n\t\t\tz.neg = !x.neg\n\t\tcase 0: // x == y\n\t\t\tz.abs = uint256.NewUint(0)\n\t\t}\n\t}\n\n\t// Ensure zero is always positive\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t}\n\treturn z\n}\n\n// SubUint256 set z to the difference x - y, where y is a uint256, and returns z\nfunc (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int {\n\tif x.neg {\n\t\tz.abs.Add(x.abs, y)\n\t\tz.neg = true\n\t} else {\n\t\tif x.abs.Lt(y) {\n\t\t\tz.abs.Sub(y, x.abs)\n\t\t\tz.neg = true\n\t\t} else {\n\t\t\tz.abs.Sub(x.abs, y)\n\t\t\tz.neg = false\n\t\t}\n\t}\n\treturn z\n}\n\n// Mul sets z to the product x*y and returns z.\nfunc (z *Int) Mul(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tz.abs = z.abs.Mul(x.abs, y.abs)\n\tz.neg = x.neg != y.neg \u0026\u0026 !z.abs.IsZero() // 0 has no sign\n\treturn z\n}\n\n// MulUint256 sets z to the product x*y, where y is a uint256, and returns z\nfunc (z *Int) MulUint256(x *Int, y *uint256.Uint) *Int {\n\tz.abs.Mul(x.abs, y)\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t} else {\n\t\tz.neg = x.neg\n\t}\n\treturn z\n}\n\n// Div sets z to the quotient x/y for y != 0 and returns z.\nfunc (z *Int) Div(x, y *Int) *Int {\n\tz.initiateAbs()\n\n\tif y.abs.IsZero() {\n\t\tpanic(\"division by zero\")\n\t}\n\n\tz.abs.Div(x.abs, y.abs)\n\tz.neg = (x.neg != y.neg) \u0026\u0026 !z.abs.IsZero() // 0 has no sign\n\n\treturn z\n}\n\n// DivUint256 sets z to the quotient x/y, where y is a uint256, and returns z\n// If y == 0, z is set to 0\nfunc (z *Int) DivUint256(x *Int, y *uint256.Uint) *Int {\n\tz.abs.Div(x.abs, y)\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t} else {\n\t\tz.neg = x.neg\n\t}\n\treturn z\n}\n\n// Quo sets z to the quotient x/y for y != 0 and returns z.\n// If y == 0, a division-by-zero run-time panic occurs.\n// OBS: differs from mempooler int256, we need to panic manually if y == 0\n// Quo implements truncated division (like Go); see QuoRem for more details.\nfunc (z *Int) Quo(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(\"division by zero\")\n\t}\n\n\tz.initiateAbs()\n\n\tz.abs = z.abs.Div(x.abs, y.abs)\n\tz.neg = !(z.abs.IsZero()) \u0026\u0026 x.neg != y.neg // 0 has no sign\n\treturn z\n}\n\n// Rem sets z to the remainder x%y for y != 0 and returns z.\n// If y == 0, a division-by-zero run-time panic occurs.\n// OBS: differs from mempooler int256, we need to panic manually if y == 0\n// Rem implements truncated modulus (like Go); see QuoRem for more details.\nfunc (z *Int) Rem(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(\"division by zero\")\n\t}\n\n\tz.initiateAbs()\n\n\tz.abs.Mod(x.abs, y.abs)\n\tz.neg = z.abs.Sign() \u003e 0 \u0026\u0026 x.neg // 0 has no sign\n\treturn z\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// If y == 0, z is set to 0 (OBS: differs from the big.Int)\nfunc (z *Int) Mod(x, y *Int) *Int {\n\tif x.neg {\n\t\tz.abs.Div(x.abs, y.abs)\n\t\tz.abs.Add(z.abs, one)\n\t\tz.abs.Mul(z.abs, y.abs)\n\t\tz.abs.Sub(z.abs, x.abs)\n\t\tz.abs.Mod(z.abs, y.abs)\n\t} else {\n\t\tz.abs.Mod(x.abs, y.abs)\n\t}\n\tz.neg = false\n\treturn z\n}\n"},{"name":"arithmetic_test.gno","body":"package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestAdd(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\"},\n\t\t// NEGATIVE\n\t\t{\"-1\", \"1\", \"0\"},\n\t\t{\"1\", \"-1\", \"0\"},\n\t\t{\"3\", \"-3\", \"0\"},\n\t\t{\"-1\", \"-1\", \"-2\"},\n\t\t{\"-1\", \"-2\", \"-3\"},\n\t\t{\"-1\", \"3\", \"2\"},\n\t\t{\"3\", \"-1\", \"2\"},\n\t\t// OVERFLOW\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Add(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Add(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\"},\n\t\t{\"-1\", \"1\", \"0\"},\n\t\t{\"-1\", \"3\", \"2\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"-1\"},\n\t\t// OVERFLOW\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.AddUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"AddUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddDelta(t *testing.T) {\n\ttests := []struct {\n\t\tz, x, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"0\", \"0\"},\n\t\t{\"0\", \"0\", \"1\", \"1\"},\n\t\t{\"0\", \"1\", \"0\", \"1\"},\n\t\t{\"0\", \"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\", \"5\"},\n\t\t{\"5\", \"10\", \"-3\", \"7\"},\n\t\t// underflow\n\t\t{\"1\", \"2\", \"-3\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz, err := uint256.FromDecimal(tc.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tx, err := uint256.FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := uint256.FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tAddDelta(z, x, y)\n\n\t\tif z.Neq(want) {\n\t\t\tt.Errorf(\"AddDelta(%s, %s, %s) = %v, want %v\", tc.z, tc.x, tc.y, z.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestAddDeltaOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tz, x, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", \"0\", false},\n\t\t// underflow\n\t\t{\"1\", \"2\", \"-3\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz, err := uint256.FromDecimal(tc.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tx, err := uint256.FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tresult := AddDeltaOverflow(z, x, y)\n\t\tif result != tc.want {\n\t\t\tt.Errorf(\"AddDeltaOverflow(%s, %s, %s) = %v, want %v\", tc.z, tc.x, tc.y, result, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSub(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"-1\", \"1\", \"-2\"},\n\t\t{\"1\", \"-1\", \"2\"},\n\t\t{\"-1\", \"-1\", \"0\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"0\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{x: \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", y: \"1\", want: \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Sub(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Sub(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestSubUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"-1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"1\", \"2\", \"-1\"},\n\t\t{\"-1\", \"1\", \"-2\"},\n\t\t{\"-1\", \"3\", \"-4\"},\n\t\t// underflow\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"-0\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"-1\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"3\", \"-2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.SubUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"SubUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMul(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"5\", \"3\", \"15\"},\n\t\t{\"-5\", \"3\", \"-15\"},\n\t\t{\"5\", \"-3\", \"-15\"},\n\t\t{\"0\", \"3\", \"0\"},\n\t\t{\"3\", \"0\", \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Mul(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mul(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMulUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1\", \"2\", \"2\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"-1\", \"3\", \"-3\"},\n\t\t{\"3\", \"4\", \"12\"},\n\t\t{\"-3\", \"4\", \"-12\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639932\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"115792089237316195423570985008687907853269984665640564039457584007913129639932\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.MulUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"MulUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestDiv(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, expected string\n\t}{\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"1\", \"-1\", \"-1\"},\n\t\t{\"-1\", \"-1\", \"1\"},\n\t\t{\"-6\", \"3\", \"-2\"},\n\t\t{\"10\", \"-2\", \"-5\"},\n\t\t{\"-10\", \"3\", \"-3\"},\n\t\t{\"7\", \"3\", \"2\"},\n\t\t{\"-7\", \"3\", \"-2\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"}, // Max uint256 / 2\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.x+\"/\"+tt.y, func(t *testing.T) {\n\t\t\tx := MustFromDecimal(tt.x)\n\t\t\ty := MustFromDecimal(tt.y)\n\t\t\tresult := Zero().Div(x, y)\n\t\t\tif result.ToString() != tt.expected {\n\t\t\t\tt.Errorf(\"Div(%s, %s) = %s, want %s\", tt.x, tt.y, result.ToString(), tt.expected)\n\t\t\t}\n\t\t\tif result.abs.IsZero() \u0026\u0026 result.neg {\n\t\t\t\tt.Errorf(\"Div(%s, %s) resulted in negative zero\", tt.x, tt.y)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Division by zero\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"Div(1, 0) did not panic\")\n\t\t\t}\n\t\t}()\n\t\tx := MustFromDecimal(\"1\")\n\t\ty := MustFromDecimal(\"0\")\n\t\tZero().Div(x, y)\n\t})\n}\n\nfunc TestDivUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1\", \"2\", \"0\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"-1\", \"3\", \"0\"},\n\t\t{\"4\", \"3\", \"1\"},\n\t\t{\"25\", \"5\", \"5\"},\n\t\t{\"25\", \"4\", \"6\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"-57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639934\", \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.DivUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"DivUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestQuo(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"10\"},\n\t\t{\"10\", \"-1\", \"-10\"},\n\t\t{\"-10\", \"1\", \"-10\"},\n\t\t{\"-10\", \"-1\", \"10\"},\n\t\t{\"10\", \"-3\", \"-3\"},\n\t\t{\"10\", \"3\", \"3\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Quo(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Quo(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestRem(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"0\"},\n\t\t{\"10\", \"-1\", \"0\"},\n\t\t{\"-10\", \"1\", \"0\"},\n\t\t{\"-10\", \"-1\", \"0\"},\n\t\t{\"10\", \"3\", \"1\"},\n\t\t{\"10\", \"-3\", \"1\"},\n\t\t{\"-10\", \"3\", \"-1\"},\n\t\t{\"-10\", \"-3\", \"-1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Rem(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rem(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n\nfunc TestMod(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"0\", \"0\"},\n\t\t{\"10\", \"1\", \"0\"},\n\t\t{\"10\", \"-1\", \"0\"},\n\t\t{\"-10\", \"0\", \"0\"},\n\t\t{\"-10\", \"1\", \"0\"},\n\t\t{\"-10\", \"-1\", \"0\"},\n\t\t{\"10\", \"3\", \"1\"},\n\t\t{\"10\", \"-3\", \"1\"},\n\t\t{\"-10\", \"3\", \"2\"},\n\t\t{\"-10\", \"-3\", \"2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Mod(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tc.x, tc.y, got.ToString(), want.ToString())\n\t\t}\n\t}\n}\n"},{"name":"bitwise.gno","body":"package int256\n\nimport (\n\t\"gno.land/p/demo/uint256\"\n)\n\n// Or sets z = x | y and returns z.\nfunc (z *Int) Or(x, y *Int) *Int {\n\tif x.neg == y.neg {\n\t\tif x.neg {\n\t\t\t// (-x) | (-y) == ^(x-1) | ^(y-1) == ^((x-1) \u0026 (y-1)) == -(((x-1) \u0026 (y-1)) + 1)\n\t\t\tx1 := new(uint256.Uint).Sub(x.abs, one)\n\t\t\ty1 := new(uint256.Uint).Sub(y.abs, one)\n\t\t\tz.abs = z.abs.Add(z.abs.And(x1, y1), one)\n\t\t\tz.neg = true // z cannot be zero if x and y are negative\n\t\t\treturn z\n\t\t}\n\n\t\t// x | y == x | y\n\t\tz.abs = z.abs.Or(x.abs, y.abs)\n\t\tz.neg = false\n\t\treturn z\n\t}\n\n\t// x.neg != y.neg\n\tif x.neg {\n\t\tx, y = y, x // | is symmetric\n\t}\n\n\t// x | (-y) == x | ^(y-1) == ^((y-1) \u0026^ x) == -(^((y-1) \u0026^ x) + 1)\n\ty1 := new(uint256.Uint).Sub(y.abs, one)\n\tz.abs = z.abs.Add(z.abs.AndNot(y1, x.abs), one)\n\tz.neg = true // z cannot be zero if one of x or y is negative\n\n\treturn z\n}\n\n// And sets z = x \u0026 y and returns z.\nfunc (z *Int) And(x, y *Int) *Int {\n\tif x.neg == y.neg {\n\t\tif x.neg {\n\t\t\t// (-x) \u0026 (-y) == ^(x-1) \u0026 ^(y-1) == ^((x-1) | (y-1)) == -(((x-1) | (y-1)) + 1)\n\t\t\tx1 := new(uint256.Uint).Sub(x.abs, one)\n\t\t\ty1 := new(uint256.Uint).Sub(y.abs, one)\n\t\t\tz.abs = z.abs.Add(z.abs.Or(x1, y1), one)\n\t\t\tz.neg = true // z cannot be zero if x and y are negative\n\t\t\treturn z\n\t\t}\n\n\t\t// x \u0026 y == x \u0026 y\n\t\tz.abs = z.abs.And(x.abs, y.abs)\n\t\tz.neg = false\n\t\treturn z\n\t}\n\n\t// x.neg != y.neg\n\t// REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1192-1202;drc=d57303e65f00b84b528ee682747dbe1fd3316d30\n\tif x.neg {\n\t\tx, y = y, x // \u0026 is symmetric\n\t}\n\n\t// x \u0026 (-y) == x \u0026 ^(y-1) == x \u0026^ (y-1)\n\ty1 := new(uint256.Uint).Sub(y.abs, uint256.One())\n\tz.abs = z.abs.AndNot(x.abs, y1)\n\tz.neg = false\n\treturn z\n}\n\n// Rsh sets z = x \u003e\u003e n and returns z.\n// OBS: Different from original implementation it was using math.Big\nfunc (z *Int) Rsh(x *Int, n uint) *Int {\n\tif !x.neg {\n\t\tz.abs.Rsh(x.abs, n)\n\t\tz.neg = x.neg\n\t\treturn z\n\t}\n\n\t// REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1118-1126;drc=d57303e65f00b84b528ee682747dbe1fd3316d30\n\tt := NewInt(0).Sub(FromUint256(x.abs), NewInt(1))\n\tt = t.Rsh(t, n)\n\n\t_tmp := t.Add(t, NewInt(1))\n\tz.abs = _tmp.Abs()\n\tz.neg = true\n\n\treturn z\n}\n\n// Lsh sets z = x \u003c\u003c n and returns z.\nfunc (z *Int) Lsh(x *Int, n uint) *Int {\n\tz.abs.Lsh(x.abs, n)\n\tz.neg = x.neg\n\treturn z\n}\n"},{"name":"bitwise_test.gno","body":"package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestOr(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx, y, want Int\n\t}{\n\t\t{\n\t\t\tname: \"all zeroes\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := New()\n\t\t\tgot.Or(\u0026tc.x, \u0026tc.y)\n\n\t\t\tif got.Neq(\u0026tc.want) {\n\t\t\t\tt.Errorf(\"Or(%v, %v) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAnd(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx, y, want Int\n\t}{\n\t\t{\n\t\t\tname: \"all zeroes\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Int{abs: \u0026uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false},\n\t\t\ty: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false},\n\t\t\twant: Int{abs: \u0026uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := New()\n\t\t\tgot.And(\u0026tc.x, \u0026tc.y)\n\n\t\t\tif got.Neq(\u0026tc.want) {\n\t\t\t\tt.Errorf(\"And(%v, %v) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\tn uint\n\t\twant string\n\t}{\n\t\t{\"1024\", 0, \"1024\"},\n\t\t{\"1024\", 1, \"512\"},\n\t\t{\"1024\", 2, \"256\"},\n\t\t{\"1024\", 10, \"1\"},\n\t\t{\"1024\", 11, \"0\"},\n\t\t{\"18446744073709551615\", 0, \"18446744073709551615\"},\n\t\t{\"18446744073709551615\", 1, \"9223372036854775807\"},\n\t\t{\"18446744073709551615\", 62, \"3\"},\n\t\t{\"18446744073709551615\", 63, \"1\"},\n\t\t{\"18446744073709551615\", 64, \"0\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 0, \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 1, \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 128, \"340282366920938463463374607431768211455\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 255, \"1\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", 256, \"0\"},\n\t\t{\"-1024\", 0, \"-1024\"},\n\t\t{\"-1024\", 1, \"-512\"},\n\t\t{\"-1024\", 2, \"-256\"},\n\t\t{\"-1024\", 10, \"-1\"},\n\t\t{\"-1024\", 10, \"-1\"},\n\t\t{\"-9223372036854775808\", 0, \"-9223372036854775808\"},\n\t\t{\"-9223372036854775808\", 1, \"-4611686018427387904\"},\n\t\t{\"-9223372036854775808\", 62, \"-2\"},\n\t\t{\"-9223372036854775808\", 63, \"-1\"},\n\t\t{\"-9223372036854775808\", 64, \"-1\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 0, \"-57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 1, \"-28948022309329048855892746252171976963317496166410141009864396001978282409984\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 253, \"-4\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 254, \"-2\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 255, \"-1\"},\n\t\t{\"-57896044618658097711785492504343953926634992332820282019728792003956564819968\", 256, \"-1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Rsh(x, tc.n)\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"Rsh(%s, %d) = %v, want %v\", tc.x, tc.n, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n\nfunc TestLsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\tn uint\n\t\twant string\n\t}{\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"2\"},\n\t\t{\"1\", 2, \"4\"},\n\t\t{\"2\", 0, \"2\"},\n\t\t{\"2\", 1, \"4\"},\n\t\t{\"2\", 2, \"8\"},\n\t\t{\"-2\", 0, \"-2\"},\n\t\t{\"-4\", 0, \"-4\"},\n\t\t{\"-8\", 0, \"-8\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Lsh(x, tc.n)\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"Lsh(%s, %d) = %v, want %v\", tc.x, tc.n, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n"},{"name":"cmp.gno","body":"package int256\n\n// Eq returns true if z == x\nfunc (z *Int) Eq(x *Int) bool {\n\treturn (z.neg == x.neg) \u0026\u0026 z.abs.Eq(x.abs)\n}\n\n// Neq returns true if z != x\nfunc (z *Int) Neq(x *Int) bool {\n\treturn !z.Eq(x)\n}\n\n// Cmp compares x and y and returns:\n//\n//\t-1 if x \u003c y\n//\t 0 if x == y\n//\t+1 if x \u003e y\nfunc (z *Int) Cmp(x *Int) (r int) {\n\t// x cmp y == x cmp y\n\t// x cmp (-y) == x\n\t// (-x) cmp y == y\n\t// (-x) cmp (-y) == -(x cmp y)\n\tswitch {\n\tcase z == x:\n\t\t// nothing to do\n\tcase z.neg == x.neg:\n\t\tr = z.abs.Cmp(x.abs)\n\t\tif z.neg {\n\t\t\tr = -r\n\t\t}\n\tcase z.neg:\n\t\tr = -1\n\tdefault:\n\t\tr = 1\n\t}\n\treturn\n}\n\n// IsZero returns true if z == 0\nfunc (z *Int) IsZero() bool {\n\treturn z.abs.IsZero()\n}\n\n// IsNeg returns true if z \u003c 0\nfunc (z *Int) IsNeg() bool {\n\treturn z.neg\n}\n\n// Lt returns true if z \u003c x\nfunc (z *Int) Lt(x *Int) bool {\n\tif z.neg {\n\t\tif x.neg {\n\t\t\treturn z.abs.Gt(x.abs)\n\t\t} else {\n\t\t\treturn true\n\t\t}\n\t} else {\n\t\tif x.neg {\n\t\t\treturn false\n\t\t} else {\n\t\t\treturn z.abs.Lt(x.abs)\n\t\t}\n\t}\n}\n\n// Gt returns true if z \u003e x\nfunc (z *Int) Gt(x *Int) bool {\n\tif z.neg {\n\t\tif x.neg {\n\t\t\treturn z.abs.Lt(x.abs)\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t} else {\n\t\tif x.neg {\n\t\t\treturn true\n\t\t} else {\n\t\t\treturn z.abs.Gt(x.abs)\n\t\t}\n\t}\n}\n\n// Clone creates a new Int identical to z\nfunc (z *Int) Clone() *Int {\n\treturn \u0026Int{z.abs.Clone(), z.neg}\n}\n"},{"name":"cmp_test.gno","body":"package int256\n\nimport \"testing\"\n\nfunc TestEq(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", true},\n\t\t{\"0\", \"1\", false},\n\t\t{\"1\", \"0\", false},\n\t\t{\"-1\", \"0\", false},\n\t\t{\"0\", \"-1\", false},\n\t\t{\"1\", \"1\", true},\n\t\t{\"-1\", \"-1\", true},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Eq(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Eq(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestNeq(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", true},\n\t\t{\"1\", \"0\", true},\n\t\t{\"-1\", \"0\", true},\n\t\t{\"0\", \"-1\", true},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Neq(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Neq(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestCmp(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant int\n\t}{\n\t\t{\"0\", \"0\", 0},\n\t\t{\"0\", \"1\", -1},\n\t\t{\"1\", \"0\", 1},\n\t\t{\"-1\", \"0\", -1},\n\t\t{\"0\", \"-1\", 1},\n\t\t{\"1\", \"1\", 0},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Cmp(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Cmp(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", true},\n\t\t{\"-0\", true},\n\t\t{\"1\", false},\n\t\t{\"-1\", false},\n\t\t{\"10\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.IsZero()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"IsZero(%s) = %v, want %v\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", false},\n\t\t{\"-0\", true}, // TODO: should this be false?\n\t\t{\"1\", false},\n\t\t{\"-1\", true},\n\t\t{\"10\", false},\n\t\t{\"-10\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.IsNeg()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"IsNeg(%s) = %v, want %v\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestLt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", true},\n\t\t{\"1\", \"0\", false},\n\t\t{\"-1\", \"0\", true},\n\t\t{\"0\", \"-1\", false},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Lt(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Lt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestGt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", false},\n\t\t{\"1\", \"0\", true},\n\t\t{\"-1\", \"0\", false},\n\t\t{\"0\", \"-1\", true},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Gt(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Gt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t}{\n\t\t{\"0\"},\n\t\t{\"-0\"},\n\t\t{\"1\"},\n\t\t{\"-1\"},\n\t\t{\"10\"},\n\t\t{\"-10\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty := x.Clone()\n\n\t\tif x.Cmp(y) != 0 {\n\t\t\tt.Errorf(\"Clone(%s) = %v, want %v\", tc.x, y, x)\n\t\t}\n\t}\n}\n"},{"name":"conversion.gno","body":"package int256\n\nimport \"gno.land/p/demo/uint256\"\n\n// SetInt64 sets z to x and returns z.\nfunc (z *Int) SetInt64(x int64) *Int {\n\tz.initiateAbs()\n\n\tneg := false\n\tif x \u003c 0 {\n\t\tneg = true\n\t\tx = -x\n\t}\n\tif z.abs == nil {\n\t\tpanic(\"abs is nil\")\n\t}\n\tz.abs = z.abs.SetUint64(uint64(x))\n\tz.neg = neg\n\treturn z\n}\n\n// SetUint64 sets z to x and returns z.\nfunc (z *Int) SetUint64(x uint64) *Int {\n\tz.initiateAbs()\n\n\tif z.abs == nil {\n\t\tpanic(\"abs is nil\")\n\t}\n\tz.abs = z.abs.SetUint64(x)\n\tz.neg = false\n\treturn z\n}\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Int) Uint64() uint64 {\n\treturn z.abs.Uint64()\n}\n\n// Int64 returns the lower 64-bits of z\nfunc (z *Int) Int64() int64 {\n\t_abs := z.abs.Clone()\n\n\tif z.neg {\n\t\treturn -int64(_abs.Uint64())\n\t}\n\treturn int64(_abs.Uint64())\n}\n\n// Neg sets z to -x and returns z.)\nfunc (z *Int) Neg(x *Int) *Int {\n\tz.abs.Set(x.abs)\n\tif z.abs.IsZero() {\n\t\tz.neg = false\n\t} else {\n\t\tz.neg = !x.neg\n\t}\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Int) Set(x *Int) *Int {\n\tz.abs.Set(x.abs)\n\tz.neg = x.neg\n\treturn z\n}\n\n// SetFromUint256 converts a uint256.Uint to Int and sets the value to z.\nfunc (z *Int) SetUint256(x *uint256.Uint) *Int {\n\tz.abs.Set(x)\n\tz.neg = false\n\treturn z\n}\n\n// OBS, differs from original mempooler int256\n// ToString returns the decimal representation of z.\nfunc (z *Int) ToString() string {\n\tif z == nil {\n\t\tpanic(\"int256: nil pointer to ToString()\")\n\t}\n\n\tt := z.abs.Dec()\n\tif z.neg {\n\t\treturn \"-\" + t\n\t}\n\n\treturn t\n}\n"},{"name":"conversion_test.gno","body":"package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestSetInt64(t *testing.T) {\n\ttests := []struct {\n\t\tx int64\n\t\twant string\n\t}{\n\t\t{0, \"0\"},\n\t\t{1, \"1\"},\n\t\t{-1, \"-1\"},\n\t\t{9223372036854775807, \"9223372036854775807\"},\n\t\t{-9223372036854775808, \"-9223372036854775808\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tvar z Int\n\t\tz.SetInt64(tc.x)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"SetInt64(%d) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSetUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx uint64\n\t\twant string\n\t}{\n\t\t{0, \"0\"},\n\t\t{1, \"1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tvar z Int\n\t\tz.SetUint64(tc.x)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"SetUint64(%d) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant uint64\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"9223372036854775807\", 9223372036854775807},\n\t\t{\"9223372036854775808\", 9223372036854775808},\n\t\t{\"18446744073709551615\", 18446744073709551615},\n\t\t{\"18446744073709551616\", 0},\n\t\t{\"18446744073709551617\", 1},\n\t\t{\"-1\", 1},\n\t\t{\"-18446744073709551615\", 18446744073709551615},\n\t\t{\"-18446744073709551616\", 0},\n\t\t{\"-18446744073709551617\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\n\t\tgot := z.Uint64()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Uint64(%s) = %d, want %d\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestInt64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant int64\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"9223372036854775807\", 9223372036854775807},\n\t\t{\"18446744073709551616\", 0},\n\t\t{\"18446744073709551617\", 1},\n\t\t{\"-1\", -1},\n\t\t{\"-9223372036854775808\", -9223372036854775808},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\n\t\tgot := z.Int64()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Uint64(%s) = %d, want %d\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"-1\"},\n\t\t{\"-1\", \"1\"},\n\t\t{\"9223372036854775807\", \"-9223372036854775807\"},\n\t\t{\"-18446744073709551615\", \"18446744073709551615\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\t\tz.Neg(z)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Neg(%s) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSet(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"-1\"},\n\t\t{\"9223372036854775807\", \"9223372036854775807\"},\n\t\t{\"-18446744073709551615\", \"-18446744073709551615\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\t\tz.Set(z)\n\n\t\tgot := z.ToString()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Set(%s) = %s, want %s\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSetUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"9223372036854775807\", \"9223372036854775807\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tgot := New()\n\n\t\tz := uint256.MustFromDecimal(tc.x)\n\t\tgot.SetUint256(z)\n\n\t\tif got.ToString() != tc.want {\n\t\t\tt.Errorf(\"SetUint256(%s) = %s, want %s\", tc.x, got.ToString(), tc.want)\n\t\t}\n\t}\n}\n\nfunc TestToString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tsetup func() *Int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Zero from subtraction\",\n\t\t\tsetup: func() *Int {\n\t\t\t\tminusThree := MustFromDecimal(\"-3\")\n\t\t\t\tthree := MustFromDecimal(\"3\")\n\t\t\t\treturn Zero().Add(minusThree, three)\n\t\t\t},\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"Zero from right shift\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn Zero().Rsh(One(), 1234)\n\t\t\t},\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"Positive number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"42\")\n\t\t\t},\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname: \"Negative number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"-42\")\n\t\t\t},\n\t\t\texpected: \"-42\",\n\t\t},\n\t\t{\n\t\t\tname: \"Large positive number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"115792089237316195423570985008687907853269984665640564039457584007913129639935\")\n\t\t\t},\n\t\t\texpected: \"115792089237316195423570985008687907853269984665640564039457584007913129639935\",\n\t\t},\n\t\t{\n\t\t\tname: \"Large negative number\",\n\t\t\tsetup: func() *Int {\n\t\t\t\treturn MustFromDecimal(\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\")\n\t\t\t},\n\t\t\texpected: \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := tt.setup()\n\t\t\tresult := z.ToString()\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"ToString() = %s, want %s\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"int256.gno","body":"// This package provides a 256-bit signed integer type, Int, and associated functions.\npackage int256\n\nimport (\n\t\"gno.land/p/demo/uint256\"\n)\n\nvar one = uint256.NewUint(1)\n\ntype Int struct {\n\tabs *uint256.Uint\n\tneg bool\n}\n\n// Zero returns a new Int set to 0.\nfunc Zero() *Int {\n\treturn NewInt(0)\n}\n\n// One returns a new Int set to 1.\nfunc One() *Int {\n\treturn NewInt(1)\n}\n\n// Sign returns:\n//\n//\t-1 if x \u003c 0\n//\t 0 if x == 0\n//\t+1 if x \u003e 0\nfunc (z *Int) Sign() int {\n\tz.initiateAbs()\n\n\tif z.abs.IsZero() {\n\t\treturn 0\n\t}\n\tif z.neg {\n\t\treturn -1\n\t}\n\treturn 1\n}\n\n// New returns a new Int set to 0.\nfunc New() *Int {\n\treturn \u0026Int{\n\t\tabs: new(uint256.Uint),\n\t}\n}\n\n// NewInt allocates and returns a new Int set to x.\nfunc NewInt(x int64) *Int {\n\treturn New().SetInt64(x)\n}\n\n// FromDecimal returns a new Int from a decimal string.\n// Returns a new Int and an error if the string is not a valid decimal.\nfunc FromDecimal(s string) (*Int, error) {\n\treturn new(Int).SetString(s)\n}\n\n// MustFromDecimal returns a new Int from a decimal string.\n// Panics if the string is not a valid decimal.\nfunc MustFromDecimal(s string) *Int {\n\tz, err := FromDecimal(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn z\n}\n\n// SetString sets s to the value of z and returns z and a boolean indicating success.\nfunc (z *Int) SetString(s string) (*Int, error) {\n\tneg := false\n\t// Remove max one leading +\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '+' {\n\t\tneg = false\n\t\ts = s[1:]\n\t}\n\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '-' {\n\t\tneg = true\n\t\ts = s[1:]\n\t}\n\tvar (\n\t\tabs *uint256.Uint\n\t\terr error\n\t)\n\tabs, err = uint256.FromDecimal(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn \u0026Int{\n\t\tabs,\n\t\tneg,\n\t}, nil\n}\n\n// FromUint256 is a convenience-constructor from uint256.Uint.\n// Returns a new Int and whether overflow occurred.\n// OBS: If u is `nil`, this method returns `nil, false`\nfunc FromUint256(x *uint256.Uint) *Int {\n\tif x == nil {\n\t\treturn nil\n\t}\n\tz := Zero()\n\n\tz.SetUint256(x)\n\treturn z\n}\n\n// OBS, differs from original mempooler int256\n// NilToZero sets z to 0 and return it if it's nil, otherwise it returns z\nfunc (z *Int) NilToZero() *Int {\n\tif z == nil {\n\t\treturn NewInt(0)\n\t}\n\treturn z\n}\n\n// initiateAbs sets default value for `z` or `z.abs` value if is nil\n// OBS: differs from mempooler int256. It checks not only `z.abs` but also `z`\nfunc (z *Int) initiateAbs() {\n\tif z == nil || z.abs == nil {\n\t\tz.abs = new(uint256.Uint)\n\t}\n}\n"},{"name":"int256_test.gno","body":"// ported from github.com/mempooler/int256\npackage int256\n\nimport \"testing\"\n\nfunc TestSign(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant int\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"-1\", -1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz := MustFromDecimal(tc.x)\n\t\tgot := z.Sign()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Sign(%s) = %d, want %d\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"json","path":"gno.land/p/demo/json","files":[{"name":"LICENSE","body":"# MIT License\n\nCopyright (c) 2019 Pyzhov Stepan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"},{"name":"README.md","body":"# JSON Parser\n\nThe JSON parser is a package that provides functionality for parsing and processing JSON strings. This package accepts JSON strings as byte slices.\n\nCurrently, gno does not [support the `reflect` package](https://docs.gno.land/concepts/effective-gno#reflection-is-never-clear), so it cannot retrieve type information at runtime. Therefore, it is designed to infer and handle type information when parsing JSON strings using a state machine approach.\n\nAfter passing through the state machine, JSON strings are represented as the `Node` type. The `Node` type represents nodes for JSON data, including various types such as `ObjectNode`, `ArrayNode`, `StringNode`, `NumberNode`, `BoolNode`, and `NullNode`.\n\nThis package provides methods for manipulating, searching, and extracting the Node type.\n\n## State Machine\n\nTo parse JSON strings, a [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine) approach is used. The state machine transitions to the next state based on the current state and the input character while parsing the JSON string. Through this method, type information can be inferred and processed without reflect, and the amount of parser code can be significantly reduced.\n\nThe image below shows the state transitions of the state machine according to the states and input characters.\n\n```mermaid\nstateDiagram-v2\n [*] --\u003e __: Start\n __ --\u003e ST: String\n __ --\u003e MI: Number\n __ --\u003e ZE: Zero\n __ --\u003e IN: Integer\n __ --\u003e T1: Boolean (true)\n __ --\u003e F1: Boolean (false)\n __ --\u003e N1: Null\n __ --\u003e ec: Empty Object End\n __ --\u003e cc: Object End\n __ --\u003e bc: Array End\n __ --\u003e co: Object Begin\n __ --\u003e bo: Array Begin\n __ --\u003e cm: Comma\n __ --\u003e cl: Colon\n __ --\u003e OK: Success/End\n ST --\u003e OK: String Complete\n MI --\u003e OK: Number Complete\n ZE --\u003e OK: Zero Complete\n IN --\u003e OK: Integer Complete\n T1 --\u003e OK: True Complete\n F1 --\u003e OK: False Complete\n N1 --\u003e OK: Null Complete\n ec --\u003e OK: Empty Object Complete\n cc --\u003e OK: Object Complete\n bc --\u003e OK: Array Complete\n co --\u003e OB: Inside Object\n bo --\u003e AR: Inside Array\n cm --\u003e KE: Expecting New Key\n cm --\u003e VA: Expecting New Value\n cl --\u003e VA: Expecting Value\n OB --\u003e ST: String in Object (Key)\n OB --\u003e ec: Empty Object\n OB --\u003e cc: End Object\n AR --\u003e ST: String in Array\n AR --\u003e bc: End Array\n KE --\u003e ST: String as Key\n VA --\u003e ST: String as Value\n VA --\u003e MI: Number as Value\n VA --\u003e T1: True as Value\n VA --\u003e F1: False as Value\n VA --\u003e N1: Null as Value\n OK --\u003e [*]: End\n```\n\n## Examples\n\nThis package provides parsing functionality along with encoding and decoding functionality. The following examples demonstrate how to use this package.\n\n### Decoding\n\nDecoding (or Unmarshaling) is the functionality that converts an input byte slice JSON string into a `Node` type.\n\nThe converted `Node` type allows you to modify the JSON data or search and extract data that meets specific conditions.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n node, err := json.Unmarshal([]byte(`{\"foo\": \"var\"}`))\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n ufmt.Sprintf(\"node: %v\", node)\n}\n```\n\n### Encoding\n\nEncoding (or Marshaling) is the functionality that converts JSON data represented as a Node type into a byte slice JSON string.\n\n\u003e ⚠️ Caution: Converting a large `Node` type into a JSON string may _impact performance_. or might be cause _unexpected behavior_.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n node := ObjectNode(\"\", map[string]*Node{\n \"foo\": StringNode(\"foo\", \"bar\"),\n \"baz\": NumberNode(\"baz\", 100500),\n \"qux\": NullNode(\"qux\"),\n })\n\n b, err := json.Marshal(node)\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n ufmt.Sprintf(\"json: %s\", string(b))\n}\n```\n\n### Searching\n\nOnce the JSON data converted into a `Node` type, you can **search** and **extract** data that satisfy specific conditions. For example, you can find data with a specific type or data with a specific key.\n\nTo use this functionality, you can use methods in the `GetXXX` prefixed methods. The `MustXXX` methods also provide the same functionality as the former methods, but they will **panic** if data doesn't satisfies the condition.\n\nHere is an example of finding data with a specific key. For more examples, please refer to the [node.gno](node.gno) file.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n root, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n value, err := root.GetKey(\"foo\")\n if err != nil {\n ufmt.Errorf(\"error occurred while getting key, %s\", err)\n }\n\n if value.MustBool() != true {\n ufmt.Errorf(\"value is not true\")\n }\n\n value, err = root.GetKey(\"bar\")\n if err != nil {\n t.Errorf(\"error occurred while getting key, %s\", err)\n }\n\n _, err = root.GetKey(\"baz\")\n if err == nil {\n t.Errorf(\"key baz is not exist. must be failed\")\n }\n}\n```\n\n## Contributing\n\nPlease submit any issues or pull requests for this package through the GitHub repository at [gnolang/gno](\u003chttps://github.com/gnolang/gno\u003e).\n"},{"name":"buffer.gno","body":"package json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype buffer struct {\n\tdata []byte\n\tlength int\n\tindex int\n\n\tlast States\n\tstate States\n\tclass Classes\n}\n\n// newBuffer creates a new buffer with the given data\nfunc newBuffer(data []byte) *buffer {\n\treturn \u0026buffer{\n\t\tdata: data,\n\t\tlength: len(data),\n\t\tlast: GO,\n\t\tstate: GO,\n\t}\n}\n\n// first retrieves the first non-whitespace (or other escaped) character in the buffer.\nfunc (b *buffer) first() (byte, error) {\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tc := b.data[b.index]\n\n\t\tif !(c == whiteSpace || c == carriageReturn || c == newLine || c == tab) {\n\t\t\treturn c, nil\n\t\t}\n\t}\n\n\treturn 0, io.EOF\n}\n\n// current returns the byte of the current index.\nfunc (b *buffer) current() (byte, error) {\n\tif b.index \u003e= b.length {\n\t\treturn 0, io.EOF\n\t}\n\n\treturn b.data[b.index], nil\n}\n\n// next moves to the next byte and returns it.\nfunc (b *buffer) next() (byte, error) {\n\tb.index++\n\treturn b.current()\n}\n\n// step just moves to the next position.\nfunc (b *buffer) step() error {\n\t_, err := b.next()\n\treturn err\n}\n\n// move moves the index by the given position.\nfunc (b *buffer) move(pos int) error {\n\tnewIndex := b.index + pos\n\n\tif newIndex \u003e b.length {\n\t\treturn io.EOF\n\t}\n\n\tb.index = newIndex\n\n\treturn nil\n}\n\n// slice returns the slice from the current index to the given position.\nfunc (b *buffer) slice(pos int) ([]byte, error) {\n\tend := b.index + pos\n\n\tif end \u003e b.length {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn b.data[b.index:end], nil\n}\n\n// sliceFromIndices returns a slice of the buffer's data starting from 'start' up to (but not including) 'stop'.\nfunc (b *buffer) sliceFromIndices(start, stop int) []byte {\n\tif start \u003e b.length {\n\t\tstart = b.length\n\t}\n\n\tif stop \u003e b.length {\n\t\tstop = b.length\n\t}\n\n\treturn b.data[start:stop]\n}\n\n// skip moves the index to skip the given byte.\nfunc (b *buffer) skip(bs byte) error {\n\tfor b.index \u003c b.length {\n\t\tif b.data[b.index] == bs \u0026\u0026 !b.backslash() {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn io.EOF\n}\n\n// skipAndReturnIndex moves the buffer index forward by one and returns the new index.\nfunc (b *buffer) skipAndReturnIndex() (int, error) {\n\terr := b.step()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn b.index, nil\n}\n\n// skipUntil moves the buffer index forward until it encounters a byte contained in the endTokens set.\nfunc (b *buffer) skipUntil(endTokens map[byte]bool) (int, error) {\n\tfor b.index \u003c b.length {\n\t\tcurrentByte, err := b.current()\n\t\tif err != nil {\n\t\t\treturn b.index, err\n\t\t}\n\n\t\t// Check if the current byte is in the set of end tokens.\n\t\tif _, exists := endTokens[currentByte]; exists {\n\t\t\treturn b.index, nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn b.index, io.EOF\n}\n\n// significantTokens is a map where the keys are the significant characters in a JSON path.\n// The values in the map are all true, which allows us to use the map as a set for quick lookups.\nvar significantTokens = [256]bool{\n\tdot: true, // access properties of an object\n\tdollarSign: true, // root object\n\tatSign: true, // current object\n\tbracketOpen: true, // start of an array index or filter expression\n\tbracketClose: true, // end of an array index or filter expression\n}\n\n// filterTokens stores the filter expression tokens.\nvar filterTokens = [256]bool{\n\taesterisk: true, // wildcard\n\tandSign: true,\n\torSign: true,\n}\n\n// skipToNextSignificantToken advances the buffer index to the next significant character.\n// Significant characters are defined based on the JSON path syntax.\nfunc (b *buffer) skipToNextSignificantToken() {\n\tfor b.index \u003c b.length {\n\t\tcurrent := b.data[b.index]\n\n\t\tif significantTokens[current] {\n\t\t\tbreak\n\t\t}\n\n\t\tb.index++\n\t}\n}\n\n// backslash checks to see if the number of backslashes before the current index is odd.\n//\n// This is used to check if the current character is escaped. However, unlike the \"unescape\" function,\n// \"backslash\" only serves to check the number of backslashes.\nfunc (b *buffer) backslash() bool {\n\tif b.index == 0 {\n\t\treturn false\n\t}\n\n\tcount := 0\n\tfor i := b.index - 1; ; i-- {\n\t\tif b.data[i] != backSlash {\n\t\t\tbreak\n\t\t}\n\n\t\tcount++\n\n\t\tif i == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn count%2 != 0\n}\n\n// numIndex holds a map of valid numeric characters\nvar numIndex = [256]bool{\n\t'0': true,\n\t'1': true,\n\t'2': true,\n\t'3': true,\n\t'4': true,\n\t'5': true,\n\t'6': true,\n\t'7': true,\n\t'8': true,\n\t'9': true,\n\t'.': true,\n\t'e': true,\n\t'E': true,\n}\n\n// pathToken checks if the current token is a valid JSON path token.\nfunc (b *buffer) pathToken() error {\n\tvar stack []byte\n\n\tinToken := false\n\tinNumber := false\n\tfirst := b.index\n\n\tfor b.index \u003c b.length {\n\t\tc := b.data[b.index]\n\n\t\tswitch {\n\t\tcase c == doubleQuote || c == singleQuote:\n\t\t\tinToken = true\n\t\t\tif err := b.step(); err != nil {\n\t\t\t\treturn errors.New(\"error stepping through buffer\")\n\t\t\t}\n\n\t\t\tif err := b.skip(c); err != nil {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\t\tif b.index \u003e= b.length {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\tcase c == bracketOpen || c == parenOpen:\n\t\t\tinToken = true\n\t\t\tstack = append(stack, c)\n\n\t\tcase c == bracketClose || c == parenClose:\n\t\t\tinToken = true\n\t\t\tif len(stack) == 0 || (c == bracketClose \u0026\u0026 stack[len(stack)-1] != bracketOpen) || (c == parenClose \u0026\u0026 stack[len(stack)-1] != parenOpen) {\n\t\t\t\treturn errUnmatchedParenthesis\n\t\t\t}\n\n\t\t\tstack = stack[:len(stack)-1]\n\n\t\tcase pathStateContainsValidPathToken(c):\n\t\t\tinToken = true\n\n\t\tcase c == plus || c == minus:\n\t\t\tif inNumber || (b.index \u003e 0 \u0026\u0026 numIndex[b.data[b.index-1]]) {\n\t\t\t\tinToken = true\n\t\t\t} else if !inToken \u0026\u0026 (b.index+1 \u003c b.length \u0026\u0026 numIndex[b.data[b.index+1]]) {\n\t\t\t\tinToken = true\n\t\t\t\tinNumber = true\n\t\t\t} else if !inToken {\n\t\t\t\treturn errInvalidToken\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif len(stack) != 0 || inToken {\n\t\t\t\tinToken = true\n\t\t\t} else {\n\t\t\t\tgoto end\n\t\t\t}\n\t\t}\n\n\t\tb.index++\n\t}\n\nend:\n\tif len(stack) != 0 {\n\t\treturn errUnmatchedParenthesis\n\t}\n\n\tif first == b.index {\n\t\treturn errors.New(\"no token found\")\n\t}\n\n\tif inNumber \u0026\u0026 !numIndex[b.data[b.index-1]] {\n\t\tinNumber = false\n\t}\n\n\treturn nil\n}\n\nfunc pathStateContainsValidPathToken(c byte) bool {\n\tif significantTokens[c] {\n\t\treturn true\n\t}\n\n\tif filterTokens[c] {\n\t\treturn true\n\t}\n\n\tif numIndex[c] {\n\t\treturn true\n\t}\n\n\tif 'A' \u003c= c \u0026\u0026 c \u003c= 'Z' || 'a' \u003c= c \u0026\u0026 c \u003c= 'z' {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (b *buffer) numeric(token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(doubleQuote)\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\tif token {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\treturn nil\n\t\t}\n\n\t\tif b.state \u003c MI || b.state \u003e E3 {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\tif b.last != ZE \u0026\u0026 b.last != IN \u0026\u0026 b.last != FR \u0026\u0026 b.last != E3 {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) getClasses(c byte) Classes {\n\tif b.data[b.index] \u003e= 128 {\n\t\treturn C_ETC\n\t}\n\n\tif c == singleQuote {\n\t\treturn QuoteAsciiClasses[b.data[b.index]]\n\t}\n\n\treturn AsciiClasses[b.data[b.index]]\n}\n\nfunc (b *buffer) getState() States {\n\tb.last = b.state\n\n\tb.class = b.getClasses(doubleQuote)\n\tif b.class == __ {\n\t\treturn __\n\t}\n\n\tb.state = StateTransitionTable[b.last][b.class]\n\n\treturn b.state\n}\n\n// string parses a string token from the buffer.\nfunc (b *buffer) string(search byte, token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(search)\n\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\tbreak\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) word(bs []byte) error {\n\tvar c byte\n\n\tmax := len(bs)\n\tindex := 0\n\n\tfor ; b.index \u003c b.length \u0026\u0026 index \u003c max; b.index++ {\n\t\tc = b.data[b.index]\n\n\t\tif c != bs[index] {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tindex++\n\t\tif index \u003e= max {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != max {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc numberKind2f64(value interface{}) (result float64, err error) {\n\tswitch typed := value.(type) {\n\tcase float64:\n\t\tresult = typed\n\tcase float32:\n\t\tresult = float64(typed)\n\tcase int:\n\t\tresult = float64(typed)\n\tcase int8:\n\t\tresult = float64(typed)\n\tcase int16:\n\t\tresult = float64(typed)\n\tcase int32:\n\t\tresult = float64(typed)\n\tcase int64:\n\t\tresult = float64(typed)\n\tcase uint:\n\t\tresult = float64(typed)\n\tcase uint8:\n\t\tresult = float64(typed)\n\tcase uint16:\n\t\tresult = float64(typed)\n\tcase uint32:\n\t\tresult = float64(typed)\n\tcase uint64:\n\t\tresult = float64(typed)\n\tdefault:\n\t\terr = ufmt.Errorf(\"invalid number type: %T\", value)\n\t}\n\n\treturn\n}\n"},{"name":"buffer_test.gno","body":"package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBufferCurrent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\texpected byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid current byte\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata: []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex: 1,\n\t\t\t},\n\t\t\texpected: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata: []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex: 4,\n\t\t\t},\n\t\t\texpected: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.current()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.current() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"buffer.current() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferStep(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid step\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.step()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.step() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferNext(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\twant byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid next byte\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twant: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twant: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.next()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.next() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"buffer.next() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSlice(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tpos int\n\t\twant []byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid slice -- 0 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 0,\n\t\t\twant: nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 1 character\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 1,\n\t\t\twant: []byte(\"t\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 2 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 2,\n\t\t\twant: []byte(\"es\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 3 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 3,\n\t\t\twant: []byte(\"tes\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 4 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 4,\n\t\t\twant: []byte(\"test\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\tpos: 2,\n\t\t\twant: nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.slice(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.slice() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(got) != string(tt.want) {\n\t\t\t\tt.Errorf(\"buffer.slice() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferMove(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tpos int\n\t\twantErr bool\n\t\twantIdx int\n\t}{\n\t\t{\n\t\t\tname: \"Valid move\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 2,\n\t\t\twantErr: false,\n\t\t\twantIdx: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Move beyond length\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 4,\n\t\t\twantErr: true,\n\t\t\twantIdx: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.move(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.move() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif tt.buffer.index != tt.wantIdx {\n\t\t\t\tt.Errorf(\"buffer.move() index = %v, want %v\", tt.buffer.index, tt.wantIdx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSkip(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tb byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Skip byte\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Skip to EOF\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb: 'x',\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.skip(tt.b)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.skip() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipToNextSignificantToken(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\texpected int\n\t}{\n\t\t{\"No significant chars\", []byte(\"abc\"), 3},\n\t\t{\"One significant char at start\", []byte(\".abc\"), 0},\n\t\t{\"Significant char in middle\", []byte(\"ab.c\"), 2},\n\t\t{\"Multiple significant chars\", []byte(\"a$.c\"), 1},\n\t\t{\"Significant char at end\", []byte(\"abc$\"), 3},\n\t\t{\"Only significant chars\", []byte(\"$.\"), 0},\n\t\t{\"Empty string\", []byte(\"\"), 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.input)\n\t\t\tb.skipToNextSignificantToken()\n\t\t\tif b.index != tt.expected {\n\t\t\t\tt.Errorf(\"after skipToNextSignificantToken(), got index = %v, want %v\", b.index, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuffer(s string) *buffer {\n\treturn newBuffer([]byte(s))\n}\n\nfunc TestSkipAndReturnIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"StartOfString\", \"\", 0},\n\t\t{\"MiddleOfString\", \"abcdef\", 1},\n\t\t{\"EndOfString\", \"abc\", 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipAndReturnIndex()\n\t\t\tif err != nil \u0026\u0026 tt.input != \"\" { // Expect no error unless input is empty\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipUntil(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\ttokens map[byte]bool\n\t\texpected int\n\t}{\n\t\t{\"SkipToToken\", \"abcdefg\", map[byte]bool{'c': true}, 2},\n\t\t{\"SkipToEnd\", \"abcdefg\", map[byte]bool{'h': true}, 7},\n\t\t{\"SkipNone\", \"abcdefg\", map[byte]bool{'a': true}, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipUntil(tt.tokens)\n\t\t\tif err != nil \u0026\u0026 got != len(tt.input) { // Expect error only if reached end without finding token\n\t\t\t\tt.Errorf(\"skipUntil() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipUntil() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSliceFromIndices(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\tstart int\n\t\tend int\n\t\texpected string\n\t}{\n\t\t{\"FullString\", \"abcdefg\", 0, 7, \"abcdefg\"},\n\t\t{\"Substring\", \"abcdefg\", 2, 5, \"cde\"},\n\t\t{\"OutOfBounds\", \"abcdefg\", 5, 10, \"fg\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot := buf.sliceFromIndices(tt.start, tt.end)\n\t\t\tif string(got) != tt.expected {\n\t\t\t\tt.Errorf(\"sliceFromIndices() = %v, want %v\", string(got), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferToken(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpath string\n\t\tindex int\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Simple valid path\",\n\t\t\tpath: \"@.length\",\n\t\t\tindex: 8,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with array expr\",\n\t\t\tpath: \"@['foo'].0.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with array expr and simple fomula\",\n\t\t\tpath: \"@['foo'].[(@.length - 1)].*\",\n\t\t\tindex: 27,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with filter expr\",\n\t\t\tpath: \"@['foo'].[?(@.bar == 1 \u0026 @.baz \u003c @.length)].*\",\n\t\t\tindex: 45,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"addition of foo and bar\",\n\t\t\tpath: \"@.foo+@.bar\",\n\t\t\tindex: 11,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"logical AND of foo and bar\",\n\t\t\tpath: \"@.foo \u0026\u0026 @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"logical OR of foo and bar\",\n\t\t\tpath: \"@.foo || @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"accessing third element of foo\",\n\t\t\tpath: \"@.foo,3\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"accessing last element of array\",\n\t\t\tpath: \"@.length-1\",\n\t\t\tindex: 10,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"number 1\",\n\t\t\tpath: \"1\",\n\t\t\tindex: 1,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float\",\n\t\t\tpath: \"3.1e4\",\n\t\t\tindex: 5,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float with minus\",\n\t\t\tpath: \"3.1e-4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float with plus\",\n\t\t\tpath: \"3.1e+4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative number\",\n\t\t\tpath: \"-12345\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float\",\n\t\t\tpath: \"-3.1e4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float with minus\",\n\t\t\tpath: \"-3.1e-4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float with plus\",\n\t\t\tpath: \"-3.1e+4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string number\",\n\t\t\tpath: \"'12345'\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string with backslash\",\n\t\t\tpath: \"'foo \\\\'bar '\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string with inner double quotes\",\n\t\t\tpath: \"'foo \\\"bar \\\"'\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis 1\",\n\t\t\tpath: \"(@abc)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis 2\",\n\t\t\tpath: \"[()]\",\n\t\t\tindex: 4,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch\",\n\t\t\tpath: \"[(])\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch 2\",\n\t\t\tpath: \"(\",\n\t\t\tindex: 1,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch 3\",\n\t\t\tpath: \"())]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"bracket mismatch\",\n\t\t\tpath: \"[()\",\n\t\t\tindex: 3,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"bracket mismatch 2\",\n\t\t\tpath: \"()]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"path does not close bracket\",\n\t\t\tpath: \"@.foo[)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := newBuffer([]byte(tt.path))\n\n\t\t\terr := buf.pathToken()\n\t\t\tif tt.isErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err == nil \u0026\u0026 tt.isErr {\n\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t}\n\n\t\t\tif buf.index != tt.index {\n\t\t\t\tt.Errorf(\"Expected final index %d, got %d (token: `%s`) for path `%s`\", tt.index, buf.index, string(buf.data[buf.index]), tt.path)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferFirst(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\texpected byte\n\t}{\n\t\t{\n\t\t\tname: \"Valid first byte\",\n\t\t\tdata: []byte(\"test\"),\n\t\t\texpected: 't',\n\t\t},\n\t\t{\n\t\t\tname: \"Empty buffer\",\n\t\t\tdata: []byte(\"\"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Whitespace buffer\",\n\t\t\tdata: []byte(\" \"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace in middle\",\n\t\t\tdata: []byte(\"hello world\"),\n\t\t\texpected: 'h',\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.data)\n\n\t\t\tgot, err := b.first()\n\t\t\tif err != nil \u0026\u0026 tt.expected != 0 {\n\t\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Expected first byte to be %q, got %q\", tt.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"builder.gno","body":"package json\n\ntype NodeBuilder struct {\n\tnode *Node\n}\n\nfunc Builder() *NodeBuilder {\n\treturn \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n}\n\nfunc (b *NodeBuilder) WriteString(key, value string) *NodeBuilder {\n\tb.node.AppendObject(key, StringNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNumber(key string, value float64) *NodeBuilder {\n\tb.node.AppendObject(key, NumberNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteBool(key string, value bool) *NodeBuilder {\n\tb.node.AppendObject(key, BoolNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNull(key string) *NodeBuilder {\n\tb.node.AppendObject(key, NullNode(\"\"))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteObject(key string, fn func(*NodeBuilder)) *NodeBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tb.node.AppendObject(key, nestedBuilder.node)\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteArray(key string, fn func(*ArrayBuilder)) *NodeBuilder {\n\tarrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(arrayBuilder)\n\tb.node.AppendObject(key, ArrayNode(\"\", arrayBuilder.nodes))\n\treturn b\n}\n\nfunc (b *NodeBuilder) Node() *Node {\n\treturn b.node\n}\n\ntype ArrayBuilder struct {\n\tnodes []*Node\n}\n\nfunc (ab *ArrayBuilder) WriteString(value string) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, StringNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNumber(value float64) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NumberNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteInt(value int) *ArrayBuilder {\n\treturn ab.WriteNumber(float64(value))\n}\n\nfunc (ab *ArrayBuilder) WriteBool(value bool) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, BoolNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNull() *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NullNode(\"\"))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteObject(fn func(*NodeBuilder)) *ArrayBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tab.nodes = append(ab.nodes, nestedBuilder.node)\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteArray(fn func(*ArrayBuilder)) *ArrayBuilder {\n\tnestedArrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(nestedArrayBuilder)\n\tab.nodes = append(ab.nodes, ArrayNode(\"\", nestedArrayBuilder.nodes))\n\treturn ab\n}\n"},{"name":"builder_test.gno","body":"package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNodeBuilder(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuild func() *Node\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"plain object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteNumber(\"age\", 30).\n\t\t\t\t\tWriteBool(\"is_student\", false).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"age\":30,\"is_student\":false}`,\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteObject(\"address\", func(b *NodeBuilder) {\n\t\t\t\t\t\tb.WriteString(\"city\", \"New York\").\n\t\t\t\t\t\t\tWriteNumber(\"zipcode\", 10001)\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"address\":{\"city\":\"New York\",\"zipcode\":10001}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"null node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().WriteNull(\"foo\").Node()\n\t\t\t},\n\t\t\texpected: `{\"foo\":null}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"items\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteString(\"item2\").\n\t\t\t\t\t\t\tWriteString(\"item3\")\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"items\":[\"item1\",\"item2\",\"item3\"]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with objects\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"users\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\tb.WriteString(\"name\", \"Bob\").\n\t\t\t\t\t\t\t\tWriteNumber(\"age\", 25)\n\t\t\t\t\t\t}).\n\t\t\t\t\t\t\tWriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\t\tb.WriteString(\"name\", \"Carol\").\n\t\t\t\t\t\t\t\t\tWriteNumber(\"age\", 27)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"users\":[{\"name\":\"Bob\",\"age\":25},{\"name\":\"Carol\",\"age\":27}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with various types\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"values\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteNumber(123).\n\t\t\t\t\t\t\tWriteBool(true).\n\t\t\t\t\t\t\tWriteNull()\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"values\":[\"item1\",123,true,null]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode := tt.build()\n\t\t\tvalue, err := Marshal(node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t}\n\t\t\tif string(value) != tt.expected {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tt.expected, string(value))\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"decode.gno","body":"// ref: https://github.com/spyzhov/ajson/blob/master/decode.go\n\npackage json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// This limits the max nesting depth to prevent stack overflow.\n// This is permitted by https://tools.ietf.org/html/rfc7159#section-9\nconst maxNestingDepth = 10000\n\n// Unmarshal parses the JSON-encoded data and returns a Node.\n// The data must be a valid JSON-encoded value.\n//\n// Usage:\n//\n//\tnode, err := json.Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err != nil {\n//\t\tufmt.Println(err)\n//\t}\n//\tprintln(node) // {\"key\": \"value\"}\nfunc Unmarshal(data []byte) (*Node, error) {\n\tbuf := newBuffer(data)\n\n\tvar (\n\t\tstate States\n\t\tkey *string\n\t\tcurrent *Node\n\t\tnesting int\n\t\tuseKey = func() **string {\n\t\t\ttmp := cptrs(key)\n\t\t\tkey = nil\n\t\t\treturn \u0026tmp\n\t\t}\n\t\terr error\n\t)\n\n\tif _, err = buf.first(); err != nil {\n\t\treturn nil, io.EOF\n\t}\n\n\tfor {\n\t\tstate = buf.getState()\n\t\tif state == __ {\n\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t}\n\n\t\t// region state machine\n\t\tif state \u003e= GO {\n\t\t\tswitch buf.state {\n\t\t\tcase ST: // string\n\t\t\t\tif current != nil \u0026\u0026 current.IsObject() \u0026\u0026 key == nil {\n\t\t\t\t\t// key detected\n\t\t\t\t\tif key, err = getString(buf); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tbuf.state = CO\n\t\t\t\t} else {\n\t\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, String, nesting, useKey())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\terr = buf.string(doubleQuote, false)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\t\t\t\t\tbuf.state = OK\n\t\t\t\t}\n\n\t\t\tcase MI, ZE, IN: // number\n\t\t\t\tcurrent, err = processNumericNode(current, buf, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase T1, F1: // boolean\n\t\t\t\tliteral := falseLiteral\n\t\t\t\tif buf.state == T1 {\n\t\t\t\t\tliteral = trueLiteral\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Boolean, literal, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase N1: // null\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Null, nullLiteral, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// region action\n\t\t\tswitch state {\n\t\t\tcase ec, cc: // \u003cempty\u003e }\n\t\t\t\tif key != nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Object)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase bc: // ]\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Array)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase co, bo: // { [\n\t\t\t\tvalTyp, bState := Object, OB\n\t\t\t\tif state == bo {\n\t\t\t\t\tvalTyp, bState = Array, AR\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, valTyp, nesting, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.state = bState\n\n\t\t\tcase cm: // ,\n\t\t\t\tif current == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif !current.isContainer() {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif current.IsObject() {\n\t\t\t\t\tbuf.state = KE // key expected\n\t\t\t\t} else {\n\t\t\t\t\tbuf.state = VA // value expected\n\t\t\t\t}\n\n\t\t\tcase cl: // :\n\t\t\t\tif current == nil || !current.IsObject() || key == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tbuf.state = VA\n\n\t\t\tdefault:\n\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t}\n\t\t}\n\n\t\tif buf.step() != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif _, err = buf.first(); err != nil {\n\t\t\terr = nil\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif current == nil || buf.state != OK {\n\t\treturn nil, io.EOF\n\t}\n\n\troot := current.root()\n\tif !root.ready() {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn root, err\n}\n\n// UnmarshalSafe parses the JSON-encoded data and returns a Node.\nfunc UnmarshalSafe(data []byte) (*Node, error) {\n\tvar safe []byte\n\tsafe = append(safe, data...)\n\treturn Unmarshal(safe)\n}\n\n// processNumericNode creates a new node, processes a numeric value,\n// sets the node's borders, and moves to the previous node.\nfunc processNumericNode(current *Node, buf *buffer, key **string) (*Node, error) {\n\tvar err error\n\tcurrent, err = createNode(current, buf, Number, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = buf.numeric(false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcurrent.borders[1] = buf.index\n\tif current.prev != nil {\n\t\tcurrent = current.prev\n\t}\n\n\tbuf.index -= 1\n\tbuf.state = OK\n\n\treturn current, nil\n}\n\n// processLiteralNode creates a new node, processes a literal value,\n// sets the node's borders, and moves to the previous node.\nfunc processLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteralValue []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tcurrent, nesting, err = createLiteralNode(current, buf, literalType, literalValue, useKey, nesting)\n\tif err != nil {\n\t\treturn nil, nesting, err\n\t}\n\treturn current, nesting, nil\n}\n\n// isValidContainerType checks if the current node is a valid container (object or array).\n// The container must satisfy the following conditions:\n// 1. The current node must not be nil.\n// 2. The current node must be an object or array.\n// 3. The current node must not be ready.\nfunc isValidContainerType(current *Node, nodeType ValueType) bool {\n\tswitch nodeType {\n\tcase Object:\n\t\treturn current != nil \u0026\u0026 current.IsObject() \u0026\u0026 !current.ready()\n\tcase Array:\n\t\treturn current != nil \u0026\u0026 current.IsArray() \u0026\u0026 !current.ready()\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// getString extracts a string from the buffer and advances the buffer index past the string.\nfunc getString(b *buffer) (*string, error) {\n\tstart := b.index\n\tif err := b.string(doubleQuote, false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalue, ok := Unquote(b.data[start:b.index+1], doubleQuote)\n\tif !ok {\n\t\treturn nil, unexpectedTokenError(b.data, start)\n\t}\n\n\treturn \u0026value, nil\n}\n\n// createNode creates a new node and sets the key if it is not nil.\nfunc createNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tkey **string,\n) (*Node, error) {\n\tvar err error\n\tcurrent, err = NewNode(current, buf, nodeType, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn current, nil\n}\n\n// createNestedNode creates a new nested node (array or object) and sets the key if it is not nil.\nfunc createNestedNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tnesting int,\n\tkey **string,\n) (*Node, int, error) {\n\tvar err error\n\tif nesting, err = checkNestingDepth(nesting); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\tif current, err = createNode(current, buf, nodeType, key); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\treturn current, nesting, nil\n}\n\n// createLiteralNode creates a new literal node and sets the key if it is not nil.\n// The literal is a byte slice that represents a boolean or null value.\nfunc createLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteral []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tif current, err = createNode(current, buf, literalType, useKey); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tif err = buf.word(literal); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, false)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// updateNode updates the current node and returns the previous node.\nfunc updateNode(\n\tcurrent *Node, buf *buffer, nesting int, decreaseLevel bool,\n) (*Node, int) {\n\tcurrent.borders[1] = buf.index + 1\n\n\tprev := current.prev\n\tif prev == nil {\n\t\treturn current, nesting\n\t}\n\n\tcurrent = prev\n\tif decreaseLevel {\n\t\tnesting--\n\t}\n\n\treturn current, nesting\n}\n\n// updateNodeAndSetBufferState updates the current node and sets the buffer state to OK.\nfunc updateNodeAndSetBufferState(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnesting int,\n\ttyp ValueType,\n) (*Node, int, error) {\n\tif !isValidContainerType(current, typ) {\n\t\treturn nil, nesting, unexpectedTokenError(buf.data, buf.index)\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// checkNestingDepth checks if the nesting depth is within the maximum allowed depth.\nfunc checkNestingDepth(nesting int) (int, error) {\n\tif nesting \u003e= maxNestingDepth {\n\t\treturn nesting, errors.New(\"maximum nesting depth exceeded\")\n\t}\n\n\treturn nesting + 1, nil\n}\n\nfunc unexpectedTokenError(data []byte, index int) error {\n\treturn ufmt.Errorf(\"unexpected token at index %d. data %b\", index, data)\n}\n"},{"name":"decode_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\ntype testNode struct {\n\tname string\n\tinput []byte\n\tvalue []byte\n\t_type ValueType\n}\n\nfunc simpleValid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.input, err.Error())\n\t} else if root == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t} else if root.nodeType != test._type {\n\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t} else if !bytes.Equal(root.source(), test.value) {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t}\n}\n\nfunc simpleInvalid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): error expected, got '%s'\", test.name, root.source())\n\t} else if root != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is not nil\", test.name)\n\t}\n}\n\nfunc simpleCorrupted(name string) *testNode {\n\treturn \u0026testNode{name: name, input: []byte(name)}\n}\n\nfunc TestUnmarshal_StringSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"blank\", input: []byte(\"\\\"\\\"\"), _type: String, value: []byte(\"\\\"\\\"\")},\n\t\t{name: \"char\", input: []byte(\"\\\"c\\\"\"), _type: String, value: []byte(\"\\\"c\\\"\")},\n\t\t{name: \"word\", input: []byte(\"\\\"cat\\\"\"), _type: String, value: []byte(\"\\\"cat\\\"\")},\n\t\t{name: \"spaces\", input: []byte(\" \\\"good cat or dog\\\"\\r\\n \"), _type: String, value: []byte(\"\\\"good cat or dog\\\"\")},\n\t\t{name: \"backslash\", input: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\")},\n\t\t{name: \"backslash 2\", input: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NumericSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"1\", input: []byte(\"1\"), _type: Number, value: []byte(\"1\")},\n\t\t{name: \"-1\", input: []byte(\"-1\"), _type: Number, value: []byte(\"-1\")},\n\n\t\t{name: \"1234567890\", input: []byte(\"1234567890\"), _type: Number, value: []byte(\"1234567890\")},\n\t\t{name: \"-123\", input: []byte(\"-123\"), _type: Number, value: []byte(\"-123\")},\n\n\t\t{name: \"123.456\", input: []byte(\"123.456\"), _type: Number, value: []byte(\"123.456\")},\n\t\t{name: \"-123.456\", input: []byte(\"-123.456\"), _type: Number, value: []byte(\"-123.456\")},\n\n\t\t{name: \"1e3\", input: []byte(\"1e3\"), _type: Number, value: []byte(\"1e3\")},\n\t\t{name: \"1e+3\", input: []byte(\"1e+3\"), _type: Number, value: []byte(\"1e+3\")},\n\t\t{name: \"1e-3\", input: []byte(\"1e-3\"), _type: Number, value: []byte(\"1e-3\")},\n\t\t{name: \"-1e3\", input: []byte(\"-1e3\"), _type: Number, value: []byte(\"-1e3\")},\n\t\t{name: \"-1e-3\", input: []byte(\"-1e-3\"), _type: Number, value: []byte(\"-1e-3\")},\n\n\t\t{name: \"1.123e3456\", input: []byte(\"1.123e3456\"), _type: Number, value: []byte(\"1.123e3456\")},\n\t\t{name: \"1.123e-3456\", input: []byte(\"1.123e-3456\"), _type: Number, value: []byte(\"1.123e-3456\")},\n\t\t{name: \"-1.123e3456\", input: []byte(\"-1.123e3456\"), _type: Number, value: []byte(\"-1.123e3456\")},\n\t\t{name: \"-1.123e-3456\", input: []byte(\"-1.123e-3456\"), _type: Number, value: []byte(\"-1.123e-3456\")},\n\n\t\t{name: \"1E3\", input: []byte(\"1E3\"), _type: Number, value: []byte(\"1E3\")},\n\t\t{name: \"1E-3\", input: []byte(\"1E-3\"), _type: Number, value: []byte(\"1E-3\")},\n\t\t{name: \"-1E3\", input: []byte(\"-1E3\"), _type: Number, value: []byte(\"-1E3\")},\n\t\t{name: \"-1E-3\", input: []byte(\"-1E-3\"), _type: Number, value: []byte(\"-1E-3\")},\n\n\t\t{name: \"1.123E3456\", input: []byte(\"1.123E3456\"), _type: Number, value: []byte(\"1.123E3456\")},\n\t\t{name: \"1.123E-3456\", input: []byte(\"1.123E-3456\"), _type: Number, value: []byte(\"1.123E-3456\")},\n\t\t{name: \"-1.123E3456\", input: []byte(\"-1.123E3456\"), _type: Number, value: []byte(\"-1.123E3456\")},\n\t\t{name: \"-1.123E-3456\", input: []byte(\"-1.123E-3456\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\n\t\t{name: \"-1.123E-3456 with spaces\", input: []byte(\" \\r -1.123E-3456 \\t\\n\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(test.input)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.name, err.Error())\n\t\t\t} else if root == nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t\t\t} else if root.nodeType != test._type {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t\t\t} else if !bytes.Equal(root.source(), test.value) {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_StringSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"white NL\", input: []byte(\"\\\"foo\\nbar\\\"\")},\n\t\t{name: \"white R\", input: []byte(\"\\\"foo\\rbar\\\"\")},\n\t\t{name: \"white Tab\", input: []byte(\"\\\"foo\\tbar\\\"\")},\n\t\t{name: \"wrong quotes\", input: []byte(\"'cat'\")},\n\t\t{name: \"double string\", input: []byte(\"\\\"Hello\\\" \\\"World\\\"\")},\n\t\t{name: \"quotes in quotes\", input: []byte(\"\\\"good \\\"cat\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"{}\", input: []byte(\"{}\"), _type: Object, value: []byte(\"{}\")},\n\t\t{name: `{ \\r\\n }`, input: []byte(\"{ \\r\\n }\"), _type: Object, value: []byte(\"{ \\r\\n }\")},\n\t\t{name: `{\"key\":1}`, input: []byte(`{\"key\":1}`), _type: Object, value: []byte(`{\"key\":1}`)},\n\t\t{name: `{\"key\":true}`, input: []byte(`{\"key\":true}`), _type: Object, value: []byte(`{\"key\":true}`)},\n\t\t{name: `{\"key\":\"value\"}`, input: []byte(`{\"key\":\"value\"}`), _type: Object, value: []byte(`{\"key\":\"value\"}`)},\n\t\t{name: `{\"foo\":\"bar\",\"baz\":\"foo\"}`, input: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`), _type: Object, value: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`)},\n\t\t{name: \"spaces\", input: []byte(` { \"foo\" : \"bar\" , \"baz\" : \"foo\" } `), _type: Object, value: []byte(`{ \"foo\" : \"bar\" , \"baz\" : \"foo\" }`)},\n\t\t{name: \"nested\", input: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`), _type: Object, value: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`)},\n\t\t{name: \"array\", input: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`), _type: Object, value: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`)},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"{{{\\\"key\\\": \\\"foo\\\"{{{{\"),\n\t\tsimpleCorrupted(\"}\"),\n\t\tsimpleCorrupted(\"{ }}}}}}}\"),\n\t\tsimpleCorrupted(\" }\"),\n\t\tsimpleCorrupted(\"{,}\"),\n\t\tsimpleCorrupted(\"{:}\"),\n\t\tsimpleCorrupted(\"{100000}\"),\n\t\tsimpleCorrupted(\"{1:1}\"),\n\t\tsimpleCorrupted(\"{'1:2,3:4'}\"),\n\t\tsimpleCorrupted(`{\"d\"}`),\n\t\tsimpleCorrupted(`{\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":}`),\n\t\tsimpleCorrupted(`{:\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":bar}`),\n\t\tsimpleCorrupted(`{\"foo\":\"bar\",}`),\n\t\tsimpleCorrupted(`{}{}`),\n\t\tsimpleCorrupted(`{},{}`),\n\t\tsimpleCorrupted(`{[},{]}`),\n\t\tsimpleCorrupted(`{[,]}`),\n\t\tsimpleCorrupted(`{[]}`),\n\t\tsimpleCorrupted(`{}1`),\n\t\tsimpleCorrupted(`1{}`),\n\t\tsimpleCorrupted(`{\"x\"::1}`),\n\t\tsimpleCorrupted(`{null:null}`),\n\t\tsimpleCorrupted(`{\"foo:\"bar\"}`),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NullSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"nul\", input: []byte(\"nul\")},\n\t\t{name: \"nil\", input: []byte(\"nil\")},\n\t\t{name: \"nill\", input: []byte(\"nill\")},\n\t\t{name: \"NILL\", input: []byte(\"NILL\")},\n\t\t{name: \"Null\", input: []byte(\"Null\")},\n\t\t{name: \"NULL\", input: []byte(\"NULL\")},\n\t\t{name: \"spaces\", input: []byte(\"Nu ll\")},\n\t\t{name: \"null1\", input: []byte(\"null1\")},\n\t\t{name: \"double\", input: []byte(\"null null\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"lower true\", input: []byte(\"true\"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"lower false\", input: []byte(\"false\"), _type: Boolean, value: []byte(\"false\")},\n\t\t{name: \"spaces true\", input: []byte(\" true\\r\\n \"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"spaces false\", input: []byte(\" false\\r\\n \"), _type: Boolean, value: []byte(\"false\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"tru\"),\n\t\tsimpleCorrupted(\"fals\"),\n\t\tsimpleCorrupted(\"tre\"),\n\t\tsimpleCorrupted(\"fal se\"),\n\t\tsimpleCorrupted(\"true false\"),\n\t\tsimpleCorrupted(\"True\"),\n\t\tsimpleCorrupted(\"TRUE\"),\n\t\tsimpleCorrupted(\"False\"),\n\t\tsimpleCorrupted(\"FALSE\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"[]\", input: []byte(\"[]\"), _type: Array, value: []byte(\"[]\")},\n\t\t{name: \"[1]\", input: []byte(\"[1]\"), _type: Array, value: []byte(\"[1]\")},\n\t\t{name: \"[1,2,3]\", input: []byte(\"[1,2,3]\"), _type: Array, value: []byte(\"[1,2,3]\")},\n\t\t{name: \"[1, 2, 3]\", input: []byte(\"[1, 2, 3]\"), _type: Array, value: []byte(\"[1, 2, 3]\")},\n\t\t{name: \"[1,[2],3]\", input: []byte(\"[1,[2],3]\"), _type: Array, value: []byte(\"[1,[2],3]\")},\n\t\t{name: \"[[],[],[]]\", input: []byte(\"[[],[],[]]\"), _type: Array, value: []byte(\"[[],[],[]]\")},\n\t\t{name: \"[[[[[]]]]]\", input: []byte(\"[[[[[]]]]]\"), _type: Array, value: []byte(\"[[[[[]]]]]\")},\n\t\t{name: \"[true,null,1,\\\"foo\\\",[]]\", input: []byte(\"[true,null,1,\\\"foo\\\",[]]\"), _type: Array, value: []byte(\"[true,null,1,\\\"foo\\\",[]]\")},\n\t\t{name: \"spaces\", input: []byte(\"\\n\\r [\\n1\\n ]\\r\\n\"), _type: Array, value: []byte(\"[\\n1\\n ]\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"[,]\"),\n\t\tsimpleCorrupted(\"[]\\\\\"),\n\t\tsimpleCorrupted(\"[1,]\"),\n\t\tsimpleCorrupted(\"[[]\"),\n\t\tsimpleCorrupted(\"[]]\"),\n\t\tsimpleCorrupted(\"1[]\"),\n\t\tsimpleCorrupted(\"[]1\"),\n\t\tsimpleCorrupted(\"[[]1]\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\n// Examples from https://json.org/example.html\nfunc TestUnmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tvalue string\n\t}{\n\t\t{\n\t\t\tname: \"glossary\",\n\t\t\tvalue: `{\n\t\t\t\t\"glossary\": {\n\t\t\t\t\t\"title\": \"example glossary\",\n\t\t\t\t\t\"GlossDiv\": {\n\t\t\t\t\t\t\"title\": \"S\",\n\t\t\t\t\t\t\"GlossList\": {\n\t\t\t\t\t\t\t\"GlossEntry\": {\n\t\t\t\t\t\t\t\t\"ID\": \"SGML\",\n\t\t\t\t\t\t\t\t\"SortAs\": \"SGML\",\n\t\t\t\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\n\t\t\t\t\t\t\t\t\"Acronym\": \"SGML\",\n\t\t\t\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\n\t\t\t\t\t\t\t\t\"GlossDef\": {\n\t\t\t\t\t\t\t\t\t\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n\t\t\t\t\t\t\t\t\t\"GlossSeeAlso\": [\"GML\", \"XML\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"GlossSee\": \"markup\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"menu\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"id\": \"file\",\n\t\t\t\t\"value\": \"File\",\n\t\t\t\t\"popup\": {\n\t\t\t\t \"menuitem\": [\n\t\t\t\t\t{\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n\t\t\t\t\t{\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n\t\t\t\t\t{\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n\t\t\t\t ]\n\t\t\t\t}\n\t\t\t}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"widget\",\n\t\t\tvalue: `{\"widget\": {\n\t\t\t\t\"debug\": \"on\",\n\t\t\t\t\"window\": {\n\t\t\t\t\t\"title\": \"Sample Konfabulator Widget\",\n\t\t\t\t\t\"name\": \"main_window\",\n\t\t\t\t\t\"width\": 500,\n\t\t\t\t\t\"height\": 500\n\t\t\t\t},\n\t\t\t\t\"image\": { \n\t\t\t\t\t\"src\": \"Images/Sun.png\",\n\t\t\t\t\t\"name\": \"sun1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 250,\n\t\t\t\t\t\"alignment\": \"center\"\n\t\t\t\t},\n\t\t\t\t\"text\": {\n\t\t\t\t\t\"data\": \"Click Here\",\n\t\t\t\t\t\"size\": 36,\n\t\t\t\t\t\"style\": \"bold\",\n\t\t\t\t\t\"name\": \"text1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 100,\n\t\t\t\t\t\"alignment\": \"center\",\n\t\t\t\t\t\"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n\t\t\t\t}\n\t\t\t}} `,\n\t\t},\n\t\t{\n\t\t\tname: \"web-app\",\n\t\t\tvalue: `{\"web-app\": {\n\t\t\t\t\"servlet\": [ \n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxCDS\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.CDSServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t \"configGlossary:installationAt\": \"Philadelphia, PA\",\n\t\t\t\t\t \"configGlossary:adminEmail\": \"ksm@pobox.com\",\n\t\t\t\t\t \"configGlossary:poweredBy\": \"Cofax\",\n\t\t\t\t\t \"configGlossary:poweredByIcon\": \"/images/cofax.gif\",\n\t\t\t\t\t \"configGlossary:staticPath\": \"/content/static\",\n\t\t\t\t\t \"templateProcessorClass\": \"org.cofax.WysiwygTemplate\",\n\t\t\t\t\t \"templateLoaderClass\": \"org.cofax.FilesTemplateLoader\",\n\t\t\t\t\t \"templatePath\": \"templates\",\n\t\t\t\t\t \"templateOverridePath\": \"\",\n\t\t\t\t\t \"defaultListTemplate\": \"listTemplate.htm\",\n\t\t\t\t\t \"defaultFileTemplate\": \"articleTemplate.htm\",\n\t\t\t\t\t \"useJSP\": false,\n\t\t\t\t\t \"jspListTemplate\": \"listTemplate.jsp\",\n\t\t\t\t\t \"jspFileTemplate\": \"articleTemplate.jsp\",\n\t\t\t\t\t \"cachePackageTagsTrack\": 200,\n\t\t\t\t\t \"cachePackageTagsStore\": 200,\n\t\t\t\t\t \"cachePackageTagsRefresh\": 60,\n\t\t\t\t\t \"cacheTemplatesTrack\": 100,\n\t\t\t\t\t \"cacheTemplatesStore\": 50,\n\t\t\t\t\t \"cacheTemplatesRefresh\": 15,\n\t\t\t\t\t \"cachePagesTrack\": 200,\n\t\t\t\t\t \"cachePagesStore\": 100,\n\t\t\t\t\t \"cachePagesRefresh\": 10,\n\t\t\t\t\t \"cachePagesDirtyRead\": 10,\n\t\t\t\t\t \"searchEngineListTemplate\": \"forSearchEnginesList.htm\",\n\t\t\t\t\t \"searchEngineFileTemplate\": \"forSearchEngines.htm\",\n\t\t\t\t\t \"searchEngineRobotsDb\": \"WEB-INF/robots.db\",\n\t\t\t\t\t \"useDataStore\": true,\n\t\t\t\t\t \"dataStoreClass\": \"org.cofax.SqlDataStore\",\n\t\t\t\t\t \"redirectionClass\": \"org.cofax.SqlRedirection\",\n\t\t\t\t\t \"dataStoreName\": \"cofax\",\n\t\t\t\t\t \"dataStoreDriver\": \"com.microsoft.jdbc.sqlserver.SQLServerDriver\",\n\t\t\t\t\t \"dataStoreUrl\": \"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\",\n\t\t\t\t\t \"dataStoreUser\": \"sa\",\n\t\t\t\t\t \"dataStorePassword\": \"dataStoreTestQuery\",\n\t\t\t\t\t \"dataStoreTestQuery\": \"SET NOCOUNT ON;select test='test';\",\n\t\t\t\t\t \"dataStoreLogFile\": \"/usr/local/tomcat/logs/datastore.log\",\n\t\t\t\t\t \"dataStoreInitConns\": 10,\n\t\t\t\t\t \"dataStoreMaxConns\": 100,\n\t\t\t\t\t \"dataStoreConnUsageLimit\": 100,\n\t\t\t\t\t \"dataStoreLogLevel\": \"debug\",\n\t\t\t\t\t \"maxUrlLength\": 500}},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxEmail\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.EmailServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t\"mailHost\": \"mail1\",\n\t\t\t\t\t\"mailHostOverride\": \"mail2\"}},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxAdmin\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.AdminServlet\"},\n\t\t\t \n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"fileServlet\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.FileServlet\"},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxTools\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cms.CofaxToolsServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t \"templatePath\": \"toolstemplates/\",\n\t\t\t\t\t \"log\": 1,\n\t\t\t\t\t \"logLocation\": \"/usr/local/tomcat/logs/CofaxTools.log\",\n\t\t\t\t\t \"logMaxSize\": \"\",\n\t\t\t\t\t \"dataLog\": 1,\n\t\t\t\t\t \"dataLogLocation\": \"/usr/local/tomcat/logs/dataLog.log\",\n\t\t\t\t\t \"dataLogMaxSize\": \"\",\n\t\t\t\t\t \"removePageCache\": \"/content/admin/remove?cache=pages\u0026id=\",\n\t\t\t\t\t \"removeTemplateCache\": \"/content/admin/remove?cache=templates\u0026id=\",\n\t\t\t\t\t \"fileTransferFolder\": \"/usr/local/tomcat/webapps/content/fileTransferFolder\",\n\t\t\t\t\t \"lookInContext\": 1,\n\t\t\t\t\t \"adminGroupID\": 4,\n\t\t\t\t\t \"betaServer\": true}}],\n\t\t\t\t\"servlet-mapping\": {\n\t\t\t\t \"cofaxCDS\": \"/\",\n\t\t\t\t \"cofaxEmail\": \"/cofaxutil/aemail/*\",\n\t\t\t\t \"cofaxAdmin\": \"/admin/*\",\n\t\t\t\t \"fileServlet\": \"/static/*\",\n\t\t\t\t \"cofaxTools\": \"/tools/*\"},\n\t\t\t \n\t\t\t\t\"taglib\": {\n\t\t\t\t \"taglib-uri\": \"cofax.tld\",\n\t\t\t\t \"taglib-location\": \"/WEB-INF/tlds/cofax.tld\"}}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"SVG Viewer\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"header\": \"SVG Viewer\",\n\t\t\t\t\"items\": [\n\t\t\t\t\t{\"id\": \"Open\"},\n\t\t\t\t\t{\"id\": \"OpenNew\", \"label\": \"Open New\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},\n\t\t\t\t\t{\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},\n\t\t\t\t\t{\"id\": \"OriginalView\", \"label\": \"Original View\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Quality\"},\n\t\t\t\t\t{\"id\": \"Pause\"},\n\t\t\t\t\t{\"id\": \"Mute\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Find\", \"label\": \"Find...\"},\n\t\t\t\t\t{\"id\": \"FindAgain\", \"label\": \"Find Again\"},\n\t\t\t\t\t{\"id\": \"Copy\"},\n\t\t\t\t\t{\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},\n\t\t\t\t\t{\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSVG\", \"label\": \"View SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSource\", \"label\": \"View Source\"},\n\t\t\t\t\t{\"id\": \"SaveAs\", \"label\": \"Save As\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Help\"},\n\t\t\t\t\t{\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}\n\t\t\t\t]\n\t\t\t}}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := Unmarshal([]byte(test.value))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshalSafe(t *testing.T) {\n\tjson := []byte(`{ \"store\": {\n\t\t\"book\": [ \n\t\t { \"category\": \"reference\",\n\t\t\t\"author\": \"Nigel Rees\",\n\t\t\t\"title\": \"Sayings of the Century\",\n\t\t\t\"price\": 8.95\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"Evelyn Waugh\",\n\t\t\t\"title\": \"Sword of Honour\",\n\t\t\t\"price\": 12.99\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"Herman Melville\",\n\t\t\t\"title\": \"Moby Dick\",\n\t\t\t\"isbn\": \"0-553-21311-3\",\n\t\t\t\"price\": 8.99\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"J. R. R. Tolkien\",\n\t\t\t\"title\": \"The Lord of the Rings\",\n\t\t\t\"isbn\": \"0-395-19395-8\",\n\t\t\t\"price\": 22.99\n\t\t }\n\t\t],\n\t\t\"bicycle\": {\n\t\t \"color\": \"red\",\n\t\t \"price\": 19.95\n\t\t}\n\t }\n\t}`)\n\tsafe, err := UnmarshalSafe(json)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t} else if safe == nil {\n\t\tt.Errorf(\"Error on Unmarshal: safe is nil\")\n\t} else {\n\t\troot, err := Unmarshal(json)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t} else if root == nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t\t} else if !bytes.Equal(root.source(), safe.source()) {\n\t\t\tt.Errorf(\"Error on UnmarshalSafe: values not same\")\n\t\t}\n\t}\n}\n\n// BenchmarkGoStdUnmarshal-8 \t 61698\t 19350 ns/op\t 288 B/op\t 6 allocs/op\n// BenchmarkUnmarshal-8 \t 45620\t 26165 ns/op\t 21889 B/op\t 367 allocs/op\n//\n// type bench struct {\n// \tName string `json:\"name\"`\n// \tValue int `json:\"value\"`\n// }\n\n// func BenchmarkGoStdUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\terr := json.Unmarshal(data, \u0026bench{})\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n\n// func BenchmarkUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\t_, err := Unmarshal(data)\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n"},{"name":"encode.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Marshal returns the JSON encoding of a Node.\nfunc Marshal(node *Node) ([]byte, error) {\n\tvar (\n\t\tbuf bytes.Buffer\n\t\tsVal string\n\t\tbVal bool\n\t\tnVal float64\n\t\toVal []byte\n\t\terr error\n\t)\n\n\tif node == nil {\n\t\treturn nil, errors.New(\"node is nil\")\n\t}\n\n\tif !node.modified \u0026\u0026 !node.ready() {\n\t\treturn nil, errors.New(\"node is not ready\")\n\t}\n\n\tif !node.modified \u0026\u0026 node.ready() {\n\t\tbuf.Write(node.source())\n\t}\n\n\tif node.modified {\n\t\tswitch node.nodeType {\n\t\tcase Null:\n\t\t\tbuf.Write(nullLiteral)\n\n\t\tcase Number:\n\t\t\tnVal, err = node.GetNumeric()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnum := strconv.FormatFloat(nVal, 'f', -1, 64)\n\t\t\tbuf.WriteString(num)\n\n\t\tcase String:\n\t\t\tsVal, err = node.GetString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tquoted := ufmt.Sprintf(\"%s\", strconv.Quote(sVal))\n\t\t\tbuf.WriteString(quoted)\n\n\t\tcase Boolean:\n\t\t\tbVal, err = node.GetBool()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbStr := ufmt.Sprintf(\"%t\", bVal)\n\t\t\tbuf.WriteString(bStr)\n\n\t\tcase Array:\n\t\t\tbuf.WriteByte(bracketOpen)\n\n\t\t\tfor i := 0; i \u003c len(node.next); i++ {\n\t\t\t\tif i != 0 {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t}\n\n\t\t\t\telem, ok := node.next[strconv.Itoa(i)]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, ufmt.Errorf(\"array element %d is not found\", i)\n\t\t\t\t}\n\n\t\t\t\toVal, err = Marshal(elem)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(bracketClose)\n\n\t\tcase Object:\n\t\t\tbuf.WriteByte(curlyOpen)\n\n\t\t\tbVal = false\n\t\t\tfor k, v := range node.next {\n\t\t\t\tif bVal {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t} else {\n\t\t\t\t\tbVal = true\n\t\t\t\t}\n\n\t\t\t\tkey := ufmt.Sprintf(\"%s\", strconv.Quote(k))\n\t\t\t\tbuf.WriteString(key)\n\t\t\t\tbuf.WriteByte(colon)\n\n\t\t\t\toVal, err = Marshal(v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(curlyClose)\n\t\t}\n\t}\n\n\treturn buf.Bytes(), nil\n}\n"},{"name":"encode_test.gno","body":"package json\n\nimport \"testing\"\n\nfunc TestMarshal_Primitive(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t}{\n\t\t{\n\t\t\tname: \"null\",\n\t\t\tnode: NullNode(\"\"),\n\t\t},\n\t\t{\n\t\t\tname: \"true\",\n\t\t\tnode: BoolNode(\"\", true),\n\t\t},\n\t\t{\n\t\t\tname: \"false\",\n\t\t\tnode: BoolNode(\"\", false),\n\t\t},\n\t\t{\n\t\t\tname: `\"string\"`,\n\t\t\tnode: StringNode(\"\", \"string\"),\n\t\t},\n\t\t{\n\t\t\tname: `\"one \\\"encoded\\\" string\"`,\n\t\t\tnode: StringNode(\"\", `one \"encoded\" string`),\n\t\t},\n\t\t{\n\t\t\tname: `{\"foo\":\"bar\"}`,\n\t\t\tnode: ObjectNode(\"\", map[string]*Node{\n\t\t\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"42\",\n\t\t\tnode: NumberNode(\"\", 42),\n\t\t},\n\t\t{\n\t\t\tname: \"3.14\",\n\t\t\tnode: NumberNode(\"\", 3.14),\n\t\t},\n\t\t{\n\t\t\tname: `[1,2,3]`,\n\t\t\tnode: ArrayNode(\"\", []*Node{\n\t\t\t\tNumberNode(\"0\", 1),\n\t\t\t\tNumberNode(\"2\", 2),\n\t\t\t\tNumberNode(\"3\", 3),\n\t\t\t}),\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t} else if string(value) != test.name {\n\t\t\t\tt.Errorf(\"wrong result: '%s', expected '%s'\", value, test.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Object(t *testing.T) {\n\tnode := ObjectNode(\"\", map[string]*Node{\n\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\"baz\": NumberNode(\"baz\", 100500),\n\t\t\"qux\": NullNode(\"qux\"),\n\t})\n\n\tmustKey := []string{\"foo\", \"baz\", \"qux\"}\n\n\tvalue, err := Marshal(node)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\t// the order of keys in the map is not guaranteed\n\t// so we need to unmarshal the result and check the keys\n\tdecoded, err := Unmarshal(value)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\tfor _, key := range mustKey {\n\t\tif node, err := decoded.GetKey(key); err != nil {\n\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t} else {\n\t\t\tif node == nil {\n\t\t\t\tt.Errorf(\"node is nil\")\n\t\t\t} else if node.key == nil {\n\t\t\t\tt.Errorf(\"key is nil\")\n\t\t\t} else if *node.key != key {\n\t\t\t\tt.Errorf(\"wrong key: '%s', expected '%s'\", *node.key, key)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc valueNode(prev *Node, key string, typ ValueType, val interface{}) *Node {\n\tcurr := \u0026Node{\n\t\tprev: prev,\n\t\tdata: nil,\n\t\tkey: \u0026key,\n\t\tborders: [2]int{0, 0},\n\t\tvalue: val,\n\t\tmodified: true,\n\t}\n\n\tif val != nil {\n\t\tcurr.nodeType = typ\n\t}\n\n\treturn curr\n}\n\nfunc TestMarshal_Errors(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode func() (node *Node)\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"broken\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = Must(Unmarshal([]byte(`{}`)))\n\t\t\t\tnode.borders[1] = 0\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Numeric\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Number, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"String\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", String, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Bool\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Boolean, 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_1\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = ArrayNode(\"\", nil)\n\t\t\t\tnode.next[\"1\"] = NullNode(\"1\")\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_2\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ArrayNode(\"\", []*Node{valueNode(nil, \"\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Object\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ObjectNode(\"\", map[string]*Node{\"key\": valueNode(nil, \"key\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node())\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected error\")\n\t\t\t} else if len(value) != 0 {\n\t\t\t\tt.Errorf(\"wrong result\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Nil(t *testing.T) {\n\t_, err := Marshal(nil)\n\tif err == nil {\n\t\tt.Error(\"Expected error for nil node, but got nil\")\n\t}\n}\n\nfunc TestMarshal_NotModified(t *testing.T) {\n\tnode := \u0026Node{}\n\t_, err := Marshal(node)\n\tif err == nil {\n\t\tt.Error(\"Expected error for not modified node, but got nil\")\n\t}\n}\n\nfunc TestMarshalCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey: stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tnext: map[string]*Node{\n\t\t\t\"next\": nil,\n\t\t},\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey: stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tprev: node1,\n\t}\n\n\tnode1.next[\"next\"] = node2\n\n\t_, err := Marshal(node1)\n\tif err == nil {\n\t\tt.Error(\"Expected error for cycle reference, but got nil\")\n\t}\n}\n\nfunc TestMarshalNoCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey: stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tvalue: \"value1\",\n\t\tmodified: true,\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey: stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tvalue: \"value2\",\n\t\tmodified: true,\n\t}\n\n\t_, err := Marshal(node1)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n\n\t_, err = Marshal(node2)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n}\n\nfunc stringPtr(s string) *string {\n\treturn \u0026s\n}\n"},{"name":"errors.gno","body":"package json\n\nimport \"errors\"\n\nvar (\n\terrNilNode = errors.New(\"node is nil\")\n\terrNotArrayNode = errors.New(\"node is not array\")\n\terrNotBoolNode = errors.New(\"node is not boolean\")\n\terrNotNullNode = errors.New(\"node is not null\")\n\terrNotNumberNode = errors.New(\"node is not number\")\n\terrNotObjectNode = errors.New(\"node is not object\")\n\terrNotStringNode = errors.New(\"node is not string\")\n\terrInvalidToken = errors.New(\"invalid token\")\n\terrIndexNotFound = errors.New(\"index not found\")\n\terrInvalidAppend = errors.New(\"can't append value to non-appendable node\")\n\terrInvalidAppendCycle = errors.New(\"appending value to itself or its children or parents will cause a cycle\")\n\terrInvalidEscapeSequence = errors.New(\"invalid escape sequence\")\n\terrInvalidStringValue = errors.New(\"invalid string value\")\n\terrEmptyBooleanNode = errors.New(\"boolean node is empty\")\n\terrEmptyStringNode = errors.New(\"string node is empty\")\n\terrKeyRequired = errors.New(\"key is required for object\")\n\terrUnmatchedParenthesis = errors.New(\"mismatched bracket or parenthesis\")\n\terrUnmatchedQuotePath = errors.New(\"unmatched quote in path\")\n)\n\nvar (\n\terrInvalidStringInput = errors.New(\"invalid string input\")\n\terrMalformedBooleanValue = errors.New(\"malformed boolean value\")\n\terrEmptyByteSlice = errors.New(\"empty byte slice\")\n\terrInvalidExponentValue = errors.New(\"invalid exponent value\")\n\terrNonDigitCharacters = errors.New(\"non-digit characters found\")\n\terrNumericRangeExceeded = errors.New(\"numeric value exceeds the range limit\")\n\terrMultipleDecimalPoints = errors.New(\"multiple decimal points found\")\n)\n"},{"name":"escape.gno","body":"package json\n\nimport (\n\t\"unicode/utf8\"\n)\n\nconst (\n\tsupplementalPlanesOffset = 0x10000\n\thighSurrogateOffset = 0xD800\n\tlowSurrogateOffset = 0xDC00\n\tsurrogateEnd = 0xDFFF\n\tbasicMultilingualPlaneOffset = 0xFFFF\n\tbadHex = -1\n\n\tsingleUnicodeEscapeLen = 6\n\tsurrogatePairLen = 12\n)\n\nvar hexLookupTable = [256]int{\n\t'0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, '4': 0x4,\n\t'5': 0x5, '6': 0x6, '7': 0x7, '8': 0x8, '9': 0x9,\n\t'A': 0xA, 'B': 0xB, 'C': 0xC, 'D': 0xD, 'E': 0xE, 'F': 0xF,\n\t'a': 0xA, 'b': 0xB, 'c': 0xC, 'd': 0xD, 'e': 0xE, 'f': 0xF,\n\t// Fill unspecified index-value pairs with key and value of -1\n\t'G': -1, 'H': -1, 'I': -1, 'J': -1,\n\t'K': -1, 'L': -1, 'M': -1, 'N': -1,\n\t'O': -1, 'P': -1, 'Q': -1, 'R': -1,\n\t'S': -1, 'T': -1, 'U': -1, 'V': -1,\n\t'W': -1, 'X': -1, 'Y': -1, 'Z': -1,\n\t'g': -1, 'h': -1, 'i': -1, 'j': -1,\n\t'k': -1, 'l': -1, 'm': -1, 'n': -1,\n\t'o': -1, 'p': -1, 'q': -1, 'r': -1,\n\t's': -1, 't': -1, 'u': -1, 'v': -1,\n\t'w': -1, 'x': -1, 'y': -1, 'z': -1,\n}\n\nfunc h2i(c byte) int {\n\treturn hexLookupTable[c]\n}\n\n// Unescape takes an input byte slice, processes it to Unescape certain characters,\n// and writes the result into an output byte slice.\n//\n// it returns the processed slice and any error encountered during the Unescape operation.\nfunc Unescape(input, output []byte) ([]byte, error) {\n\t// ensure the output slice has enough capacity to hold the input slice.\n\tinputLen := len(input)\n\tif cap(output) \u003c inputLen {\n\t\toutput = make([]byte, inputLen)\n\t}\n\n\tinPos, outPos := 0, 0\n\n\tfor inPos \u003c len(input) {\n\t\tc := input[inPos]\n\t\tif c != backSlash {\n\t\t\toutput[outPos] = c\n\t\t\tinPos++\n\t\t\toutPos++\n\t\t} else {\n\t\t\t// process escape sequence\n\t\t\tinLen, outLen, err := processEscapedUTF8(input[inPos:], output[outPos:])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tinPos += inLen\n\t\t\toutPos += outLen\n\t\t}\n\t}\n\n\treturn output[:outPos], nil\n}\n\n// isSurrogatePair returns true if the rune is a surrogate pair.\n//\n// A surrogate pairs are used in UTF-16 encoding to encode characters\n// outside the Basic Multilingual Plane (BMP).\nfunc isSurrogatePair(r rune) bool {\n\treturn highSurrogateOffset \u003c= r \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// isHighSurrogate checks if the rune is a high surrogate (U+D800 to U+DBFF).\nfunc isHighSurrogate(r rune) bool {\n\treturn r \u003e= highSurrogateOffset \u0026\u0026 r \u003c= 0xDBFF\n}\n\n// isLowSurrogate checks if the rune is a low surrogate (U+DC00 to U+DFFF).\nfunc isLowSurrogate(r rune) bool {\n\treturn r \u003e= lowSurrogateOffset \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// combineSurrogates reconstruct the original unicode code points in the\n// supplemental plane by combinin the high and low surrogate.\n//\n// The hight surrogate in the range from U+D800 to U+DBFF,\n// and the low surrogate in the range from U+DC00 to U+DFFF.\n//\n// The formula to combine the surrogates is:\n// (high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000\nfunc combineSurrogates(high, low rune) rune {\n\treturn ((high - highSurrogateOffset) \u003c\u003c 10) + (low - lowSurrogateOffset) + supplementalPlanesOffset\n}\n\n// deocdeSingleUnicodeEscape decodes a unicode escape sequence (e.g., \\uXXXX) into a rune.\nfunc decodeSingleUnicodeEscape(b []byte) (rune, bool) {\n\tif len(b) \u003c 6 {\n\t\treturn utf8.RuneError, false\n\t}\n\n\t// convert hex to decimal\n\th1, h2, h3, h4 := h2i(b[2]), h2i(b[3]), h2i(b[4]), h2i(b[5])\n\tif h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex {\n\t\treturn utf8.RuneError, false\n\t}\n\n\treturn rune(h1\u003c\u003c12 + h2\u003c\u003c8 + h3\u003c\u003c4 + h4), true\n}\n\n// decodeUnicodeEscape decodes a Unicode escape sequence from a byte slice.\n// It handles both single Unicode escape sequences and surrogate pairs.\nfunc decodeUnicodeEscape(b []byte) (rune, int) {\n\t// decode the first Unicode escape sequence.\n\tr, ok := decodeSingleUnicodeEscape(b)\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is within the BMP and not a surrogate, return it\n\tif r \u003c= basicMultilingualPlaneOffset \u0026\u0026 !isSurrogatePair(r) {\n\t\treturn r, 6\n\t}\n\n\tif !isHighSurrogate(r) {\n\t\t// invalid surrogate pair.\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is a high surrogate, need to decode the next escape sequence.\n\n\t// ensure there are enough bytes for the next escape sequence.\n\tif len(b) \u003c surrogatePairLen {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// decode the second Unicode escape sequence.\n\tr2, ok := decodeSingleUnicodeEscape(b[singleUnicodeEscapeLen:])\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// check if the second rune is a low surrogate.\n\tif isLowSurrogate(r2) {\n\t\tcombined := combineSurrogates(r, r2)\n\t\treturn combined, surrogatePairLen\n\t}\n\treturn utf8.RuneError, -1\n}\n\nvar escapeByteSet = [256]byte{\n\t'\"': doubleQuote,\n\t'\\\\': backSlash,\n\t'/': slash,\n\t'b': backSpace,\n\t'f': formFeed,\n\t'n': newLine,\n\t'r': carriageReturn,\n\t't': tab,\n}\n\n// Unquote takes a byte slice and unquotes it by removing\n// the surrounding quotes and unescaping the contents.\nfunc Unquote(s []byte, border byte) (string, bool) {\n\ts, ok := unquoteBytes(s, border)\n\treturn string(s), ok\n}\n\n// unquoteBytes takes a byte slice and unquotes it by removing\nfunc unquoteBytes(s []byte, border byte) ([]byte, bool) {\n\tif len(s) \u003c 2 || s[0] != border || s[len(s)-1] != border {\n\t\treturn nil, false\n\t}\n\n\ts = s[1 : len(s)-1]\n\n\tr := 0\n\tfor r \u003c len(s) {\n\t\tc := s[r]\n\n\t\tif c == backSlash || c == border || c \u003c 0x20 {\n\t\t\tbreak\n\t\t}\n\n\t\tif c \u003c utf8.RuneSelf {\n\t\t\tr++\n\t\t\tcontinue\n\t\t}\n\n\t\trr, size := utf8.DecodeRune(s[r:])\n\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\tbreak\n\t\t}\n\n\t\tr += size\n\t}\n\n\tif r == len(s) {\n\t\treturn s, true\n\t}\n\n\tutfDoubleMax := utf8.UTFMax * 2\n\tb := make([]byte, len(s)+utfDoubleMax)\n\tw := copy(b, s[0:r])\n\n\tfor r \u003c len(s) {\n\t\tif w \u003e= len(b)-utf8.UTFMax {\n\t\t\tnb := make([]byte, utfDoubleMax+(2*len(b)))\n\t\t\tcopy(nb, b)\n\t\t\tb = nb\n\t\t}\n\n\t\tc := s[r]\n\t\tif c == backSlash {\n\t\t\tr++\n\t\t\tif r \u003e= len(s) {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tif s[r] == 'u' {\n\t\t\t\trr, res := decodeUnicodeEscape(s[r-1:])\n\t\t\t\tif res \u003c 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t\t\tr += 5\n\t\t\t} else {\n\t\t\t\tdecode := escapeByteSet[s[r]]\n\t\t\t\tif decode == 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tif decode == doubleQuote || decode == backSlash || decode == slash {\n\t\t\t\t\tdecode = s[r]\n\t\t\t\t}\n\n\t\t\t\tb[w] = decode\n\t\t\t\tr++\n\t\t\t\tw++\n\t\t\t}\n\t\t} else if c == border || c \u003c 0x20 {\n\t\t\treturn nil, false\n\t\t} else if c \u003c utf8.RuneSelf {\n\t\t\tb[w] = c\n\t\t\tr++\n\t\t\tw++\n\t\t} else {\n\t\t\trr, size := utf8.DecodeRune(s[r:])\n\n\t\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tr += size\n\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t}\n\t}\n\n\treturn b[:w], true\n}\n\n// processEscapedUTF8 converts escape sequences to UTF-8 characters.\n// It decodes Unicode escape sequences (\\uXXXX) to UTF-8 and\n// converts standard escape sequences (e.g., \\n) to their corresponding special characters.\nfunc processEscapedUTF8(in, out []byte) (int, int, error) {\n\tif len(in) \u003c 2 || in[0] != backSlash {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\tescapeSeqLen := 2\n\tescapeChar := in[1]\n\n\tif escapeChar != 'u' {\n\t\tval := escapeByteSet[escapeChar]\n\t\tif val == 0 {\n\t\t\treturn -1, -1, errInvalidEscapeSequence\n\t\t}\n\n\t\tout[0] = val\n\t\treturn escapeSeqLen, 1, nil\n\t}\n\n\tr, size := decodeUnicodeEscape(in)\n\tif size == -1 {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\toutLen := utf8.EncodeRune(out, r)\n\n\treturn size, outLen, nil\n}\n"},{"name":"escape_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestHexToInt(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tc byte\n\t\twant int\n\t}{\n\t\t{\"Digit 0\", '0', 0},\n\t\t{\"Digit 9\", '9', 9},\n\t\t{\"Uppercase A\", 'A', 10},\n\t\t{\"Uppercase F\", 'F', 15},\n\t\t{\"Lowercase a\", 'a', 10},\n\t\t{\"Lowercase f\", 'f', 15},\n\t\t{\"Invalid character1\", 'g', badHex},\n\t\t{\"Invalid character2\", 'G', badHex},\n\t\t{\"Invalid character3\", 'z', badHex},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := h2i(tt.c); got != tt.want {\n\t\t\t\tt.Errorf(\"h2i() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsSurrogatePair(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tr rune\n\t\texpected bool\n\t}{\n\t\t{\"high surrogate start\", 0xD800, true},\n\t\t{\"high surrogate end\", 0xDBFF, true},\n\t\t{\"low surrogate start\", 0xDC00, true},\n\t\t{\"low surrogate end\", 0xDFFF, true},\n\t\t{\"Non-surrogate\", 0x0000, false},\n\t\t{\"Non-surrogate 2\", 0xE000, false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif got := isSurrogatePair(tc.r); got != tc.expected {\n\t\t\t\tt.Errorf(\"isSurrogate() = %v, want %v\", got, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCombineSurrogates(t *testing.T) {\n\ttestCases := []struct {\n\t\thigh, low rune\n\t\texpected rune\n\t}{\n\t\t{0xD83D, 0xDC36, 0x1F436}, // 🐶 U+1F436 DOG FACE\n\t\t{0xD83D, 0xDE00, 0x1F600}, // 😀 U+1F600 GRINNING FACE\n\t\t{0xD83C, 0xDF03, 0x1F303}, // 🌃 U+1F303 NIGHT WITH STARS\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := combineSurrogates(tc.high, tc.low)\n\t\tif result != tc.expected {\n\t\t\tt.Errorf(\"combineSurrogates(%U, %U) = %U; want %U\", tc.high, tc.low, result, tc.expected)\n\t\t}\n\t}\n}\n\nfunc TestDecodeSingleUnicodeEscape(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput []byte\n\t\texpected rune\n\t\tisValid bool\n\t}{\n\t\t// valid unicode escape sequences\n\t\t{[]byte(`\\u0041`), 'A', true},\n\t\t{[]byte(`\\u03B1`), 'α', true},\n\t\t{[]byte(`\\u00E9`), 'é', true}, // valid non-English character\n\t\t{[]byte(`\\u0021`), '!', true}, // valid special character\n\t\t{[]byte(`\\uFF11`), '1', true},\n\t\t{[]byte(`\\uD83D`), 0xD83D, true},\n\t\t{[]byte(`\\uDE03`), 0xDE03, true},\n\n\t\t// invalid unicode escape sequences\n\t\t{[]byte(`\\u004`), utf8.RuneError, false}, // too short\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, false}, // invalid hex\n\t\t{[]byte(`\\u00G1`), utf8.RuneError, false}, // non-hex character\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult, isValid := decodeSingleUnicodeEscape(tc.input)\n\t\tif result != tc.expected || isValid != tc.isValid {\n\t\t\tt.Errorf(\"decodeSingleUnicodeEscape(%s) = (%U, %v); want (%U, %v)\", tc.input, result, isValid, tc.expected, tc.isValid)\n\t\t}\n\t}\n}\n\nfunc TestDecodeUnicodeEscape(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpected rune\n\t\tsize int\n\t}{\n\t\t{[]byte(`\\u0041`), 'A', 6},\n\t\t{[]byte(`\\uD83D\\uDE00`), 0x1F600, 12}, // 😀\n\t\t{[]byte(`\\uD834\\uDD1E`), 0x1D11E, 12}, // 𝄞\n\t\t{[]byte(`\\uFFFF`), '\\uFFFF', 6},\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, -1},\n\t\t{[]byte(`\\uD800`), utf8.RuneError, -1}, // single high surrogate\n\t\t{[]byte(`\\uDC00`), utf8.RuneError, -1}, // single low surrogate\n\t\t{[]byte(`\\uD800\\uDC00`), 0x10000, 12}, // First code point above U+FFFF\n\t\t{[]byte(`\\uDBFF\\uDFFF`), 0x10FFFF, 12}, // Maximum code point\n\t\t{[]byte(`\\uD83D\\u0041`), utf8.RuneError, -1}, // invalid surrogate pair\n\t}\n\n\tfor _, tc := range tests {\n\t\tr, size := decodeUnicodeEscape(tc.input)\n\t\tif r != tc.expected || size != tc.size {\n\t\t\tt.Errorf(\"decodeUnicodeEscape(%q) = (%U, %d); want (%U, %d)\", tc.input, r, size, tc.expected, tc.size)\n\t\t}\n\t}\n}\n\nfunc TestUnescapeToUTF8(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpectedIn int\n\t\texpectedOut int\n\t\tisError bool\n\t}{\n\t\t// valid escape sequences\n\t\t{[]byte(`\\n`), 2, 1, false},\n\t\t{[]byte(`\\t`), 2, 1, false},\n\t\t{[]byte(`\\u0041`), 6, 1, false},\n\t\t{[]byte(`\\u03B1`), 6, 2, false},\n\t\t{[]byte(`\\uD830\\uDE03`), 12, 4, false},\n\n\t\t// invalid escape sequences\n\t\t{[]byte(`\\`), -1, -1, true}, // incomplete escape sequence\n\t\t{[]byte(`\\x`), -1, -1, true}, // invalid escape character\n\t\t{[]byte(`\\u`), -1, -1, true}, // incomplete unicode escape sequence\n\t\t{[]byte(`\\u004`), -1, -1, true}, // invalid unicode escape sequence\n\t\t{[]byte(`\\uXYZW`), -1, -1, true}, // invalid unicode escape sequence\n\t\t{[]byte(`\\uD83D\\u0041`), -1, -1, true}, // invalid unicode escape sequence\n\t}\n\n\tfor _, tc := range tests {\n\t\tinput := make([]byte, len(tc.input))\n\t\tcopy(input, tc.input)\n\t\toutput := make([]byte, utf8.UTFMax)\n\t\tinLen, outLen, err := processEscapedUTF8(input, output)\n\t\tif (err != nil) != tc.isError {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = %v; want %v\", tc.input, err, tc.isError)\n\t\t}\n\n\t\tif inLen != tc.expectedIn || outLen != tc.expectedOut {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = (%d, %d); want (%d, %d)\", tc.input, inLen, outLen, tc.expectedIn, tc.expectedOut)\n\t\t}\n\t}\n}\n\nfunc TestUnescape(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\texpected []byte\n\t\tisError bool\n\t}{\n\t\t{\"NoEscape\", []byte(\"hello world\"), []byte(\"hello world\"), false},\n\t\t{\"SingleEscape\", []byte(\"hello\\\\nworld\"), []byte(\"hello\\nworld\"), false},\n\t\t{\"MultipleEscapes\", []byte(\"line1\\\\nline2\\\\r\\\\nline3\"), []byte(\"line1\\nline2\\r\\nline3\"), false},\n\t\t{\"UnicodeEscape\", []byte(\"snowman:\\\\u2603\"), []byte(\"snowman:\\u2603\"), false},\n\t\t{\"SurrogatePair\", []byte(\"emoji:\\\\uD83D\\\\uDE00\"), []byte(\"emoji:😀\"), false},\n\t\t{\"InvalidEscape\", []byte(\"hello\\\\xworld\"), nil, true},\n\t\t{\"IncompleteUnicode\", []byte(\"incomplete:\\\\u123\"), nil, true},\n\t\t{\"InvalidSurrogatePair\", []byte(\"invalid:\\\\uD83D\\\\u0041\"), nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := make([]byte, len(tc.input)*2) // Allocate extra space for possible expansion\n\t\t\tresult, err := Unescape(tc.input, output)\n\t\t\tif (err != nil) != tc.isError {\n\t\t\t\tt.Errorf(\"Unescape(%q) error = %v; want error = %v\", tc.input, err, tc.isError)\n\t\t\t}\n\n\t\t\tif !tc.isError \u0026\u0026 !bytes.Equal(result, tc.expected) {\n\t\t\t\tt.Errorf(\"Unescape(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnquoteBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\tborder byte\n\t\texpected []byte\n\t\tok bool\n\t}{\n\t\t{[]byte(\"\\\"hello\\\"\"), '\"', []byte(\"hello\"), true},\n\t\t{[]byte(\"'hello'\"), '\\'', []byte(\"hello\"), true},\n\t\t{[]byte(\"\\\"hello\"), '\"', nil, false},\n\t\t{[]byte(\"hello\\\"\"), '\"', nil, false},\n\t\t{[]byte(\"\\\"he\\\\\\\"llo\\\"\"), '\"', []byte(\"he\\\"llo\"), true},\n\t\t{[]byte(\"\\\"he\\\\nllo\\\"\"), '\"', []byte(\"he\\nllo\"), true},\n\t\t{[]byte(\"\\\"\\\"\"), '\"', []byte(\"\"), true},\n\t\t{[]byte(\"''\"), '\\'', []byte(\"\"), true},\n\t\t{[]byte(\"\\\"\\\\u0041\\\"\"), '\"', []byte(\"A\"), true},\n\t\t{[]byte(`\"Hello, 世界\"`), '\"', []byte(\"Hello, 世界\"), true},\n\t\t{[]byte(`\"Hello, \\x80\"`), '\"', nil, false},\n\t\t{[]byte(`\"invalid surrogate: \\uD83D\\u0041\"`), '\"', nil, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tresult, pass := unquoteBytes(tc.input, tc.border)\n\n\t\tif pass != tc.ok {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %v; want %v\", tc.input, pass, tc.ok)\n\t\t}\n\n\t\tif !bytes.Equal(result, tc.expected) {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t}\n\t}\n}\n"},{"name":"indent.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n)\n\n// indentGrowthFactor specifies the growth factor of indenting JSON input.\n// A factor no higher than 2 ensures that wasted space never exceeds 50%.\nconst indentGrowthFactor = 2\n\n// IndentJSON formats the JSON data with the specified indentation.\nfunc Indent(data []byte, indent string) ([]byte, error) {\n\tvar (\n\t\tout bytes.Buffer\n\t\tlevel int\n\t\tinArray bool\n\t\tarrayDepth int\n\t)\n\n\tfor i := 0; i \u003c len(data); i++ {\n\t\tc := data[i] // current character\n\n\t\tswitch c {\n\t\tcase bracketOpen:\n\t\t\tarrayDepth++\n\t\t\tif arrayDepth \u003e 1 {\n\t\t\t\tlevel++ // increase the level if it's nested array\n\t\t\t\tinArray = true\n\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// case of the top-level array\n\t\t\t\tinArray = true\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase bracketClose:\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tlevel--\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tarrayDepth--\n\t\t\tif arrayDepth == 0 {\n\t\t\t\tinArray = false\n\t\t\t}\n\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase curlyOpen:\n\t\t\t// check if the empty object or array\n\t\t\t// we don't need to apply the indent when it's empty containers.\n\t\t\tif i+1 \u003c len(data) \u0026\u0026 data[i+1] == curlyClose {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\ti++ // skip next character\n\t\t\t\tif err := out.WriteByte(data[i]); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tlevel++\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase curlyClose:\n\t\t\tlevel--\n\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase comma, colon:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else if c == colon {\n\t\t\t\tif err := out.WriteByte(' '); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out.Bytes(), nil\n}\n\nfunc writeNewlineAndIndent(out *bytes.Buffer, level int, indent string) error {\n\tif err := out.WriteByte('\\n'); err != nil {\n\t\treturn err\n\t}\n\n\tidt := strings.Repeat(indent, level*indentGrowthFactor)\n\tif _, err := out.WriteString(idt); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"},{"name":"indent_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestIndentJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\tindent string\n\t\texpected []byte\n\t}{\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tinput: []byte(`{}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(`{}`),\n\t\t},\n\t\t{\n\t\t\tname: \"empty array\",\n\t\t\tinput: []byte(`[]`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(`[]`),\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tinput: []byte(`{{}}`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(\"{\\n\\t\\t{}\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"nested array\",\n\t\t\tinput: []byte(`[[[]]]`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(\"[[\\n\\t\\t[\\n\\t\\t\\t\\t\\n\\t\\t]\\n]]\"),\n\t\t},\n\t\t{\n\t\t\tname: \"top-level array\",\n\t\t\tinput: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t},\n\t\t{\n\t\t\tname: \"array of arrays\",\n\t\t\tinput: []byte(`[\"apple\",[\"banana\",\"cherry\"],\"date\"]`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"[\\\"apple\\\",[\\n \\\"banana\\\",\\n \\\"cherry\\\"\\n],\\\"date\\\"]\"),\n\t\t},\n\n\t\t{\n\t\t\tname: \"nested array in object\",\n\t\t\tinput: []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"{\\n \\\"fruits\\\": [\\\"apple\\\",[\\n \\\"banana\\\",\\n \\\"cherry\\\"\\n ],\\\"date\\\"]\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"complex nested structure\",\n\t\t\tinput: []byte(`{\"data\":{\"array\":[1,2,3],\"bool\":true,\"nestedArray\":[[\"a\",\"b\"],\"c\"]}}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"{\\n \\\"data\\\": {\\n \\\"array\\\": [1,2,3],\\\"bool\\\": true,\\\"nestedArray\\\": [[\\n \\\"a\\\",\\n \\\"b\\\"\\n ],\\\"c\\\"]\\n }\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"custom ident character\",\n\t\t\tinput: []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent: \"*\",\n\t\t\texpected: []byte(\"{\\n**\\\"fruits\\\": [\\\"apple\\\",[\\n****\\\"banana\\\",\\n****\\\"cherry\\\"\\n**],\\\"date\\\"]\\n}\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual, err := Indent(tt.input, tt.indent)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"IndentJSON() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !bytes.Equal(actual, tt.expected) {\n\t\t\t\tt.Errorf(\"IndentJSON() = %q, want %q\", actual, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"},{"name":"internal.gno","body":"package json\n\n// Reference: https://github.com/freddierice/php_source/blob/467ed5d6edff72219afd3e644516f131118ef48e/ext/json/JSON_parser.c\n// Copyright (c) 2005 JSON.org\n\n// Go implementation is taken from: https://github.com/spyzhov/ajson/blob/master/internal/state.go\n\ntype (\n\tStates int8 // possible states of the parser\n\tClasses int8 // JSON string character types\n)\n\nconst __ = -1\n\n// enum classes\nconst (\n\tC_SPACE Classes = iota /* space */\n\tC_WHITE /* other whitespace */\n\tC_LCURB /* { */\n\tC_RCURB /* } */\n\tC_LSQRB /* [ */\n\tC_RSQRB /* ] */\n\tC_COLON /* : */\n\tC_COMMA /* , */\n\tC_QUOTE /* \" */\n\tC_BACKS /* \\ */\n\tC_SLASH /* / */\n\tC_PLUS /* + */\n\tC_MINUS /* - */\n\tC_POINT /* . */\n\tC_ZERO /* 0 */\n\tC_DIGIT /* 123456789 */\n\tC_LOW_A /* a */\n\tC_LOW_B /* b */\n\tC_LOW_C /* c */\n\tC_LOW_D /* d */\n\tC_LOW_E /* e */\n\tC_LOW_F /* f */\n\tC_LOW_L /* l */\n\tC_LOW_N /* n */\n\tC_LOW_R /* r */\n\tC_LOW_S /* s */\n\tC_LOW_T /* t */\n\tC_LOW_U /* u */\n\tC_ABCDF /* ABCDF */\n\tC_E /* E */\n\tC_ETC /* everything else */\n)\n\n// AsciiClasses array maps the 128 ASCII characters into character classes.\nvar AsciiClasses = [128]Classes{\n\t/*\n\t This array maps the 128 ASCII characters into character classes.\n\t The remaining Unicode characters should be mapped to C_ETC.\n\t Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_QUOTE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n// QuoteAsciiClasses is a HACK for single quote from AsciiClasses\nvar QuoteAsciiClasses = [128]Classes{\n\t/*\n\t This array maps the 128 ASCII characters into character classes.\n\t The remaining Unicode characters should be mapped to C_ETC.\n\t Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_QUOTE,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n/*\nThe state codes.\n*/\nconst (\n\tGO States = iota /* start */\n\tOK /* ok */\n\tOB /* object */\n\tKE /* key */\n\tCO /* colon */\n\tVA /* value */\n\tAR /* array */\n\tST /* string */\n\tES /* escape */\n\tU1 /* u1 */\n\tU2 /* u2 */\n\tU3 /* u3 */\n\tU4 /* u4 */\n\tMI /* minus */\n\tZE /* zero */\n\tIN /* integer */\n\tDT /* dot */\n\tFR /* fraction */\n\tE1 /* e */\n\tE2 /* ex */\n\tE3 /* exp */\n\tT1 /* tr */\n\tT2 /* tru */\n\tT3 /* true */\n\tF1 /* fa */\n\tF2 /* fal */\n\tF3 /* fals */\n\tF4 /* false */\n\tN1 /* nu */\n\tN2 /* nul */\n\tN3 /* null */\n)\n\n// List of action codes.\n// these constants are defining an action that should be performed under certain conditions.\nconst (\n\tcl States = -2 /* colon */\n\tcm States = -3 /* comma */\n\tqt States = -4 /* quote */\n\tbo States = -5 /* bracket open */\n\tco States = -6 /* curly bracket open */\n\tbc States = -7 /* bracket close */\n\tcc States = -8 /* curly bracket close */\n\tec States = -9 /* curly bracket empty */\n)\n\n// StateTransitionTable is the state transition table takes the current state and the current symbol, and returns either\n// a new state or an action. An action is represented as a negative number. A JSON text is accepted if at the end of the\n// text the state is OK and if the mode is DONE.\nvar StateTransitionTable = [31][31]States{\n\t/*\n\t The state transition table takes the current state and the current symbol,\n\t and returns either a new state or an action. An action is represented as a\n\t negative number. A JSON text is accepted if at the end of the text the\n\t state is OK and if the mode is DONE.\n\t white 1-9 ABCDF etc\n\t space | { } [ ] : , \" \\ / + - . 0 | a b c d e f l n r s t u | E |*/\n\t/*start GO*/ {GO, GO, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*ok OK*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*object OB*/ {OB, OB, __, ec, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*key KE*/ {KE, KE, __, __, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*colon CO*/ {CO, CO, __, __, __, __, cl, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*value VA*/ {VA, VA, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*array AR*/ {AR, AR, co, __, bo, bc, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*string ST*/ {ST, __, ST, ST, ST, ST, ST, ST, qt, ES, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST},\n\t/*escape ES*/ {__, __, __, __, __, __, __, __, ST, ST, ST, __, __, __, __, __, __, ST, __, __, __, ST, __, ST, ST, __, ST, U1, __, __, __},\n\t/*u1 U1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U2, U2, U2, U2, U2, U2, U2, U2, __, __, __, __, __, __, U2, U2, __},\n\t/*u2 U2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U3, U3, U3, U3, U3, U3, U3, U3, __, __, __, __, __, __, U3, U3, __},\n\t/*u3 U3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U4, U4, U4, U4, U4, U4, U4, U4, __, __, __, __, __, __, U4, U4, __},\n\t/*u4 U4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ST, ST, ST, ST, ST, ST, ST, ST, __, __, __, __, __, __, ST, ST, __},\n\t/*minus MI*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ZE, IN, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*zero ZE*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, __, __, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*int IN*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, IN, IN, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*dot DT*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, FR, FR, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*frac FR*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, FR, FR, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*e E1*/ {__, __, __, __, __, __, __, __, __, __, __, E2, E2, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*ex E2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*exp E3*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*tr T1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T2, __, __, __, __, __, __},\n\t/*tru T2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T3, __, __, __},\n\t/*true T3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*fa F1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F2, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*fal F2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F3, __, __, __, __, __, __, __, __},\n\t/*fals F3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F4, __, __, __, __, __},\n\t/*false F4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*nu N1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N2, __, __, __},\n\t/*nul N2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N3, __, __, __, __, __, __, __, __},\n\t/*null N3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __},\n}\n"},{"name":"node.gno","body":"package json\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Node represents a JSON node.\ntype Node struct {\n\tprev *Node // prev is the parent node of the current node.\n\tnext map[string]*Node // next is the child nodes of the current node.\n\tkey *string // key holds the key of the current node in the parent node.\n\tdata []byte // byte slice of JSON data\n\tvalue interface{} // value holds the value of the current node.\n\tnodeType ValueType // NodeType holds the type of the current node. (Object, Array, String, Number, Boolean, Null)\n\tindex *int // index holds the index of the current node in the parent array node.\n\tborders [2]int // borders stores the start and end index of the current node in the data.\n\tmodified bool // modified indicates the current node is changed or not.\n}\n\n// NewNode creates a new node instance with the given parent node, buffer, type, and key.\nfunc NewNode(prev *Node, b *buffer, typ ValueType, key **string) (*Node, error) {\n\tcurr := \u0026Node{\n\t\tprev: prev,\n\t\tdata: b.data,\n\t\tborders: [2]int{b.index, 0},\n\t\tkey: *key,\n\t\tnodeType: typ,\n\t\tmodified: false,\n\t}\n\n\tif typ == Object || typ == Array {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\tif prev != nil {\n\t\tif prev.IsArray() {\n\t\t\tsize := len(prev.next)\n\t\t\tcurr.index = \u0026size\n\n\t\t\tprev.next[strconv.Itoa(size)] = curr\n\t\t} else if prev.IsObject() {\n\t\t\tif key == nil {\n\t\t\t\treturn nil, errKeyRequired\n\t\t\t}\n\n\t\t\tprev.next[**key] = curr\n\t\t} else {\n\t\t\treturn nil, errors.New(\"invalid parent type\")\n\t\t}\n\t}\n\n\treturn curr, nil\n}\n\n// load retrieves the value of the current node.\nfunc (n *Node) load() interface{} {\n\treturn n.value\n}\n\n// Changed checks the current node is changed or not.\nfunc (n *Node) Changed() bool {\n\treturn n.modified\n}\n\n// Key returns the key of the current node.\nfunc (n *Node) Key() string {\n\tif n == nil || n.key == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *n.key\n}\n\n// HasKey checks the current node has the given key or not.\nfunc (n *Node) HasKey(key string) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\t_, ok := n.next[key]\n\treturn ok\n}\n\n// GetKey returns the value of the given key from the current object node.\nfunc (n *Node) GetKey(key string) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.Type() != Object {\n\t\treturn nil, ufmt.Errorf(\"target node is not object type. got: %s\", n.Type().String())\n\t}\n\n\tvalue, ok := n.next[key]\n\tif !ok {\n\t\treturn nil, ufmt.Errorf(\"key not found: %s\", key)\n\t}\n\n\treturn value, nil\n}\n\n// MustKey returns the value of the given key from the current object node.\nfunc (n *Node) MustKey(key string) *Node {\n\tval, err := n.GetKey(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// UniqueKeyLists traverses the current JSON nodes and collects all the unique keys.\nfunc (n *Node) UniqueKeyLists() []string {\n\tvar collectKeys func(*Node) []string\n\tcollectKeys = func(node *Node) []string {\n\t\tif node == nil || !node.IsObject() {\n\t\t\treturn nil\n\t\t}\n\n\t\tresult := make(map[string]bool)\n\t\tfor key, childNode := range node.next {\n\t\t\tresult[key] = true\n\t\t\tchildKeys := collectKeys(childNode)\n\t\t\tfor _, childKey := range childKeys {\n\t\t\t\tresult[childKey] = true\n\t\t\t}\n\t\t}\n\n\t\tkeys := make([]string, 0, len(result))\n\t\tfor key := range result {\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\treturn keys\n\t}\n\n\treturn collectKeys(n)\n}\n\n// Empty returns true if the current node is empty.\nfunc (n *Node) Empty() bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\treturn len(n.next) == 0\n}\n\n// Type returns the type (ValueType) of the current node.\nfunc (n *Node) Type() ValueType {\n\treturn n.nodeType\n}\n\n// Value returns the value of the current node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tval, err := root.MustKey(\"key\").Value()\n//\tif err != nil {\n//\t\tt.Errorf(\"Value returns error: %v\", err)\n//\t}\n//\n//\tresult: \"value\"\nfunc (n *Node) Value() (value interface{}, err error) {\n\tvalue = n.load()\n\n\tif value == nil {\n\t\tswitch n.nodeType {\n\t\tcase Null:\n\t\t\treturn nil, nil\n\n\t\tcase Number:\n\t\t\tvalue, err = strconv.ParseFloat(string(n.source()), 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase String:\n\t\t\tvar ok bool\n\t\t\tvalue, ok = Unquote(n.source(), doubleQuote)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", errInvalidStringValue\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase Boolean:\n\t\t\tif len(n.source()) == 0 {\n\t\t\t\treturn nil, errEmptyBooleanNode\n\t\t\t}\n\n\t\t\tb := n.source()[0]\n\t\t\tvalue = b == 't' || b == 'T'\n\t\t\tn.value = value\n\n\t\tcase Array:\n\t\t\telems := make([]*Node, len(n.next))\n\n\t\t\tfor _, e := range n.next {\n\t\t\t\telems[*e.index] = e\n\t\t\t}\n\n\t\t\tvalue = elems\n\t\t\tn.value = value\n\n\t\tcase Object:\n\t\t\tobj := make(map[string]*Node, len(n.next))\n\n\t\t\tfor k, v := range n.next {\n\t\t\t\tobj[k] = v\n\t\t\t}\n\n\t\t\tvalue = obj\n\t\t\tn.value = value\n\t\t}\n\t}\n\n\treturn value, nil\n}\n\n// Delete removes the current node from the parent node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err := root.MustKey(\"key\").Delete(); err != nil {\n//\t\tt.Errorf(\"Delete returns error: %v\", err)\n//\t}\n//\n//\tresult: {} (empty object)\nfunc (n *Node) Delete() error {\n\tif n == nil {\n\t\treturn errors.New(\"can't delete nil node\")\n\t}\n\n\tif n.prev == nil {\n\t\treturn nil\n\t}\n\n\treturn n.prev.remove(n)\n}\n\n// Size returns the size (length) of the current array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.Size() != 2 {\n//\t\tt.Errorf(\"ArrayNode returns wrong size: %d\", root.Size())\n//\t}\nfunc (n *Node) Size() int {\n\tif n == nil {\n\t\treturn 0\n\t}\n\n\treturn len(n.next)\n}\n\n// Index returns the index of the current node in the parent array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.MustIndex(1).Index() != 1 {\n//\t\tt.Errorf(\"Index returns wrong index: %d\", root.MustIndex(1).Index())\n//\t}\n//\n// We can also use the index to the byte slice of the JSON data directly.\n//\n// Example:\n//\n//\troot := Unmarshal([]byte(`[\"foo\", 1]`))\n//\tif root == nil {\n//\t\tt.Errorf(\"Unmarshal returns nil\")\n//\t}\n//\n//\tif string(root.MustIndex(1).source()) != \"1\" {\n//\t\tt.Errorf(\"source returns wrong result: %s\", root.MustIndex(1).source())\n//\t}\nfunc (n *Node) Index() int {\n\tif n == nil || n.index == nil {\n\t\treturn -1\n\t}\n\n\treturn *n.index\n}\n\n// MustIndex returns the array element at the given index.\n//\n// If the index is negative, it returns the index is from the end of the array.\n// Also, it panics if the index is not found.\n//\n// check the Index method for detailed usage.\nfunc (n *Node) MustIndex(expectIdx int) *Node {\n\tval, err := n.GetIndex(expectIdx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// GetIndex returns the array element at the given index.\n//\n// if the index is negative, it returns the index is from the end of the array.\nfunc (n *Node) GetIndex(idx int) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsArray() {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tif idx \u003e n.Size() {\n\t\treturn nil, errors.New(\"input index exceeds the array size\")\n\t}\n\n\tif idx \u003c 0 {\n\t\tidx += len(n.next)\n\t}\n\n\tchild, ok := n.next[strconv.Itoa(idx)]\n\tif !ok {\n\t\treturn nil, errIndexNotFound\n\t}\n\n\treturn child, nil\n}\n\n// DeleteIndex removes the array element at the given index.\nfunc (n *Node) DeleteIndex(idx int) error {\n\tnode, err := n.GetIndex(idx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn n.remove(node)\n}\n\n// NullNode creates a new null type node.\n//\n// Usage:\n//\n//\t_ := NullNode(\"\")\nfunc NullNode(key string) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: nil,\n\t\tnodeType: Null,\n\t\tmodified: true,\n\t}\n}\n\n// NumberNode creates a new number type node.\n//\n// Usage:\n//\n//\troot := NumberNode(\"\", 1)\n//\tif root == nil {\n//\t\tt.Errorf(\"NumberNode returns nil\")\n//\t}\nfunc NumberNode(key string, value float64) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: Number,\n\t\tmodified: true,\n\t}\n}\n\n// StringNode creates a new string type node.\n//\n// Usage:\n//\n//\troot := StringNode(\"\", \"foo\")\n//\tif root == nil {\n//\t\tt.Errorf(\"StringNode returns nil\")\n//\t}\nfunc StringNode(key string, value string) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: String,\n\t\tmodified: true,\n\t}\n}\n\n// BoolNode creates a new given boolean value node.\n//\n// Usage:\n//\n//\troot := BoolNode(\"\", true)\n//\tif root == nil {\n//\t\tt.Errorf(\"BoolNode returns nil\")\n//\t}\nfunc BoolNode(key string, value bool) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: Boolean,\n\t\tmodified: true,\n\t}\n}\n\n// ArrayNode creates a new array type node.\n//\n// If the given value is nil, it creates an empty array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\nfunc ArrayNode(key string, value []*Node) *Node {\n\tcurr := \u0026Node{\n\t\tkey: \u0026key,\n\t\tnodeType: Array,\n\t\tmodified: true,\n\t}\n\n\tcurr.next = make(map[string]*Node, len(value))\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor i, v := range value {\n\t\t\tidx := i\n\t\t\tcurr.next[strconv.Itoa(i)] = v\n\n\t\t\tv.prev = curr\n\t\t\tv.index = \u0026idx\n\t\t}\n\t}\n\n\treturn curr\n}\n\n// ObjectNode creates a new object type node.\n//\n// If the given value is nil, it creates an empty object node.\n//\n// next is a map of key and value pairs of the object.\nfunc ObjectNode(key string, value map[string]*Node) *Node {\n\tcurr := \u0026Node{\n\t\tnodeType: Object,\n\t\tkey: \u0026key,\n\t\tnext: value,\n\t\tmodified: true,\n\t}\n\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor key, val := range value {\n\t\t\tvkey := key\n\t\t\tval.prev = curr\n\t\t\tval.key = \u0026vkey\n\t\t}\n\t} else {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\treturn curr\n}\n\n// IsArray returns true if the current node is array type.\nfunc (n *Node) IsArray() bool {\n\treturn n.nodeType == Array\n}\n\n// IsObject returns true if the current node is object type.\nfunc (n *Node) IsObject() bool {\n\treturn n.nodeType == Object\n}\n\n// IsNull returns true if the current node is null type.\nfunc (n *Node) IsNull() bool {\n\treturn n.nodeType == Null\n}\n\n// IsBool returns true if the current node is boolean type.\nfunc (n *Node) IsBool() bool {\n\treturn n.nodeType == Boolean\n}\n\n// IsString returns true if the current node is string type.\nfunc (n *Node) IsString() bool {\n\treturn n.nodeType == String\n}\n\n// IsNumber returns true if the current node is number type.\nfunc (n *Node) IsNumber() bool {\n\treturn n.nodeType == Number\n}\n\n// ready checks the current node is ready or not.\n//\n// the meaning of ready is the current node is parsed and has a valid value.\nfunc (n *Node) ready() bool {\n\treturn n.borders[1] != 0\n}\n\n// source returns the source of the current node.\nfunc (n *Node) source() []byte {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified \u0026\u0026 n.data != nil {\n\t\treturn (n.data)[n.borders[0]:n.borders[1]]\n\t}\n\n\treturn nil\n}\n\n// root returns the root node of the current node.\nfunc (n *Node) root() *Node {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tcurr := n\n\tfor curr.prev != nil {\n\t\tcurr = curr.prev\n\t}\n\n\treturn curr\n}\n\n// GetNull returns the null value if current node is null type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"null\"))\n//\tval, err := root.GetNull()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNull returns error: %v\", err)\n//\t}\n//\tif val != nil {\n//\t\tt.Errorf(\"GetNull returns wrong result: %v\", val)\n//\t}\nfunc (n *Node) GetNull() (interface{}, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsNull() {\n\t\treturn nil, errNotNullNode\n\t}\n\n\treturn nil, nil\n}\n\n// MustNull returns the null value if current node is null type.\n//\n// It panics if the current node is not null type.\nfunc (n *Node) MustNull() interface{} {\n\tv, err := n.GetNull()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetNumeric returns the numeric (int/float) value if current node is number type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"10.5\"))\n//\tval, err := root.GetNumeric()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNumeric returns error: %v\", err)\n//\t}\n//\tprintln(val) // 10.5\nfunc (n *Node) GetNumeric() (float64, error) {\n\tif n == nil {\n\t\treturn 0, errNilNode\n\t}\n\n\tif n.nodeType != Number {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tv, ok := val.(float64)\n\tif !ok {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\treturn v, nil\n}\n\n// MustNumeric returns the numeric (int/float) value if current node is number type.\n//\n// It panics if the current node is not number type.\nfunc (n *Node) MustNumeric() float64 {\n\tv, err := n.GetNumeric()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetString returns the string value if current node is string type.\n//\n// Usage:\n//\n//\troot, err := Unmarshal([]byte(\"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n//\t}\n//\n//\tstr, err := root.GetString()\n//\tif err != nil {\n//\t\tt.Errorf(\"should retrieve string value: %s\", err)\n//\t}\n//\n//\tprintln(str) // \"foo\"\nfunc (n *Node) GetString() (string, error) {\n\tif n == nil {\n\t\treturn \"\", errEmptyStringNode\n\t}\n\n\tif !n.IsString() {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tv, ok := val.(string)\n\tif !ok {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\treturn v, nil\n}\n\n// MustString returns the string value if current node is string type.\n//\n// It panics if the current node is not string type.\nfunc (n *Node) MustString() string {\n\tv, err := n.GetString()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetBool returns the boolean value if current node is boolean type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"true\"))\n//\tval, err := root.GetBool()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetBool returns error: %v\", err)\n//\t}\n//\tprintln(val) // true\nfunc (n *Node) GetBool() (bool, error) {\n\tif n == nil {\n\t\treturn false, errNilNode\n\t}\n\n\tif n.nodeType != Boolean {\n\t\treturn false, errNotBoolNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tv, ok := val.(bool)\n\tif !ok {\n\t\treturn false, errNotBoolNode\n\t}\n\n\treturn v, nil\n}\n\n// MustBool returns the boolean value if current node is boolean type.\n//\n// It panics if the current node is not boolean type.\nfunc (n *Node) MustBool() bool {\n\tv, err := n.GetBool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetArray returns the array value if current node is array type.\n//\n// Usage:\n//\n//\t\troot := Must(Unmarshal([]byte(`[\"foo\", 1]`)))\n//\t\tarr, err := root.GetArray()\n//\t\tif err != nil {\n//\t\t\tt.Errorf(\"GetArray returns error: %v\", err)\n//\t\t}\n//\n//\t\tfor _, val := range arr {\n//\t\t\tprintln(val)\n//\t\t}\n//\n//\t result: \"foo\", 1\nfunc (n *Node) GetArray() ([]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.nodeType != Array {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.([]*Node)\n\tif !ok {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\treturn v, nil\n}\n\n// MustArray returns the array value if current node is array type.\n//\n// It panics if the current node is not array type.\nfunc (n *Node) MustArray() []*Node {\n\tv, err := n.GetArray()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendArray appends the given values to the current array node.\n//\n// If the current node is not array type, it returns an error.\n//\n// Example 1:\n//\n//\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n//\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n//\t\tt.Errorf(\"should not return error: %s\", err)\n//\t}\n//\n//\tresult: [{\"foo\":\"bar\"}, null]\n//\n// Example 2:\n//\n//\troot := Must(Unmarshal([]byte(`[\"bar\", \"baz\"]`)))\n//\terr := root.AppendArray(NumberNode(\"\", 1), StringNode(\"\", \"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n//\t }\n//\n//\tresult: [\"bar\", \"baz\", 1, \"foo\"]\nfunc (n *Node) AppendArray(value ...*Node) error {\n\tif !n.IsArray() {\n\t\treturn errInvalidAppend\n\t}\n\n\tfor _, val := range value {\n\t\tif err := n.append(nil, val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ArrayEach executes the callback for each element in the JSON array.\n//\n// Usage:\n//\n//\tjsonArrayNode.ArrayEach(func(i int, valueNode *Node) {\n//\t ufmt.Println(i, valueNode)\n//\t})\nfunc (n *Node) ArrayEach(callback func(i int, target *Node)) {\n\tif n == nil || !n.IsArray() {\n\t\treturn\n\t}\n\n\tfor idx := 0; idx \u003c len(n.next); idx++ {\n\t\telement, err := n.GetIndex(idx)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcallback(idx, element)\n\t}\n}\n\n// GetObject returns the object value if current node is object type.\n//\n// Usage:\n//\n//\troot := Must(Unmarshal([]byte(`{\"key\": \"value\"}`)))\n//\tobj, err := root.GetObject()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetObject returns error: %v\", err)\n//\t}\n//\n//\tresult: map[string]*Node{\"key\": StringNode(\"key\", \"value\")}\nfunc (n *Node) GetObject() (map[string]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsObject() {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.(map[string]*Node)\n\tif !ok {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\treturn v, nil\n}\n\n// MustObject returns the object value if current node is object type.\n//\n// It panics if the current node is not object type.\nfunc (n *Node) MustObject() map[string]*Node {\n\tv, err := n.GetObject()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendObject appends the given key and value to the current object node.\n//\n// If the current node is not object type, it returns an error.\nfunc (n *Node) AppendObject(key string, value *Node) error {\n\tif !n.IsObject() {\n\t\treturn errInvalidAppend\n\t}\n\n\tif err := n.append(\u0026key, value); err != nil {\n\t\treturn err\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ObjectEach executes the callback for each key-value pair in the JSON object.\n//\n// Usage:\n//\n//\tjsonObjectNode.ObjectEach(func(key string, valueNode *Node) {\n//\t ufmt.Println(key, valueNode)\n//\t})\nfunc (n *Node) ObjectEach(callback func(key string, value *Node)) {\n\tif n == nil || !n.IsObject() {\n\t\treturn\n\t}\n\n\tfor key, child := range n.next {\n\t\tcallback(key, child)\n\t}\n}\n\n// String converts the node to a string representation.\nfunc (n *Node) String() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified {\n\t\treturn string(n.source())\n\t}\n\n\tval, err := Marshal(n)\n\tif err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\n\treturn string(val)\n}\n\n// Path builds the path of the current node.\n//\n// For example:\n//\n//\t{ \"key\": { \"sub\": [ \"val1\", \"val2\" ] }}\n//\n// The path of \"val2\" is: $.key.sub[1]\nfunc (n *Node) Path() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tif n.prev == nil {\n\t\tsb.WriteString(\"$\")\n\t} else {\n\t\tsb.WriteString(n.prev.Path())\n\n\t\tif n.key != nil {\n\t\t\tsb.WriteString(\"['\" + n.Key() + \"']\")\n\t\t} else {\n\t\t\tsb.WriteString(\"[\" + strconv.Itoa(n.Index()) + \"]\")\n\t\t}\n\t}\n\n\treturn sb.String()\n}\n\n// mark marks the current node as modified.\nfunc (n *Node) mark() {\n\tnode := n\n\tfor node != nil \u0026\u0026 !node.modified {\n\t\tnode.modified = true\n\t\tnode = node.prev\n\t}\n}\n\n// isContainer checks the current node type is array or object.\nfunc (n *Node) isContainer() bool {\n\treturn n.IsArray() || n.IsObject()\n}\n\n// remove removes the value from the current container type node.\nfunc (n *Node) remove(v *Node) error {\n\tif !n.isContainer() {\n\t\treturn ufmt.Errorf(\n\t\t\t\"can't remove value from non-array or non-object node. got=%s\",\n\t\t\tn.Type().String(),\n\t\t)\n\t}\n\n\tif v.prev != n {\n\t\treturn errors.New(\"invalid parent node\")\n\t}\n\n\tn.mark()\n\tif n.IsArray() {\n\t\tdelete(n.next, strconv.Itoa(*v.index))\n\t\tn.dropIndex(*v.index)\n\t} else {\n\t\tdelete(n.next, *v.key)\n\t}\n\n\tv.prev = nil\n\treturn nil\n}\n\n// dropIndex rebase the index of current array node values.\nfunc (n *Node) dropIndex(idx int) {\n\tfor i := idx + 1; i \u003c= len(n.next); i++ {\n\t\tprv := i - 1\n\t\tif curr, ok := n.next[strconv.Itoa(i)]; ok {\n\t\t\tcurr.index = \u0026prv\n\t\t\tn.next[strconv.Itoa(prv)] = curr\n\t\t}\n\n\t\tdelete(n.next, strconv.Itoa(i))\n\t}\n}\n\n// append is a helper function to append the given value to the current container type node.\nfunc (n *Node) append(key *string, val *Node) error {\n\tif n.isSameOrParentNode(val) {\n\t\treturn errInvalidAppendCycle\n\t}\n\n\tif val.prev != nil {\n\t\tif err := val.prev.remove(val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tval.prev = n\n\tval.key = key\n\n\tif key == nil {\n\t\tsize := len(n.next)\n\t\tval.index = \u0026size\n\t\tn.next[strconv.Itoa(size)] = val\n\t} else {\n\t\tif old, ok := n.next[*key]; ok {\n\t\t\tif err := n.remove(old); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tn.next[*key] = val\n\t}\n\n\treturn nil\n}\n\nfunc (n *Node) isSameOrParentNode(nd *Node) bool {\n\treturn n == nd || n.isParentNode(nd)\n}\n\nfunc (n *Node) isParentNode(nd *Node) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\tfor curr := nd.prev; curr != nil; curr = curr.prev {\n\t\tif curr == n {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// cptrs returns the pointer of the given string value.\nfunc cptrs(cpy *string) *string {\n\tif cpy == nil {\n\t\treturn nil\n\t}\n\n\tval := *cpy\n\n\treturn \u0026val\n}\n\n// cptri returns the pointer of the given integer value.\nfunc cptri(i *int) *int {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\tval := *i\n\treturn \u0026val\n}\n\n// Must panics if the given node is not fulfilled the expectation.\n// Usage:\n//\n//\tnode := Must(Unmarshal([]byte(`{\"key\": \"value\"}`))\nfunc Must(root *Node, expect error) *Node {\n\tif expect != nil {\n\t\tpanic(expect)\n\t}\n\n\treturn root\n}\n"},{"name":"node_test.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tnilKey *string\n\tdummyKey = \"key\"\n)\n\ntype _args struct {\n\tprev *Node\n\tbuf *buffer\n\ttyp ValueType\n\tkey **string\n}\n\ntype simpleNode struct {\n\tname string\n\tnode *Node\n}\n\nfunc TestNode_CreateNewNode(t *testing.T) {\n\trel := \u0026dummyKey\n\n\ttests := []struct {\n\t\tname string\n\t\targs _args\n\t\texpectCurr *Node\n\t\texpectErr bool\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\tname: \"child for non container type\",\n\t\t\targs: _args{\n\t\t\t\tprev: BoolNode(\"\", true),\n\t\t\t\tbuf: newBuffer(make([]byte, 10)),\n\t\t\t\ttyp: Boolean,\n\t\t\t\tkey: \u0026rel,\n\t\t\t},\n\t\t\texpectCurr: nil,\n\t\t\texpectErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif tt.expectPanic {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tt.Errorf(\"%s panic occurred when not expected: %v\", tt.name, r)\n\t\t\t\t} else if tt.expectPanic {\n\t\t\t\t\tt.Errorf(\"%s expected panic but didn't occur\", tt.name)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tgot, err := NewNode(tt.args.prev, tt.args.buf, tt.args.typ, tt.args.key)\n\t\t\tif (err != nil) != tt.expectErr {\n\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.expectErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expectErr {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !compareNodes(got, tt.expectCurr) {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expectCurr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Value(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\t_type ValueType\n\t\texpected interface{}\n\t\terrExpected bool\n\t}{\n\t\t{name: \"null\", data: []byte(\"null\"), _type: Null, expected: nil},\n\t\t{name: \"1\", data: []byte(\"1\"), _type: Number, expected: float64(1)},\n\t\t{name: \".1\", data: []byte(\".1\"), _type: Number, expected: float64(.1)},\n\t\t{name: \"-.1e1\", data: []byte(\"-.1e1\"), _type: Number, expected: float64(-1)},\n\t\t{name: \"string\", data: []byte(\"\\\"foo\\\"\"), _type: String, expected: \"foo\"},\n\t\t{name: \"space\", data: []byte(\"\\\"foo bar\\\"\"), _type: String, expected: \"foo bar\"},\n\t\t{name: \"true\", data: []byte(\"true\"), _type: Boolean, expected: true},\n\t\t{name: \"invalid true\", data: []byte(\"tru\"), _type: Unknown, errExpected: true},\n\t\t{name: \"invalid false\", data: []byte(\"fals\"), _type: Unknown, errExpected: true},\n\t\t{name: \"false\", data: []byte(\"false\"), _type: Boolean, expected: false},\n\t\t{name: \"e1\", data: []byte(\"e1\"), _type: Unknown, errExpected: true},\n\t\t{name: \"1a\", data: []byte(\"1a\"), _type: Unknown, errExpected: true},\n\t\t{name: \"string error\", data: []byte(\"\\\"foo\\nbar\\\"\"), _type: String, errExpected: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcurr := \u0026Node{\n\t\t\t\tdata: tt.data,\n\t\t\t\tnodeType: tt._type,\n\t\t\t\tborders: [2]int{0, len(tt.data)},\n\t\t\t}\n\n\t\t\tgot, err := curr.Value()\n\t\t\tif err != nil {\n\t\t\t\tif !tt.errExpected {\n\t\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.errExpected)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Delete(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{\"foo\":\"bar\"}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tfoo := root.MustKey(\"foo\")\n\tif err := foo.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error while handling foo: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif value, err := Marshal(foo); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `\"bar\"` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif foo.prev != nil {\n\t\tt.Errorf(\"foo.prev should be nil\")\n\t}\n}\n\nfunc TestNode_ObjectNode(t *testing.T) {\n\tobjs := map[string]*Node{\n\t\t\"key1\": NullNode(\"null\"),\n\t\t\"key2\": NumberNode(\"answer\", 42),\n\t\t\"key3\": StringNode(\"string\", \"foobar\"),\n\t\t\"key4\": BoolNode(\"bool\", true),\n\t}\n\n\tnode := ObjectNode(\"test\", objs)\n\n\tif len(node.next) != len(objs) {\n\t\tt.Errorf(\"ObjectNode: want %v got %v\", len(objs), len(node.next))\n\t}\n\n\tfor k, v := range objs {\n\t\tif node.next[k] == nil {\n\t\t\tt.Errorf(\"ObjectNode: want %v got %v\", v, node.next[k])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendObject(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`{\"foo\":\"bar\",\"baz\":null}`))).AppendObject(\"biz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendArray should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.AppendObject(\"baz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendObject should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if isSameObject(string(value), `\"{\"foo\":\"bar\",\"baz\":null}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif err := root.AppendObject(\"biz\", NumberNode(\"\", 42)); err != nil {\n\t\tt.Errorf(\"AppendObject returns error: %v\", err)\n\t}\n\n\tval, err := Marshal(root)\n\tif err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif isSameObject(string(val), `\"{\"foo\":\"bar\",\"baz\":null,\"biz\":42}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(val))\n\t}\n}\n\nfunc TestNode_ArrayNode(t *testing.T) {\n\tarr := []*Node{\n\t\tNullNode(\"nil\"),\n\t\tNumberNode(\"num\", 42),\n\t\tStringNode(\"str\", \"foobar\"),\n\t\tBoolNode(\"bool\", true),\n\t}\n\n\tnode := ArrayNode(\"test\", arr)\n\n\tif len(node.next) != len(arr) {\n\t\tt.Errorf(\"ArrayNode: want %v got %v\", len(arr), len(node.next))\n\t}\n\n\tfor i, v := range arr {\n\t\tif node.next[strconv.Itoa(i)] == nil {\n\t\t\tt.Errorf(\"ArrayNode: want %v got %v\", v, node.next[strconv.Itoa(i)])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendArray(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`))).AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif err := root.AppendArray(\n\t\tNumberNode(\"\", 1),\n\t\tStringNode(\"\", \"foo\"),\n\t\tMust(Unmarshal([]byte(`[0,1,null,true,\"example\"]`))),\n\t\tMust(Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123}`))),\n\t); err != nil {\n\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null,1,\"foo\",[0,1,null,true,\"example\"],{\"foo\": true, \"bar\": null, \"baz\": 123}]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n}\n\n/******** value getter ********/\n\nfunc TestNode_GetBool(t *testing.T) {\n\troot, err := Unmarshal([]byte(`true`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetBool()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetBool(): %s\", err.Error())\n\t}\n\n\tif !value {\n\t\tt.Errorf(\"root.GetBool() is corrupted\")\n\t}\n}\n\nfunc TestNode_GetBool_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"literally null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetBool(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"true\", BoolNode(\"\", true)},\n\t\t{\"false\", BoolNode(\"\", false)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif !tt.node.IsBool() {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool_With_Unmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson []byte\n\t\twant bool\n\t}{\n\t\t{\"true\", []byte(\"true\"), true},\n\t\t{\"false\", []byte(\"false\"), true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.json)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\t\t}\n\n\t\t\tif root.IsBool() != tt.want {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar nullJson = []byte(`null`)\n\nfunc TestNode_GetNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue, err := root.GetNull()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting null, %s\", err)\n\t}\n\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNull_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"number node is null\", NumberNode(\"\", 42)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNull(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue := root.MustNull()\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNumeric_Float(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123.456`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123.456) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123.456, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_Scientific_Notation(t *testing.T) {\n\troot, err := Unmarshal([]byte(`1e3`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(1000) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 1000, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_With_Unmarshal(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"string node\", StringNode(\"\", \"123\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNumeric(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetString(t *testing.T) {\n\troot, err := Unmarshal([]byte(`\"123foobar 3456\"`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t}\n\n\tvalue, err := root.GetString()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetString(): %s\", err)\n\t}\n\n\tif value != \"123foobar 3456\" {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %s\", value))\n\t}\n}\n\nfunc TestNode_GetString_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetString(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t}{\n\t\t{\"foo\", []byte(`\"foo\"`)},\n\t\t{\"foo bar\", []byte(`\"foo bar\"`)},\n\t\t{\"\", []byte(`\"\"`)},\n\t\t{\"안녕하세요\", []byte(`\"안녕하세요\"`)},\n\t\t{\"こんにちは\", []byte(`\"こんにちは\"`)},\n\t\t{\"你好\", []byte(`\"你好\"`)},\n\t\t{\"one \\\"encoded\\\" string\", []byte(`\"one \\\"encoded\\\" string\"`)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\t\t}\n\n\t\t\tvalue := root.MustString()\n\t\t\tif value != tt.name {\n\t\t\t\tt.Errorf(\"value is not matched. expected: %s, got: %s\", tt.name, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_Array(t *testing.T) {\n\troot, err := Unmarshal([]byte(\" [1,[\\\"1\\\",[1,[1,2,3]]]]\\r\\n\"))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t}\n\n\tif root == nil {\n\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(\"Error on Unmarshal: wrong type\")\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err)\n\t} else if len(array) != 2 {\n\t\tt.Errorf(\"expected 2 elements, got %d\", len(array))\n\t} else if val, err := array[0].GetNumeric(); err != nil {\n\t\tt.Errorf(\"value of array[0] is not numeric. got: %v\", array[0].value)\n\t} else if val != 1 {\n\t\tt.Errorf(\"Error on array[0].GetNumeric(): expected to be '1', got: %v\", val)\n\t} else if val, err := array[1].GetArray(); err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err.Error())\n\t} else if len(val) != 2 {\n\t\tt.Errorf(\"Error on array[1].GetArray(): expected 2 elements, got %d\", len(val))\n\t} else if el, err := val[0].GetString(); err != nil {\n\t\tt.Errorf(\"error occurred while getting string, %s\", err.Error())\n\t} else if el != \"1\" {\n\t\tt.Errorf(\"Error on val[0].GetString(): expected to be '1', got: %s\", el)\n\t}\n}\n\nvar sampleArr = []byte(`[-1, 2, 3, 4, 5, 6]`)\n\nfunc TestNode_GetArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetArray(): %s\", err)\n\t}\n\n\tif len(array) != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"length is not matched. expected: 3, got: %d\", len(array)))\n\t}\n\n\tfor i, node := range array {\n\t\tfor j, val := range []int{-1, 2, 3, 4, 5, 6} {\n\t\t\tif i == j {\n\t\t\t\tif v, err := node.GetNumeric(); err != nil {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"Error on node.GetNumeric(): %s\", err))\n\t\t\t\t} else if v != float64(val) {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: %d, got: %v\", val, v))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNode_GetArray_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetArray(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(ufmt.Sprintf(\"Must be an array. got: %s\", root.Type().String()))\n\t}\n}\n\nfunc TestNode_ArrayEach(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected []int\n\t}{\n\t\t{\n\t\t\tname: \"empty array\",\n\t\t\tjson: `[]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"single element\",\n\t\t\tjson: `[42]`,\n\t\t\texpected: []int{42},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements\",\n\t\t\tjson: `[1, 2, 3, 4, 5]`,\n\t\t\texpected: []int{1, 2, 3, 4, 5},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements but all values are same\",\n\t\t\tjson: `[1, 1, 1, 1, 1]`,\n\t\t\texpected: []int{1, 1, 1, 1, 1},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements with non-numeric values\",\n\t\t\tjson: `[\"a\", \"b\", \"c\", \"d\", \"e\"]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"non-array node\",\n\t\t\tjson: `{\"not\": \"an array\"}`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"array containing numeric and non-numeric elements\",\n\t\t\tjson: `[\"1\", 2, 3, \"4\", 5, \"6\"]`,\n\t\t\texpected: []int{2, 3, 5},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tvar result []int // callback result\n\t\t\troot.ArrayEach(func(index int, element *Node) {\n\t\t\t\tif val, err := strconv.Atoi(element.String()); err == nil {\n\t\t\t\t\tresult = append(result, val)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d elements, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor i, val := range result {\n\t\t\t\tif val != tc.expected[i] {\n\t\t\t\t\tt.Errorf(\"%s: expected value at index %d to be %d, got %d\", tc.name, i, tc.expected[i], val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Key(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123, \"biz\": [1,2,3]}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tobj := root.MustObject()\n\tfor key, node := range obj {\n\t\tif key != node.Key() {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", node.Key(), key)\n\t\t}\n\t}\n\n\tkeys := []string{\"foo\", \"bar\", \"baz\", \"biz\"}\n\tfor _, key := range keys {\n\t\tif obj[key].Key() != key {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", obj[key].Key(), key)\n\t\t}\n\t}\n\n\t// TODO: resolve stack overflow\n\t// if root.MustKey(\"foo\").Clone().Key() != \"\" {\n\t// \tt.Errorf(\"wrong key found for cloned key\")\n\t// }\n\n\tif (*Node)(nil).Key() != \"\" {\n\t\tt.Errorf(\"wrong key found for nil node\")\n\t}\n}\n\nfunc TestNode_Size(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tsize := root.Size()\n\tif size != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 6. got: %v\", size))\n\t}\n\n\tif (*Node)(nil).Size() != 0 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 0. got: %v\", (*Node)(nil).Size()))\n\t}\n}\n\nfunc TestNode_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tarr := root.MustArray()\n\tfor i, node := range arr {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_Index_Fail(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant int\n\t}{\n\t\t{\"nil node\", (*Node)(nil), -1},\n\t\t{\"null node\", NullNode(\"\"), -1},\n\t\t{\"object node\", ObjectNode(\"\", nil), -1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Index(); got != tt.want {\n\t\t\t\tt.Errorf(\"Index() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetIndex(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`)))\n\texpected := []int{1, 2, 3, 4, 5, 6}\n\n\tif len(expected) != root.Size() {\n\t\tt.Errorf(\"length is not matched. expected: %d, got: %d\", len(expected), root.Size())\n\t}\n\n\t// TODO: if length exceeds, stack overflow occurs. need to fix\n\tfor i, v := range expected {\n\t\tval, err := root.GetIndex(i)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"error occurred while getting index %d, %s\", i, err)\n\t\t}\n\n\t\tif val.MustNumeric() != float64(v) {\n\t\t\tt.Errorf(\"value is not matched. expected: %d, got: %v\", v, val.MustNumeric())\n\t\t}\n\t}\n}\n\nfunc TestNode_GetIndex_InputIndex_Exceed_Original_Node_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\t_, err = root.GetIndex(10)\n\tif err == nil {\n\t\tt.Errorf(\"GetIndex should return error\")\n\t}\n}\n\nfunc TestNode_DeleteIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\texpected string\n\t\tindex int\n\t\tok bool\n\t}{\n\t\t{`null`, ``, 0, false},\n\t\t{`1`, ``, 0, false},\n\t\t{`{}`, ``, 0, false},\n\t\t{`{\"foo\":\"bar\"}`, ``, 0, false},\n\t\t{`true`, ``, 0, false},\n\t\t{`[]`, ``, 0, false},\n\t\t{`[]`, ``, -1, false},\n\t\t{`[1]`, `[]`, 0, true},\n\t\t{`[{}]`, `[]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, -1, true},\n\t\t{`[{}, [], 42]`, `[[], 42]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, 42]`, 1, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, 2, true},\n\t\t{`[{}, [], 42]`, ``, 10, false},\n\t\t{`[{}, [], 42]`, ``, -10, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot := Must(Unmarshal([]byte(tt.name)))\n\t\t\terr := root.DeleteIndex(tt.index)\n\t\t\tif err != nil \u0026\u0026 tt.ok {\n\t\t\t\tt.Errorf(\"DeleteIndex returns error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetKey(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tvalue, err := root.GetKey(\"foo\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\tif value.MustBool() != true {\n\t\tt.Errorf(\"value is not matched. expected: true, got: %v\", value.MustBool())\n\t}\n\n\tvalue, err = root.GetKey(\"bar\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\t_, err = root.GetKey(\"baz\")\n\tif err == nil {\n\t\tt.Errorf(\"key baz is not exist. must be failed\")\n\t}\n\n\tif value.MustNull() != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value.MustNull())\n\t}\n}\n\nfunc TestNode_GetKey_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetKey(\"\"); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetUniqueKeyList(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"simple foo/bar\",\n\t\t\tjson: `{\"foo\": true, \"bar\": null}`,\n\t\t\texpected: []string{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tjson: `{}`,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tjson: `{\n\t\t\t\t\"outer\": {\n\t\t\t\t\t\"inner\": {\n\t\t\t\t\t\t\"key\": \"value\"\n\t\t\t\t\t},\n\t\t\t\t\t\"array\": [1, 2, 3]\n\t\t\t\t},\n\t\t\t\t\"another\": \"item\"\n\t\t\t}`,\n\t\t\texpected: []string{\"outer\", \"inner\", \"key\", \"array\", \"another\"},\n\t\t},\n\t\t{\n\t\t\tname: \"complex object\",\n\t\t\tjson: `{\n\t\t\t\t\"Image\": {\n\t\t\t\t\t\"Width\": 800,\n\t\t\t\t\t\"Height\": 600,\n\t\t\t\t\t\"Title\": \"View from 15th Floor\",\n\t\t\t\t\t\"Thumbnail\": {\n\t\t\t\t\t\t\"Url\": \"http://www.example.com/image/481989943\",\n\t\t\t\t\t\t\"Height\": 125,\n\t\t\t\t\t\t\"Width\": 100\n\t\t\t\t\t},\n\t\t\t\t\t\"Animated\": false,\n\t\t\t\t\t\"IDs\": [116, 943, 234, 38793]\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpected: []string{\"Image\", \"Width\", \"Height\", \"Title\", \"Thumbnail\", \"Url\", \"Animated\", \"IDs\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tt.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error occurred while unmarshal\")\n\t\t\t}\n\n\t\t\tvalue := root.UniqueKeyLists()\n\t\t\tif len(value) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"%s length must be %v. got: %v. retrieved keys: %s\", tt.name, len(tt.expected), len(value), value)\n\t\t\t}\n\n\t\t\tfor _, key := range value {\n\t\t\t\tif !contains(tt.expected, key) {\n\t\t\t\t\tt.Errorf(\"EachKey() must be in %v. got: %v\", tt.expected, key)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TODO: resolve stack overflow\nfunc TestNode_IsEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\texpected bool\n\t}{\n\t\t{\"nil node\", (*Node)(nil), false}, // nil node is not empty.\n\t\t// {\"null node\", NullNode(\"\"), true},\n\t\t{\"empty object\", ObjectNode(\"\", nil), true},\n\t\t{\"empty array\", ArrayNode(\"\", nil), true},\n\t\t{\"non-empty object\", ObjectNode(\"\", map[string]*Node{\"foo\": BoolNode(\"foo\", true)}), false},\n\t\t{\"non-empty array\", ArrayNode(\"\", []*Node{BoolNode(\"0\", true)}), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Empty(); got != tt.expected {\n\t\t\t\tt.Errorf(\"%s = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Index_EmptyList(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tarray := root.MustArray()\n\tfor i, node := range array {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_GetObject(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true,\"bar\": null}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetObject()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetObject(): %s\", err.Error())\n\t}\n\n\tif _, ok := value[\"foo\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: foo\")\n\t}\n\n\tif _, ok := value[\"bar\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: bar\")\n\t}\n}\n\nfunc TestNode_GetObject_Fail(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"get object from null node\", NullNode(\"\")},\n\t\t{\"not object node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetObject(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ObjectEach(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected map[string]int\n\t}{\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tjson: `{}`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t\t{\n\t\t\tname: \"single key-value pair\",\n\t\t\tjson: `{\"key\": 42}`,\n\t\t\texpected: map[string]int{\"key\": 42},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple key-value pairs\",\n\t\t\tjson: `{\"one\": 1, \"two\": 2, \"three\": 3}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"two\": 2, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple key-value pairs with some non-numeric values\",\n\t\t\tjson: `{\"one\": 1, \"two\": \"2\", \"three\": 3, \"four\": \"4\"}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname: \"non-object node\",\n\t\t\tjson: `[\"not\", \"an\", \"object\"]`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tresult := make(map[string]int)\n\t\t\troot.ObjectEach(func(key string, value *Node) {\n\t\t\t\t// extract integer values from the object\n\t\t\t\tif val, err := strconv.Atoi(value.String()); err == nil {\n\t\t\t\t\tresult[key] = val\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d key-value pairs, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor key, val := range tc.expected {\n\t\t\t\tif result[key] != val {\n\t\t\t\t\tt.Errorf(\"%s: expected value for key %s to be %d, got %d\", tc.name, key, val, result[key])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ExampleMust(t *testing.T) {\n\tdata := []byte(`{\n \"Image\": {\n \"Width\": 800,\n \"Height\": 600,\n \"Title\": \"View from 15th Floor\",\n \"Thumbnail\": {\n \"Url\": \"http://www.example.com/image/481989943\",\n \"Height\": 125,\n \"Width\": 100\n },\n \"Animated\" : false,\n \"IDs\": [116, 943, 234, 38793]\n }\n }`)\n\n\troot := Must(Unmarshal(data))\n\tif root.Size() != 1 {\n\t\tt.Errorf(\"root.Size() must be 1. got: %v\", root.Size())\n\t}\n\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n\t// Output:\n\t// Object has 1 inheritors inside\n}\n\n// Calculate AVG price from different types of objects, JSON from: https://goessner.net/articles/JsonPath/index.html#e3\nfunc TestExampleUnmarshal(t *testing.T) {\n\tdata := []byte(`{ \"store\": {\n \"book\": [ \n { \"category\": \"reference\",\n \"author\": \"Nigel Rees\",\n \"title\": \"Sayings of the Century\",\n \"price\": 8.95\n },\n { \"category\": \"fiction\",\n \"author\": \"Evelyn Waugh\",\n \"title\": \"Sword of Honour\",\n \"price\": 12.99\n },\n { \"category\": \"fiction\",\n \"author\": \"Herman Melville\",\n \"title\": \"Moby Dick\",\n \"isbn\": \"0-553-21311-3\",\n \"price\": 8.99\n },\n { \"category\": \"fiction\",\n \"author\": \"J. R. R. Tolkien\",\n \"title\": \"The Lord of the Rings\",\n \"isbn\": \"0-395-19395-8\",\n \"price\": 22.99\n }\n ],\n \"bicycle\": { \"color\": \"red\",\n \"price\": 19.95\n },\n \"tools\": null\n }\n}`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred when unmarshal\")\n\t}\n\n\tstore := root.MustKey(\"store\").MustObject()\n\n\tvar prices float64\n\tsize := 0\n\tfor _, objects := range store {\n\t\tif objects.IsArray() \u0026\u0026 objects.Size() \u003e 0 {\n\t\t\tsize += objects.Size()\n\t\t\tfor _, object := range objects.MustArray() {\n\t\t\t\tprices += object.MustKey(\"price\").MustNumeric()\n\t\t\t}\n\t\t} else if objects.IsObject() \u0026\u0026 objects.HasKey(\"price\") {\n\t\t\tsize++\n\t\t\tprices += objects.MustKey(\"price\").MustNumeric()\n\t\t}\n\t}\n\n\tresult := int(prices / float64(size))\n\tufmt.Sprintf(\"AVG price: %d\", result)\n}\n\nfunc TestNode_ExampleMust_panic(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\tdata := []byte(`{]`)\n\troot := Must(Unmarshal(data))\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n}\n\nfunc TestNode_Path(t *testing.T) {\n\tdata := []byte(`{\n \"Image\": {\n \"Width\": 800,\n \"Height\": 600,\n \"Title\": \"View from 15th Floor\",\n \"Thumbnail\": {\n \"Url\": \"http://www.example.com/image/481989943\",\n \"Height\": 125,\n \"Width\": 100\n },\n \"Animated\" : false,\n \"IDs\": [116, 943, 234, 38793]\n }\n }`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tif root.Path() != \"$\" {\n\t\tt.Errorf(\"Wrong root.Path()\")\n\t}\n\n\telement := root.MustKey(\"Image\").MustKey(\"Thumbnail\").MustKey(\"Url\")\n\tif element.Path() != \"$['Image']['Thumbnail']['Url']\" {\n\t\tt.Errorf(\"Wrong path found: %s\", element.Path())\n\t}\n\n\tif (*Node)(nil).Path() != \"\" {\n\t\tt.Errorf(\"Wrong (nil).Path()\")\n\t}\n}\n\nfunc TestNode_Path2(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"Node with key\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tkey: func() *string { s := \"key\"; return \u0026s }(),\n\t\t\t},\n\t\t\twant: \"$['key']\",\n\t\t},\n\t\t{\n\t\t\tname: \"Node with index\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tindex: func() *int { i := 1; return \u0026i }(),\n\t\t\t},\n\t\t\twant: \"$[1]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Path(); got != tt.want {\n\t\t\t\tt.Errorf(\"Path() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Root(t *testing.T) {\n\troot := \u0026Node{}\n\tchild := \u0026Node{prev: root}\n\tgrandChild := \u0026Node{prev: child}\n\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant *Node\n\t}{\n\t\t{\n\t\t\tname: \"Root node\",\n\t\t\tnode: root,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Child node\",\n\t\t\tnode: child,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Grandchild node\",\n\t\t\tnode: grandChild,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Node is nil\",\n\t\t\tnode: nil,\n\t\t\twant: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.root(); got != tt.want {\n\t\t\t\tt.Errorf(\"root() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc contains(slice []string, item string) bool {\n\tfor _, a := range slice {\n\t\tif a == item {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ignore the sequence of keys by ordering them.\n// need to avoid import encoding/json and reflect package.\n// because gno does not support them for now.\n// TODO: use encoding/json to compare the result after if possible in gno.\nfunc isSameObject(a, b string) bool {\n\taPairs := strings.Split(strings.Trim(a, \"{}\"), \",\")\n\tbPairs := strings.Split(strings.Trim(b, \"{}\"), \",\")\n\n\taMap := make(map[string]string)\n\tbMap := make(map[string]string)\n\tfor _, pair := range aPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\taMap[key] = value\n\t}\n\tfor _, pair := range bPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\tbMap[key] = value\n\t}\n\n\taKeys := make([]string, 0, len(aMap))\n\tbKeys := make([]string, 0, len(bMap))\n\tfor k := range aMap {\n\t\taKeys = append(aKeys, k)\n\t}\n\n\tfor k := range bMap {\n\t\tbKeys = append(bKeys, k)\n\t}\n\n\tsort.Strings(aKeys)\n\tsort.Strings(bKeys)\n\n\tif len(aKeys) != len(bKeys) {\n\t\treturn false\n\t}\n\n\tfor i := range aKeys {\n\t\tif aKeys[i] != bKeys[i] {\n\t\t\treturn false\n\t\t}\n\n\t\tif aMap[aKeys[i]] != bMap[bKeys[i]] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc compareNodes(n1, n2 *Node) bool {\n\tif n1 == nil || n2 == nil {\n\t\treturn n1 == n2\n\t}\n\n\tif n1.key != n2.key {\n\t\treturn false\n\t}\n\n\tif !bytes.Equal(n1.data, n2.data) {\n\t\treturn false\n\t}\n\n\tif n1.index != n2.index {\n\t\treturn false\n\t}\n\n\tif n1.borders != n2.borders {\n\t\treturn false\n\t}\n\n\tif n1.modified != n2.modified {\n\t\treturn false\n\t}\n\n\tif n1.nodeType != n2.nodeType {\n\t\treturn false\n\t}\n\n\tif !compareNodes(n1.prev, n2.prev) {\n\t\treturn false\n\t}\n\n\tif len(n1.next) != len(n2.next) {\n\t\treturn false\n\t}\n\n\tfor k, v := range n1.next {\n\t\tif !compareNodes(v, n2.next[k]) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"},{"name":"parser.gno","body":"package json\n\nimport (\n\t\"bytes\"\n)\n\nconst (\n\tunescapeStackBufSize = 64\n\tabsMinInt64 = 1 \u003c\u003c 63\n\tmaxInt64 = absMinInt64 - 1\n\tmaxUint64 = 1\u003c\u003c64 - 1\n)\n\n// PaseStringLiteral parses a string from the given byte slice.\nfunc ParseStringLiteral(data []byte) (string, error) {\n\tvar buf [unescapeStackBufSize]byte\n\n\tbf, err := Unescape(data, buf[:])\n\tif err != nil {\n\t\treturn \"\", errInvalidStringInput\n\t}\n\n\treturn string(bf), nil\n}\n\n// ParseBoolLiteral parses a boolean value from the given byte slice.\nfunc ParseBoolLiteral(data []byte) (bool, error) {\n\tswitch {\n\tcase bytes.Equal(data, trueLiteral):\n\t\treturn true, nil\n\tcase bytes.Equal(data, falseLiteral):\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, errMalformedBooleanValue\n\t}\n}\n"},{"name":"parser_test.gno","body":"package json\n\nimport \"testing\"\n\nfunc TestParseStringLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t\tisError bool\n\t}{\n\t\t{`\"Hello, World!\"`, \"\\\"Hello, World!\\\"\", false},\n\t\t{`\\uFF11`, \"\\uFF11\", false},\n\t\t{`\\uFFFF`, \"\\uFFFF\", false},\n\t\t{`true`, \"true\", false},\n\t\t{`false`, \"false\", false},\n\t\t{`\\uDF00`, \"\", true},\n\t}\n\n\tfor i, tt := range tests {\n\t\ts, err := ParseStringLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif s != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%s, but actual=%s\", i, tt.expected, s)\n\t\t}\n\t}\n}\n\nfunc TestParseBoolLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected bool\n\t\tisError bool\n\t}{\n\t\t{`true`, true, false},\n\t\t{`false`, false, false},\n\t\t{`TRUE`, false, true},\n\t\t{`FALSE`, false, true},\n\t\t{`foo`, false, true},\n\t\t{`\"true\"`, false, true},\n\t\t{`\"false\"`, false, true},\n\t}\n\n\tfor i, tt := range tests {\n\t\tb, err := ParseBoolLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif b != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%t, but actual=%t\", i, tt.expected, b)\n\t\t}\n\t}\n}\n"},{"name":"path.gno","body":"package json\n\nimport (\n\t\"errors\"\n)\n\n// ParsePath takes a JSONPath string and returns a slice of strings representing the path segments.\nfunc ParsePath(path string) ([]string, error) {\n\tbuf := newBuffer([]byte(path))\n\tresult := make([]string, 0)\n\n\tfor {\n\t\tb, err := buf.current()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch {\n\t\tcase b == dollarSign || b == atSign:\n\t\t\tresult = append(result, string(b))\n\t\t\tbuf.step()\n\n\t\tcase b == dot:\n\t\t\tbuf.step()\n\n\t\t\tif next, _ := buf.current(); next == dot {\n\t\t\t\tbuf.step()\n\t\t\t\tresult = append(result, \"..\")\n\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t} else {\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t}\n\n\t\tcase b == bracketOpen:\n\t\t\tstart := buf.index\n\t\t\tbuf.step()\n\n\t\t\tfor {\n\t\t\t\tif buf.index \u003e= buf.length || buf.data[buf.index] == bracketClose {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tbuf.step()\n\t\t\t}\n\n\t\t\tif buf.index \u003e= buf.length {\n\t\t\t\treturn nil, errors.New(\"unexpected end of path\")\n\t\t\t}\n\n\t\t\tsegment := string(buf.sliceFromIndices(start+1, buf.index))\n\t\t\tresult = append(result, segment)\n\n\t\t\tbuf.step()\n\n\t\tdefault:\n\t\t\tbuf.step()\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// extractNextSegment extracts the segment from the current index\n// to the next significant character and adds it to the resulting slice.\nfunc extractNextSegment(buf *buffer, result *[]string) {\n\tstart := buf.index\n\tbuf.skipToNextSignificantToken()\n\n\tif buf.index \u003c= start {\n\t\treturn\n\t}\n\n\tsegment := string(buf.sliceFromIndices(start, buf.index))\n\tif segment != \"\" {\n\t\t*result = append(*result, segment)\n\t}\n}\n"},{"name":"path_test.gno","body":"package json\n\nimport \"testing\"\n\nfunc TestParseJSONPath(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpath string\n\t\texpected []string\n\t}{\n\t\t{name: \"Empty string path\", path: \"\", expected: []string{}},\n\t\t{name: \"Root only path\", path: \"$\", expected: []string{\"$\"}},\n\t\t{name: \"Root with dot path\", path: \"$.\", expected: []string{\"$\"}},\n\t\t{name: \"All objects in path\", path: \"$..\", expected: []string{\"$\", \"..\"}},\n\t\t{name: \"Only children in path\", path: \"$.*\", expected: []string{\"$\", \"*\"}},\n\t\t{name: \"All objects' children in path\", path: \"$..*\", expected: []string{\"$\", \"..\", \"*\"}},\n\t\t{name: \"Simple dot notation path\", path: \"$.root.element\", expected: []string{\"$\", \"root\", \"element\"}},\n\t\t{name: \"Complex dot notation path with wildcard\", path: \"$.root.*.element\", expected: []string{\"$\", \"root\", \"*\", \"element\"}},\n\t\t{name: \"Path with array wildcard\", path: \"$.phoneNumbers[*].type\", expected: []string{\"$\", \"phoneNumbers\", \"*\", \"type\"}},\n\t\t{name: \"Path with filter expression\", path: \"$.store.book[?(@.price \u003c 10)].title\", expected: []string{\"$\", \"store\", \"book\", \"?(@.price \u003c 10)\", \"title\"}},\n\t\t{name: \"Path with formula\", path: \"$..phoneNumbers..('ty' + 'pe')\", expected: []string{\"$\", \"..\", \"phoneNumbers\", \"..\", \"('ty' + 'pe')\"}},\n\t\t{name: \"Simple bracket notation path\", path: \"$['root']['element']\", expected: []string{\"$\", \"'root'\", \"'element'\"}},\n\t\t{name: \"Complex bracket notation path with wildcard\", path: \"$['root'][*]['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Bracket notation path with integer index\", path: \"$['store']['book'][0]['title']\", expected: []string{\"$\", \"'store'\", \"'book'\", \"0\", \"'title'\"}},\n\t\t{name: \"Complex path with wildcard in bracket notation\", path: \"$['root'].*['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot after bracket\", path: \"$.['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot before bracket\", path: \"$['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Single character path with root\", path: \"$.a\", expected: []string{\"$\", \"a\"}},\n\t\t{name: \"Multiple characters path with root\", path: \"$.abc\", expected: []string{\"$\", \"abc\"}},\n\t\t{name: \"Multiple segments path with root\", path: \"$.a.b.c\", expected: []string{\"$\", \"a\", \"b\", \"c\"}},\n\t\t{name: \"Multiple segments path with wildcard and root\", path: \"$.a.*.c\", expected: []string{\"$\", \"a\", \"*\", \"c\"}},\n\t\t{name: \"Multiple segments path with filter and root\", path: \"$.a[?(@.b == 'c')].d\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\"}},\n\t\t{name: \"Complex path with multiple filters\", path: \"$.a[?(@.b == 'c')].d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Complex path with multiple filters and wildcards\", path: \"$.a[?(@.b == 'c')].*.d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"*\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Path with array index and root\", path: \"$.a[0].b\", expected: []string{\"$\", \"a\", \"0\", \"b\"}},\n\t\t{name: \"Path with multiple array indices and root\", path: \"$.a[0].b[1].c\", expected: []string{\"$\", \"a\", \"0\", \"b\", \"1\", \"c\"}},\n\t\t{name: \"Path with array index, wildcard and root\", path: \"$.a[0].*.c\", expected: []string{\"$\", \"a\", \"0\", \"*\", \"c\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treult, _ := ParsePath(tt.path)\n\t\t\tif !isEqualSlice(reult, tt.expected) {\n\t\t\t\tt.Errorf(\"ParsePath(%s) expected: %v, got: %v\", tt.path, tt.expected, reult)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc isEqualSlice(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"},{"name":"token.gno","body":"package json\n\nconst (\n\tbracketOpen = '['\n\tbracketClose = ']'\n\tparenOpen = '('\n\tparenClose = ')'\n\tcurlyOpen = '{'\n\tcurlyClose = '}'\n\tcomma = ','\n\tdot = '.'\n\tcolon = ':'\n\tbackTick = '`'\n\tsingleQuote = '\\''\n\tdoubleQuote = '\"'\n\temptyString = \"\"\n\twhiteSpace = ' '\n\tplus = '+'\n\tminus = '-'\n\taesterisk = '*'\n\tbang = '!'\n\tquestion = '?'\n\tnewLine = '\\n'\n\ttab = '\\t'\n\tcarriageReturn = '\\r'\n\tformFeed = '\\f'\n\tbackSpace = '\\b'\n\tslash = '/'\n\tbackSlash = '\\\\'\n\tunderScore = '_'\n\tdollarSign = '$'\n\tatSign = '@'\n\tandSign = '\u0026'\n\torSign = '|'\n)\n\nvar (\n\ttrueLiteral = []byte(\"true\")\n\tfalseLiteral = []byte(\"false\")\n\tnullLiteral = []byte(\"null\")\n)\n\ntype ValueType int\n\nconst (\n\tNotExist ValueType = iota\n\tString\n\tNumber\n\tFloat\n\tObject\n\tArray\n\tBoolean\n\tNull\n\tUnknown\n)\n\nfunc (v ValueType) String() string {\n\tswitch v {\n\tcase NotExist:\n\t\treturn \"not-exist\"\n\tcase String:\n\t\treturn \"string\"\n\tcase Number:\n\t\treturn \"number\"\n\tcase Object:\n\t\treturn \"object\"\n\tcase Array:\n\t\treturn \"array\"\n\tcase Boolean:\n\t\treturn \"boolean\"\n\tcase Null:\n\t\treturn \"null\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"int32","path":"gno.land/p/demo/math_eval/int32","files":[{"name":"int32.gno","body":"// eval/int32 is a evaluator for int32 expressions.\n// This code is heavily forked from https://github.com/dengsgo/math-engine\n// which is licensed under Apache 2.0:\n// https://raw.githubusercontent.com/dengsgo/math-engine/298e2b57b7e7350d0f67bd036916efd5709abe25/LICENSE\npackage int32\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tIdentifier = iota\n\tNumber // numbers\n\tOperator // +, -, *, /, etc.\n\tVariable // x, y, z, etc. (one-letter only)\n)\n\ntype expression interface {\n\tString() string\n}\n\ntype expressionRaw struct {\n\texpression string\n\tType int\n\tFlag int\n\tOffset int\n}\n\ntype parser struct {\n\tInput string\n\tch byte\n\toffset int\n\terr error\n}\n\ntype expressionNumber struct {\n\tVal int\n\tStr string\n}\n\ntype expressionVariable struct {\n\tVal int\n\tStr string\n}\n\ntype expressionOperation struct {\n\tOp string\n\tLhs,\n\tRhs expression\n}\n\ntype ast struct {\n\trawexpressions []*expressionRaw\n\tsource string\n\tcurrentexpression *expressionRaw\n\tcurrentIndex int\n\tdepth int\n\terr error\n}\n\n// Parse takes an expression string, e.g. \"1+2\" and returns\n// a parsed expression. If there is an error it will return.\nfunc Parse(s string) (ar expression, err error) {\n\ttoks, err := lexer(s)\n\tif err != nil {\n\t\treturn\n\t}\n\tast, err := newAST(toks, s)\n\tif err != nil {\n\t\treturn\n\t}\n\tar, err = ast.parseExpression()\n\treturn\n}\n\n// Eval takes a parsed expression and a map of variables (or nil). The parsed\n// expression is evaluated using any variables and returns the\n// resulting int and/or error.\nfunc Eval(expr expression, variables map[string]int) (res int, err error) {\n\tif err != nil {\n\t\treturn\n\t}\n\tvar l, r int\n\tswitch expr.(type) {\n\tcase expressionVariable:\n\t\tast := expr.(expressionVariable)\n\t\tok := false\n\t\tif variables != nil {\n\t\t\tres, ok = variables[ast.Str]\n\t\t}\n\t\tif !ok {\n\t\t\terr = ufmt.Errorf(\"variable '%s' not found\", ast.Str)\n\t\t}\n\t\treturn\n\tcase expressionOperation:\n\t\tast := expr.(expressionOperation)\n\t\tl, err = Eval(ast.Lhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tr, err = Eval(ast.Rhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tswitch ast.Op {\n\t\tcase \"+\":\n\t\t\tres = l + r\n\t\tcase \"-\":\n\t\t\tres = l - r\n\t\tcase \"*\":\n\t\t\tres = l * r\n\t\tcase \"/\":\n\t\t\tif r == 0 {\n\t\t\t\terr = ufmt.Errorf(\"violation of arithmetic specification: a division by zero in Eval: [%d/%d]\", l, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tres = l / r\n\t\tcase \"%\":\n\t\t\tif r == 0 {\n\t\t\t\tres = 0\n\t\t\t} else {\n\t\t\t\tres = l % r\n\t\t\t}\n\t\tcase \"^\":\n\t\t\tres = l ^ r\n\t\tcase \"\u003e\u003e\":\n\t\t\tres = l \u003e\u003e r\n\t\tcase \"\u003c\u003c\":\n\t\t\tres = l \u003c\u003c r\n\t\tcase \"\u003e\":\n\t\t\tif l \u003e r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u003c\":\n\t\t\tif l \u003c r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u0026\":\n\t\t\tres = l \u0026 r\n\t\tcase \"|\":\n\t\t\tres = l | r\n\t\tdefault:\n\n\t\t}\n\tcase expressionNumber:\n\t\tres = expr.(expressionNumber).Val\n\t}\n\n\treturn\n}\n\nfunc expressionError(s string, pos int) string {\n\tr := strings.Repeat(\"-\", len(s)) + \"\\n\"\n\ts += \"\\n\"\n\tfor i := 0; i \u003c pos; i++ {\n\t\ts += \" \"\n\t}\n\ts += \"^\\n\"\n\treturn r + s + r\n}\n\nfunc (n expressionVariable) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionVariable: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (n expressionNumber) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionNumber: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (b expressionOperation) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionOperation: (%s %s %s)\",\n\t\tb.Op,\n\t\tb.Lhs.String(),\n\t\tb.Rhs.String(),\n\t)\n}\n\nfunc newAST(toks []*expressionRaw, s string) (*ast, error) {\n\ta := \u0026ast{\n\t\trawexpressions: toks,\n\t\tsource: s,\n\t}\n\tif a.rawexpressions == nil || len(a.rawexpressions) == 0 {\n\t\treturn a, errors.New(\"empty token\")\n\t} else {\n\t\ta.currentIndex = 0\n\t\ta.currentexpression = a.rawexpressions[0]\n\t}\n\treturn a, nil\n}\n\nfunc (a *ast) parseExpression() (expression, error) {\n\ta.depth++ // called depth\n\tlhs := a.parsePrimary()\n\tr := a.parseBinOpRHS(0, lhs)\n\ta.depth--\n\tif a.depth == 0 \u0026\u0026 a.currentIndex != len(a.rawexpressions) \u0026\u0026 a.err == nil {\n\t\treturn r, ufmt.Errorf(\"bad expression, reaching the end or missing the operator\\n%s\",\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t}\n\treturn r, nil\n}\n\nfunc (a *ast) getNextexpressionRaw() *expressionRaw {\n\ta.currentIndex++\n\tif a.currentIndex \u003c len(a.rawexpressions) {\n\t\ta.currentexpression = a.rawexpressions[a.currentIndex]\n\t\treturn a.currentexpression\n\t}\n\treturn nil\n}\n\nfunc (a *ast) getTokPrecedence() int {\n\tswitch a.currentexpression.expression {\n\tcase \"/\", \"%\", \"*\":\n\t\treturn 100\n\tcase \"\u003c\u003c\", \"\u003e\u003e\":\n\t\treturn 80\n\tcase \"+\", \"-\":\n\t\treturn 75\n\tcase \"\u003c\", \"\u003e\":\n\t\treturn 70\n\tcase \"\u0026\":\n\t\treturn 60\n\tcase \"^\":\n\t\treturn 50\n\tcase \"|\":\n\t\treturn 40\n\t}\n\treturn -1\n}\n\nfunc (a *ast) parseNumber() expressionNumber {\n\tf64, err := strconv.Atoi(a.currentexpression.expression)\n\tif err != nil {\n\t\ta.err = ufmt.Errorf(\"%v\\nwant '(' or '0-9' but get '%s'\\n%s\",\n\t\t\terr.Error(),\n\t\t\ta.currentexpression.expression,\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\treturn expressionNumber{}\n\t}\n\tn := expressionNumber{\n\t\tVal: f64,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parseVariable() expressionVariable {\n\tn := expressionVariable{\n\t\tVal: 0,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parsePrimary() expression {\n\tswitch a.currentexpression.Type {\n\tcase Variable:\n\t\treturn a.parseVariable()\n\tcase Number:\n\t\treturn a.parseNumber()\n\tcase Operator:\n\t\tif a.currentexpression.expression == \"(\" {\n\t\t\tt := a.getNextexpressionRaw()\n\t\t\tif t == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\te, _ := a.parseExpression()\n\t\t\tif e == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif a.currentexpression.expression != \")\" {\n\t\t\t\ta.err = ufmt.Errorf(\"want ')' but get %s\\n%s\",\n\t\t\t\t\ta.currentexpression.expression,\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\ta.getNextexpressionRaw()\n\t\t\treturn e\n\t\t} else if a.currentexpression.expression == \"-\" {\n\t\t\tif a.getNextexpressionRaw() == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '0-9' but get '-'\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tbin := expressionOperation{\n\t\t\t\tOp: \"-\",\n\t\t\t\tLhs: expressionNumber{},\n\t\t\t\tRhs: a.parsePrimary(),\n\t\t\t}\n\t\t\treturn bin\n\t\t} else {\n\t\t\treturn a.parseNumber()\n\t\t}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (a *ast) parseBinOpRHS(execPrec int, lhs expression) expression {\n\tfor {\n\t\ttokPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c execPrec {\n\t\t\treturn lhs\n\t\t}\n\t\tbinOp := a.currentexpression.expression\n\t\tif a.getNextexpressionRaw() == nil {\n\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\treturn nil\n\t\t}\n\t\trhs := a.parsePrimary()\n\t\tif rhs == nil {\n\t\t\treturn nil\n\t\t}\n\t\tnextPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c nextPrec {\n\t\t\trhs = a.parseBinOpRHS(tokPrec+1, rhs)\n\t\t\tif rhs == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tlhs = expressionOperation{\n\t\t\tOp: binOp,\n\t\t\tLhs: lhs,\n\t\t\tRhs: rhs,\n\t\t}\n\t}\n}\n\nfunc lexer(s string) ([]*expressionRaw, error) {\n\tp := \u0026parser{\n\t\tInput: s,\n\t\terr: nil,\n\t\tch: s[0],\n\t}\n\ttoks := p.parse()\n\tif p.err != nil {\n\t\treturn nil, p.err\n\t}\n\treturn toks, nil\n}\n\nfunc (p *parser) parse() []*expressionRaw {\n\ttoks := make([]*expressionRaw, 0)\n\tfor {\n\t\ttok := p.nextTok()\n\t\tif tok == nil {\n\t\t\tbreak\n\t\t}\n\t\ttoks = append(toks, tok)\n\t}\n\treturn toks\n}\n\nfunc (p *parser) nextTok() *expressionRaw {\n\tif p.offset \u003e= len(p.Input) || p.err != nil {\n\t\treturn nil\n\t}\n\tvar err error\n\tfor p.isWhitespace(p.ch) \u0026\u0026 err == nil {\n\t\terr = p.nextCh()\n\t}\n\tstart := p.offset\n\tvar tok *expressionRaw\n\tswitch p.ch {\n\tcase\n\t\t'(',\n\t\t')',\n\t\t'+',\n\t\t'-',\n\t\t'*',\n\t\t'/',\n\t\t'^',\n\t\t'\u0026',\n\t\t'|',\n\t\t'%':\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: string(p.ch),\n\t\t\tType: Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\terr = p.nextCh()\n\tcase '\u003e', '\u003c':\n\t\ttokS := string(p.ch)\n\t\tbb, be := p.nextChPeek()\n\t\tif be == nil \u0026\u0026 string(bb) == tokS {\n\t\t\ttokS += string(p.ch)\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: tokS,\n\t\t\tType: Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\tif len(tokS) \u003e 1 {\n\t\t\tp.nextCh()\n\t\t}\n\t\terr = p.nextCh()\n\tcase\n\t\t'0',\n\t\t'1',\n\t\t'2',\n\t\t'3',\n\t\t'4',\n\t\t'5',\n\t\t'6',\n\t\t'7',\n\t\t'8',\n\t\t'9':\n\t\tfor p.isDigitNum(p.ch) \u0026\u0026 p.nextCh() == nil {\n\t\t\tif (p.ch == '-' || p.ch == '+') \u0026\u0026 p.Input[p.offset-1] != 'e' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: strings.ReplaceAll(p.Input[start:p.offset], \"_\", \"\"),\n\t\t\tType: Number,\n\t\t}\n\t\ttok.Offset = start\n\tdefault:\n\t\tif p.isChar(p.ch) {\n\t\t\ttok = \u0026expressionRaw{\n\t\t\t\texpression: string(p.ch),\n\t\t\t\tType: Variable,\n\t\t\t}\n\t\t\ttok.Offset = start\n\t\t\terr = p.nextCh()\n\t\t} else if p.ch != ' ' {\n\t\t\tp.err = ufmt.Errorf(\"symbol error: unknown '%v', pos [%v:]\\n%s\",\n\t\t\t\tstring(p.ch),\n\t\t\t\tstart,\n\t\t\t\texpressionError(p.Input, start))\n\t\t}\n\t}\n\treturn tok\n}\n\nfunc (p *parser) nextChPeek() (byte, error) {\n\toffset := p.offset + 1\n\tif offset \u003c len(p.Input) {\n\t\treturn p.Input[offset], nil\n\t}\n\treturn byte(0), errors.New(\"no byte\")\n}\n\nfunc (p *parser) nextCh() error {\n\tp.offset++\n\tif p.offset \u003c len(p.Input) {\n\t\tp.ch = p.Input[p.offset]\n\t\treturn nil\n\t}\n\treturn errors.New(\"EOF\")\n}\n\nfunc (p *parser) isWhitespace(c byte) bool {\n\treturn c == ' ' ||\n\t\tc == '\\t' ||\n\t\tc == '\\n' ||\n\t\tc == '\\v' ||\n\t\tc == '\\f' ||\n\t\tc == '\\r'\n}\n\nfunc (p *parser) isDigitNum(c byte) bool {\n\treturn '0' \u003c= c \u0026\u0026 c \u003c= '9' || c == '.' || c == '_' || c == 'e' || c == '-' || c == '+'\n}\n\nfunc (p *parser) isChar(c byte) bool {\n\treturn 'a' \u003c= c \u0026\u0026 c \u003c= 'z' || 'A' \u003c= c \u0026\u0026 c \u003c= 'Z'\n}\n\nfunc (p *parser) isWordChar(c byte) bool {\n\treturn p.isChar(c) || '0' \u003c= c \u0026\u0026 c \u003c= '9'\n}\n"},{"name":"int32_test.gno","body":"package int32\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\tttt := []struct {\n\t\texp string\n\t\tres int\n\t}{\n\t\t{\"1\", 1},\n\t\t{\"--1\", 1},\n\t\t{\"1+2\", 3},\n\t\t{\"-1+2\", 1},\n\t\t{\"-(1+2)\", -3},\n\t\t{\"-(1+2)*5\", -15},\n\t\t{\"-(1+2)*5/3\", -5},\n\t\t{\"1+(-(1+2)*5/3)\", -4},\n\t\t{\"3^4\", 3 ^ 4},\n\t\t{\"8%2\", 8 % 2},\n\t\t{\"8%3\", 8 % 3},\n\t\t{\"8|3\", 8 | 3},\n\t\t{\"10%2\", 0},\n\t\t{\"(4 + 3)/2-1+11*15\", (4+3)/2 - 1 + 11*15},\n\t\t{\n\t\t\t\"(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64)\",\n\t\t\t(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64),\n\t\t},\n\t\t{\n\t\t\t\"(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64)\",\n\t\t\t(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64),\n\t\t},\n\t\t{\"((0000+1)*0000)\", 0},\n\t}\n\tfor _, tc := range ttt {\n\t\tt.Run(tc.exp, func(t *testing.T) {\n\t\t\texp, err := Parse(tc.exp)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"%s:\\n%s\", tc.exp, err.Error())\n\t\t\t} else {\n\t\t\t\tres, errEval := Eval(exp, nil)\n\t\t\t\tif errEval != nil {\n\t\t\t\t\tt.Errorf(\"eval error: %s\", errEval.Error())\n\t\t\t\t} else if res != tc.res {\n\t\t\t\t\tt.Errorf(\"%s:\\nexpected %d, got %d\", tc.exp, tc.res, res)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVariables(t *testing.T) {\n\tfn := func(x, y int) int {\n\t\treturn 1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\n\t}\n\texpr := \"1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\"\n\texp, err := Parse(expr)\n\tif err != nil {\n\t\tt.Errorf(\"could not parse: %s\", err.Error())\n\t}\n\tvariables := make(map[string]int)\n\tfor i := 0; i \u003c 10; i++ {\n\t\tvariables[\"x\"] = i\n\t\tvariables[\"y\"] = 2\n\t\tres, errEval := Eval(exp, variables)\n\t\tif errEval != nil {\n\t\t\tt.Errorf(\"could not evaluate: %s\", err.Error())\n\t\t}\n\t\texpected := fn(variables[\"x\"], variables[\"y\"])\n\t\tif res != expected {\n\t\t\tt.Errorf(\"expected: %d, actual: %d\", expected, res)\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"membstore","path":"gno.land/p/demo/membstore","files":[{"name":"members.gno","body":"package membstore\n\nimport (\n\t\"std\"\n)\n\n// MemberStore defines the member storage abstraction\ntype MemberStore interface {\n\t// Members returns all members in the store\n\tMembers(offset, count uint64) []Member\n\n\t// Size returns the current size of the store\n\tSize() int\n\n\t// IsMember returns a flag indicating if the given address\n\t// belongs to a member\n\tIsMember(address std.Address) bool\n\n\t// TotalPower returns the total voting power of the member store\n\tTotalPower() uint64\n\n\t// Member returns the requested member\n\tMember(address std.Address) (Member, error)\n\n\t// AddMember adds a member to the store\n\tAddMember(member Member) error\n\n\t// UpdateMember updates the member in the store.\n\t// If updating a member's voting power to 0,\n\t// the member will be removed\n\tUpdateMember(address std.Address, member Member) error\n}\n\n// Member holds the relevant member information\ntype Member struct {\n\tAddress std.Address // bech32 gno address of the member (unique)\n\tVotingPower uint64 // the voting power of the member\n}\n"},{"name":"membstore.gno","body":"package membstore\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrAlreadyMember = errors.New(\"address is already a member\")\n\tErrMissingMember = errors.New(\"address is not a member\")\n\tErrInvalidAddressUpdate = errors.New(\"invalid address update\")\n\tErrNotGovDAO = errors.New(\"caller not correct govdao instance\")\n)\n\n// maxRequestMembers is the maximum number of\n// paginated members that can be requested\nconst maxRequestMembers = 50\n\ntype Option func(*MembStore)\n\n// WithInitialMembers initializes the member store\n// with an initial member list\nfunc WithInitialMembers(members []Member) Option {\n\treturn func(store *MembStore) {\n\t\tfor _, m := range members {\n\t\t\tmemberAddr := m.Address.String()\n\n\t\t\t// Check if the member already exists\n\t\t\tif store.members.Has(memberAddr) {\n\t\t\t\tpanic(ufmt.Errorf(\"%s, %s\", memberAddr, ErrAlreadyMember))\n\t\t\t}\n\n\t\t\tstore.members.Set(memberAddr, m)\n\t\t\tstore.totalVotingPower += m.VotingPower\n\t\t}\n\t}\n}\n\n// WithDAOPkgPath initializes the member store\n// with a dao package path guard\nfunc WithDAOPkgPath(daoPkgPath string) Option {\n\treturn func(store *MembStore) {\n\t\tstore.daoPkgPath = daoPkgPath\n\t}\n}\n\n// MembStore implements the dao.MembStore abstraction\ntype MembStore struct {\n\tdaoPkgPath string // active dao pkg path, if any\n\tmembers *avl.Tree // std.Address -\u003e Member\n\ttotalVotingPower uint64 // cached value for quick lookups\n}\n\n// NewMembStore creates a new member store\nfunc NewMembStore(opts ...Option) *MembStore {\n\tm := \u0026MembStore{\n\t\tmembers: avl.NewTree(), // empty set\n\t\tdaoPkgPath: \"\", // no dao guard\n\t\ttotalVotingPower: 0,\n\t}\n\n\t// Apply the options\n\tfor _, opt := range opts {\n\t\topt(m)\n\t}\n\n\treturn m\n}\n\n// AddMember adds member to the member store `m`.\n// It fails if the caller is not GovDAO or\n// if the member is already present\nfunc (m *MembStore) AddMember(member Member) error {\n\tif !m.isCallerDAORealm() {\n\t\treturn ErrNotGovDAO\n\t}\n\n\t// Check if the member exists\n\tif m.IsMember(member.Address) {\n\t\treturn ErrAlreadyMember\n\t}\n\n\t// Add the member\n\tm.members.Set(member.Address.String(), member)\n\n\t// Update the total voting power\n\tm.totalVotingPower += member.VotingPower\n\n\treturn nil\n}\n\n// UpdateMember updates the member with the given address.\n// Updating fails if the caller is not GovDAO.\nfunc (m *MembStore) UpdateMember(address std.Address, member Member) error {\n\tif !m.isCallerDAORealm() {\n\t\treturn ErrNotGovDAO\n\t}\n\n\t// Get the member\n\toldMember, err := m.Member(address)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if this is a removal request\n\tif member.VotingPower == 0 {\n\t\tm.members.Remove(address.String())\n\n\t\t// Update the total voting power\n\t\tm.totalVotingPower -= oldMember.VotingPower\n\n\t\treturn nil\n\t}\n\n\t// Check that the member wouldn't be\n\t// overwriting an existing one\n\tisAddressUpdate := address != member.Address\n\tif isAddressUpdate \u0026\u0026 m.IsMember(member.Address) {\n\t\treturn ErrInvalidAddressUpdate\n\t}\n\n\t// Remove the old member info\n\t// in case the address changed\n\tif address != member.Address {\n\t\tm.members.Remove(address.String())\n\t}\n\n\t// Save the new member info\n\tm.members.Set(member.Address.String(), member)\n\n\t// Update the total voting power\n\tdifference := member.VotingPower - oldMember.VotingPower\n\tm.totalVotingPower += difference\n\n\treturn nil\n}\n\n// IsMember returns a flag indicating if the given\n// address belongs to a member of the member store\nfunc (m *MembStore) IsMember(address std.Address) bool {\n\t_, exists := m.members.Get(address.String())\n\n\treturn exists\n}\n\n// Member returns the member associated with the given address\nfunc (m *MembStore) Member(address std.Address) (Member, error) {\n\tmember, exists := m.members.Get(address.String())\n\tif !exists {\n\t\treturn Member{}, ErrMissingMember\n\t}\n\n\treturn member.(Member), nil\n}\n\n// Members returns a paginated list of members from\n// the member store. If the store is empty, an empty slice\n// is returned instead\nfunc (m *MembStore) Members(offset, count uint64) []Member {\n\t// Calculate the left and right bounds\n\tif count \u003c 1 || offset \u003e= uint64(m.members.Size()) {\n\t\treturn []Member{}\n\t}\n\n\t// Limit the maximum number of returned members\n\tif count \u003e maxRequestMembers {\n\t\tcount = maxRequestMembers\n\t}\n\n\t// Gather the members\n\tmembers := make([]Member, 0)\n\tm.members.IterateByOffset(\n\t\tint(offset),\n\t\tint(count),\n\t\tfunc(_ string, val interface{}) bool {\n\t\t\tmember := val.(Member)\n\n\t\t\t// Save the member\n\t\t\tmembers = append(members, member)\n\n\t\t\treturn false\n\t\t})\n\n\treturn members\n}\n\n// Size returns the number of active members in the member store\nfunc (m *MembStore) Size() int {\n\treturn m.members.Size()\n}\n\n// TotalPower returns the total voting power\n// of the member store\nfunc (m *MembStore) TotalPower() uint64 {\n\treturn m.totalVotingPower\n}\n\n// isCallerDAORealm returns a flag indicating if the\n// current caller context is the active DAO Realm.\n// We need to include a dao guard, even if the\n// executor guarantees it, because\n// the API of the member store is public and callable\n// by anyone who has a reference to the member store instance.\nfunc (m *MembStore) isCallerDAORealm() bool {\n\treturn m.daoPkgPath == \"\" || std.CurrentRealm().PkgPath() == m.daoPkgPath\n}\n"},{"name":"membstore_test.gno","body":"package membstore\n\nimport (\n\t\"testing\"\n\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateMembers generates dummy govdao members\nfunc generateMembers(t *testing.T, count int) []Member {\n\tt.Helper()\n\n\tmembers := make([]Member, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tmembers = append(members, Member{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"member %d\", i)),\n\t\t\tVotingPower: 10,\n\t\t})\n\t}\n\n\treturn members\n}\n\nfunc TestMembStore_GetMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"member not found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\t_, err := m.Member(testutils.TestAddress(\"random\"))\n\t\tuassert.ErrorIs(t, err, ErrMissingMember)\n\t})\n\n\tt.Run(\"valid member fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\t_, err := m.Member(members[0].Address)\n\t\tuassert.NoError(t, err)\n\t})\n}\n\nfunc TestMembStore_GetMembers(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no members\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tmembers := m.Members(0, 10)\n\t\tuassert.Equal(t, 0, len(members))\n\t})\n\n\tt.Run(\"proper pagination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumMembers = maxRequestMembers * 2\n\t\t\thalfRange = numMembers / 2\n\n\t\t\tmembers = generateMembers(t, numMembers)\n\t\t\tm = NewMembStore(WithInitialMembers(members))\n\n\t\t\tverifyMembersPresent = func(members, fetchedMembers []Member) {\n\t\t\t\tfor _, fetchedMember := range fetchedMembers {\n\t\t\t\t\tfor _, member := range members {\n\t\t\t\t\t\tif member.Address != fetchedMember.Address {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tuassert.Equal(t, member.VotingPower, fetchedMember.VotingPower)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\turequire.Equal(t, numMembers, m.Size())\n\n\t\tfetchedMembers := m.Members(0, uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedMembers))\n\n\t\t// Verify the members\n\t\tverifyMembersPresent(members, fetchedMembers)\n\n\t\t// Fetch the other half\n\t\tfetchedMembers = m.Members(uint64(halfRange), uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedMembers))\n\n\t\t// Verify the members\n\t\tverifyMembersPresent(members, fetchedMembers)\n\t})\n}\n\nfunc TestMembStore_IsMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"non-existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tuassert.False(t, m.IsMember(testutils.TestAddress(\"random\")))\n\t})\n\n\tt.Run(\"existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 50)\n\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\tfor _, member := range members {\n\t\t\tuassert.True(t, m.IsMember(member.Address))\n\t\t}\n\t})\n}\n\nfunc TestMembStore_AddMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller not govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore(WithDAOPkgPath(\"gno.land/r/gov/dao\"))\n\n\t\t// Attempt to add a member\n\t\tmember := generateMembers(t, 1)[0]\n\t\tuassert.ErrorIs(t, m.AddMember(member), ErrNotGovDAO)\n\t})\n\n\tt.Run(\"member already exists\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\t// Attempt to add a member\n\t\tuassert.ErrorIs(t, m.AddMember(members[0]), ErrAlreadyMember)\n\t})\n\n\tt.Run(\"new member added\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create an empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath))\n\n\t\t// Attempt to add a member\n\t\turequire.NoError(t, m.AddMember(members[0]))\n\n\t\t// Make sure the member is added\n\t\tuassert.True(t, m.IsMember(members[0].Address))\n\t})\n}\n\nfunc TestMembStore_Size(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tuassert.Equal(t, 0, m.Size())\n\t})\n\n\tt.Run(\"non-empty govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 50)\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\tuassert.Equal(t, len(members), m.Size())\n\t})\n}\n\nfunc TestMembStore_UpdateMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller not govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore(WithDAOPkgPath(\"gno.land/r/gov/dao\"))\n\n\t\t// Attempt to update a member\n\t\tmember := generateMembers(t, 1)[0]\n\t\tuassert.ErrorIs(t, m.UpdateMember(member.Address, member), ErrNotGovDAO)\n\t})\n\n\tt.Run(\"non-existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create an empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath))\n\n\t\t// Attempt to update a member\n\t\tuassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), ErrMissingMember)\n\t})\n\n\tt.Run(\"overwrite member attempt\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 2)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\t// Attempt to update a member\n\t\tuassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), ErrInvalidAddressUpdate)\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\toldVotingPower := m.totalVotingPower\n\t\turequire.Equal(t, members[0].VotingPower, oldVotingPower)\n\n\t\tvotingPower := uint64(300)\n\t\tmembers[0].VotingPower = votingPower\n\n\t\t// Attempt to update a member\n\t\tuassert.NoError(t, m.UpdateMember(members[0].Address, members[0]))\n\t\tuassert.Equal(t, votingPower, m.Members(0, 10)[0].VotingPower)\n\t\turequire.Equal(t, votingPower, m.totalVotingPower)\n\t})\n\n\tt.Run(\"member removed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\tstd.TestSetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\tvotingPower := uint64(0)\n\t\tmembers[0].VotingPower = votingPower\n\n\t\t// Attempt to update a member\n\t\tuassert.NoError(t, m.UpdateMember(members[0].Address, members[0]))\n\n\t\t// Make sure the member was removed\n\t\tuassert.False(t, m.IsMember(members[0].Address))\n\t})\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ownable","path":"gno.land/p/demo/ownable","files":[{"name":"errors.gno","body":"package ownable\n\nimport \"errors\"\n\nvar (\n\tErrUnauthorized = errors.New(\"ownable: caller is not owner\")\n\tErrInvalidAddress = errors.New(\"ownable: new owner address is invalid\")\n)\n"},{"name":"ownable.gno","body":"package ownable\n\nimport \"std\"\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\n// Ownable is meant to be used as a top-level object to make your contract ownable OR\n// being embedded in a Gno object to manage per-object ownership.\ntype Ownable struct {\n\towner std.Address\n}\n\nfunc New() *Ownable {\n\treturn \u0026Ownable{\n\t\towner: std.PrevRealm().Addr(),\n\t}\n}\n\nfunc NewWithAddress(addr std.Address) *Ownable {\n\treturn \u0026Ownable{\n\t\towner: addr,\n\t}\n}\n\n// TransferOwnership transfers ownership of the Ownable struct to a new address\nfunc (o *Ownable) TransferOwnership(newOwner std.Address) error {\n\terr := o.CallerIsOwner()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !newOwner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tprevOwner := o.owner\n\to.owner = newOwner\n\tstd.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", string(prevOwner),\n\t\t\"to\", string(newOwner),\n\t)\n\n\treturn nil\n}\n\n// DropOwnership removes the owner, effectively disabling any owner-related actions\n// Top-level usage: disables all only-owner actions/functions,\n// Embedded usage: behaves like a burn functionality, removing the owner from the struct\nfunc (o *Ownable) DropOwnership() error {\n\terr := o.CallerIsOwner()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tprevOwner := o.owner\n\to.owner = \"\"\n\n\tstd.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", string(prevOwner),\n\t\t\"to\", \"\",\n\t)\n\n\treturn nil\n}\n\n// Owner returns the owner address from Ownable\nfunc (o Ownable) Owner() std.Address {\n\treturn o.owner\n}\n\n// CallerIsOwner checks if the caller of the function is the Realm's owner\nfunc (o Ownable) CallerIsOwner() error {\n\tif std.PrevRealm().Addr() == o.owner {\n\t\treturn nil\n\t}\n\n\treturn ErrUnauthorized\n}\n\n// AssertCallerIsOwner panics if the caller is not the owner\nfunc (o Ownable) AssertCallerIsOwner() {\n\tif std.PrevRealm().Addr() != o.owner {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n"},{"name":"ownable_test.gno","body":"package ownable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n)\n\nfunc TestNew(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // TODO(bug): should not be needed\n\n\to := New()\n\tgot := o.Owner()\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestNewWithAddress(t *testing.T) {\n\to := NewWithAddress(alice)\n\n\tgot := o.Owner()\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestTransferOwnership(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.TransferOwnership(bob)\n\tif err != nil {\n\t\tt.Fatalf(\"TransferOwnership failed, %v\", err)\n\t}\n\n\tgot := o.Owner()\n\tif bob != got {\n\t\tt.Fatalf(\"Expected: %s, got: %s\", bob, got)\n\t}\n}\n\nfunc TestCallerIsOwner(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\tunauthorizedCaller := bob\n\n\tstd.TestSetRealm(std.NewUserRealm(unauthorizedCaller))\n\tstd.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed\n\n\terr := o.CallerIsOwner()\n\tuassert.Error(t, err) // XXX: IsError(..., unauthorizedCaller)\n}\n\nfunc TestDropOwnership(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.DropOwnership()\n\tuassert.NoError(t, err, \"DropOwnership failed\")\n\n\towner := o.Owner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n}\n\n// Errors\n\nfunc TestErrUnauthorized(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // TODO(bug): should not be needed\n\n\to := New()\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob) // TODO(bug): should not be needed\n\n\terr := o.TransferOwnership(alice)\n\tif err != ErrUnauthorized {\n\t\tt.Fatalf(\"Should've been ErrUnauthorized, was %v\", err)\n\t}\n\n\terr = o.DropOwnership()\n\tuassert.ErrorContains(t, err, ErrUnauthorized.Error())\n}\n\nfunc TestErrInvalidAddress(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.TransferOwnership(\"\")\n\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\n\terr = o.TransferOwnership(\"10000000001000000000100000000010000000001000000000\")\n\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"seqid","path":"gno.land/p/demo/seqid","files":[{"name":"README.md","body":"# seqid\n\n```\npackage seqid // import \"gno.land/p/demo/seqid\"\n\nPackage seqid provides a simple way to have sequential IDs which will be ordered\ncorrectly when inserted in an AVL tree.\n\nSample usage:\n\n var id seqid.ID\n var users avl.Tree\n\n func NewUser() {\n \tusers.Set(id.Next().Binary(), \u0026User{ ... })\n }\n\nTYPES\n\ntype ID uint64\n An ID is a simple sequential ID generator.\n\nfunc FromBinary(b string) (ID, bool)\n FromBinary creates a new ID from the given string.\n\nfunc (i ID) Binary() string\n Binary returns a big-endian binary representation of the ID, suitable to be\n used as an AVL key.\n\nfunc (i *ID) Next() ID\n Next advances the ID i. It will panic if increasing ID would overflow.\n\nfunc (i *ID) TryNext() (ID, bool)\n TryNext increases i by 1 and returns its value. It returns true if\n successful, or false if the increment would result in an overflow.\n```\n"},{"name":"seqid.gno","body":"// Package seqid provides a simple way to have sequential IDs which will be\n// ordered correctly when inserted in an AVL tree.\n//\n// Sample usage:\n//\n//\tvar id seqid.ID\n//\tvar users avl.Tree\n//\n//\tfunc NewUser() {\n//\t\tusers.Set(id.Next().String(), \u0026User{ ... })\n//\t}\npackage seqid\n\nimport (\n\t\"encoding/binary\"\n\n\t\"gno.land/p/demo/cford32\"\n)\n\n// An ID is a simple sequential ID generator.\ntype ID uint64\n\n// Next advances the ID i.\n// It will panic if increasing ID would overflow.\nfunc (i *ID) Next() ID {\n\tnext, ok := i.TryNext()\n\tif !ok {\n\t\tpanic(\"seqid: next ID overflows uint64\")\n\t}\n\treturn next\n}\n\nconst maxID ID = 1\u003c\u003c64 - 1\n\n// TryNext increases i by 1 and returns its value.\n// It returns true if successful, or false if the increment would result in\n// an overflow.\nfunc (i *ID) TryNext() (ID, bool) {\n\tif *i == maxID {\n\t\t// Addition will overflow.\n\t\treturn 0, false\n\t}\n\t*i++\n\treturn *i, true\n}\n\n// Binary returns a big-endian binary representation of the ID,\n// suitable to be used as an AVL key.\nfunc (i ID) Binary() string {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(buf, uint64(i))\n\treturn string(buf)\n}\n\n// String encodes i using cford32's compact encoding. For more information,\n// see the documentation for package [gno.land/p/demo/cford32].\n//\n// The result of String will be a 7-byte string for IDs [0,2^34), and a\n// 13-byte string for all values following that. All generated string IDs\n// follow the same lexicographic order as their number values; that is, for any\n// two IDs (x, y) such that x \u003c y, x.String() \u003c y.String().\n// As such, this string representation is suitable to be used as an AVL key.\nfunc (i ID) String() string {\n\treturn string(cford32.PutCompact(uint64(i)))\n}\n\n// FromBinary creates a new ID from the given string, expected to be a binary\n// big-endian encoding of an ID (such as that of [ID.Binary]).\n// The second return value is true if the conversion was successful.\nfunc FromBinary(b string) (ID, bool) {\n\tif len(b) != 8 {\n\t\treturn 0, false\n\t}\n\treturn ID(binary.BigEndian.Uint64([]byte(b))), true\n}\n\n// FromString creates a new ID from the given string, expected to be a string\n// representation using cford32, such as that returned by [ID.String].\n//\n// The encoding scheme used by cford32 allows the same ID to have many\n// different representations (though the one returned by [ID.String] is only\n// one, deterministic and safe to be used in AVL). The encoding scheme is\n// \"human-centric\" and is thus case insensitive, and maps some ambiguous\n// characters to be the same, ie. L = I = 1, O = 0. For this reason, when\n// parsing user input to retrieve a key (encoded as a string), always sanitize\n// it first using FromString, then run String(), instead of using the user's\n// input directly.\nfunc FromString(b string) (ID, error) {\n\tn, err := cford32.Uint64([]byte(b))\n\treturn ID(n), err\n}\n"},{"name":"seqid_test.gno","body":"package seqid\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestID(t *testing.T) {\n\tvar i ID\n\n\tfor j := 0; j \u003c 100; j++ {\n\t\ti.Next()\n\t}\n\tif i != 100 {\n\t\tt.Fatalf(\"invalid: wanted %d got %d\", 100, i)\n\t}\n}\n\nfunc TestID_Overflow(t *testing.T) {\n\ti := ID(maxID)\n\n\tdefer func() {\n\t\terr := recover()\n\t\tif !strings.Contains(fmt.Sprint(err), \"next ID overflows\") {\n\t\t\tt.Errorf(\"did not overflow\")\n\t\t}\n\t}()\n\n\ti.Next()\n}\n\nfunc TestID_Binary(t *testing.T) {\n\tvar i ID\n\tprev := i.Binary()\n\n\tfor j := 0; j \u003c 1000; j++ {\n\t\tcur := i.Next().Binary()\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %x \u003e prev %x\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n}\n\nfunc TestID_String(t *testing.T) {\n\tvar i ID\n\tprev := i.String()\n\n\tfor j := 0; j \u003c 1000; j++ {\n\t\tcur := i.Next().String()\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %s \u003e prev %s\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n\n\t// Test for when cford32 switches over to the long encoding.\n\ti = 1\u003c\u003c34 - 512\n\tfor j := 0; j \u003c 1024; j++ {\n\t\tcur := i.Next().String()\n\t\t// println(cur)\n\t\tif cur \u003c= prev {\n\t\t\tt.Fatalf(\"cur %s \u003e prev %s\", cur, prev)\n\t\t}\n\t\tprev = cur\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"memeland","path":"gno.land/p/demo/memeland","files":[{"name":"memeland.gno","body":"package memeland\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nconst (\n\tDATE_CREATED = \"DATE_CREATED\"\n\tUPVOTES = \"UPVOTES\"\n)\n\ntype Post struct {\n\tID string\n\tData string\n\tAuthor std.Address\n\tTimestamp time.Time\n\tUpvoteTracker *avl.Tree // address \u003e struct{}{}\n}\n\ntype Memeland struct {\n\t*ownable.Ownable\n\tPosts []*Post\n\tMemeCounter seqid.ID\n}\n\nfunc NewMemeland() *Memeland {\n\treturn \u0026Memeland{\n\t\tOwnable: ownable.New(),\n\t\tPosts: make([]*Post, 0),\n\t}\n}\n\n// PostMeme - Adds a new post\nfunc (m *Memeland) PostMeme(data string, timestamp int64) string {\n\tif data == \"\" || timestamp \u003c= 0 {\n\t\tpanic(\"timestamp or data cannot be empty\")\n\t}\n\n\t// Generate ID\n\tid := m.MemeCounter.Next().String()\n\n\tnewPost := \u0026Post{\n\t\tID: id,\n\t\tData: data,\n\t\tAuthor: std.PrevRealm().Addr(),\n\t\tTimestamp: time.Unix(timestamp, 0),\n\t\tUpvoteTracker: avl.NewTree(),\n\t}\n\n\tm.Posts = append(m.Posts, newPost)\n\treturn id\n}\n\nfunc (m *Memeland) Upvote(id string) string {\n\tpost := m.getPost(id)\n\tif post == nil {\n\t\tpanic(\"post with specified ID does not exist\")\n\t}\n\n\tcaller := std.PrevRealm().Addr().String()\n\n\tif _, exists := post.UpvoteTracker.Get(caller); exists {\n\t\tpanic(\"user has already upvoted this post\")\n\t}\n\n\tpost.UpvoteTracker.Set(caller, struct{}{})\n\n\treturn \"upvote successful\"\n}\n\n// GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination\nfunc (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\tif len(m.Posts) == 0 {\n\t\treturn \"[]\"\n\t}\n\n\tif page \u003c 1 {\n\t\tpanic(\"page number cannot be less than 1\")\n\t}\n\n\t// No empty pages\n\tif pageSize \u003c 1 {\n\t\tpanic(\"page size cannot be less than 1\")\n\t}\n\n\t// No pages larger than 10\n\tif pageSize \u003e 10 {\n\t\tpanic(\"page size cannot be larger than 10\")\n\t}\n\n\t// Need to pass in a sort parameter\n\tif sortBy == \"\" {\n\t\tpanic(\"sort order cannot be empty\")\n\t}\n\n\tvar filteredPosts []*Post\n\n\tstart := time.Unix(startTimestamp, 0)\n\tend := time.Unix(endTimestamp, 0)\n\n\t// Filtering posts\n\tfor _, p := range m.Posts {\n\t\tif !p.Timestamp.Before(start) \u0026\u0026 !p.Timestamp.After(end) {\n\t\t\tfilteredPosts = append(filteredPosts, p)\n\t\t}\n\t}\n\n\tswitch sortBy {\n\t// Sort by upvote descending\n\tcase UPVOTES:\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].UpvoteTracker.Size() \u003e filteredPosts[j].UpvoteTracker.Size()\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tcase DATE_CREATED:\n\t\t// Sort by timestamp, beginning with newest\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].Timestamp.After(filteredPosts[j].Timestamp)\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tdefault:\n\t\tpanic(\"sort order can only be \\\"UPVOTES\\\" or \\\"DATE_CREATED\\\"\")\n\t}\n\n\t// Pagination\n\tstartIndex := (page - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\n\t// If page does not contain any posts\n\tif startIndex \u003e= len(filteredPosts) {\n\t\treturn \"[]\"\n\t}\n\n\t// If page contains fewer posts than the page size\n\tif endIndex \u003e len(filteredPosts) {\n\t\tendIndex = len(filteredPosts)\n\t}\n\n\t// Return JSON representation of paginated and sorted posts\n\treturn PostsToJSONString(filteredPosts[startIndex:endIndex])\n}\n\n// RemovePost allows the owner to remove a post with a specific ID\nfunc (m *Memeland) RemovePost(id string) string {\n\tif id == \"\" {\n\t\tpanic(\"id cannot be empty\")\n\t}\n\n\tif err := m.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor i, post := range m.Posts {\n\t\tif post.ID == id {\n\t\t\tm.Posts = append(m.Posts[:i], m.Posts[i+1:]...)\n\t\t\treturn id\n\t\t}\n\t}\n\n\tpanic(\"post with specified id does not exist\")\n}\n\n// PostsToJSONString converts a slice of Post structs into a JSON string\nfunc PostsToJSONString(posts []*Post) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"[\")\n\n\tfor i, post := range posts {\n\t\tif i \u003e 0 {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\n\t\tsb.WriteString(PostToJSONString(post))\n\t}\n\tsb.WriteString(\"]\")\n\n\treturn sb.String()\n}\n\n// PostToJSONString returns a Post formatted as a JSON string\nfunc PostToJSONString(post *Post) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"{\")\n\tsb.WriteString(`\"id\":\"` + post.ID + `\",`)\n\tsb.WriteString(`\"data\":\"` + escapeString(post.Data) + `\",`)\n\tsb.WriteString(`\"author\":\"` + escapeString(post.Author.String()) + `\",`)\n\tsb.WriteString(`\"timestamp\":\"` + strconv.Itoa(int(post.Timestamp.Unix())) + `\",`)\n\tsb.WriteString(`\"upvotes\":` + strconv.Itoa(post.UpvoteTracker.Size()))\n\tsb.WriteString(\"}\")\n\n\treturn sb.String()\n}\n\n// escapeString escapes quotes in a string for JSON compatibility.\nfunc escapeString(s string) string {\n\treturn strings.ReplaceAll(s, `\"`, `\\\"`)\n}\n\nfunc (m *Memeland) getPost(id string) *Post {\n\tfor _, p := range m.Posts {\n\t\tif p.ID == id {\n\t\t\treturn p\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PostSorter is a flexible sorter for the *Post slice\ntype PostSorter struct {\n\tPosts []*Post\n\tLessF func(i, j int) bool\n}\n\nfunc (p PostSorter) Len() int {\n\treturn len(p.Posts)\n}\n\nfunc (p PostSorter) Swap(i, j int) {\n\tp.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i]\n}\n\nfunc (p PostSorter) Less(i, j int) bool {\n\treturn p.LessF(i, j)\n}\n"},{"name":"memeland_test.gno","body":"package memeland\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestPostMeme(t *testing.T) {\n\tm := NewMemeland()\n\tid := m.PostMeme(\"Test meme data\", time.Now().Unix())\n\tuassert.NotEqual(t, \"\", string(id), \"Expected valid ID, got empty string\")\n}\n\nfunc TestGetPostsInRangePagination(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttestCases := []struct {\n\t\tpage int\n\t\tpageSize int\n\t\texpectedNumOfPosts int\n\t}{\n\t\t{page: 1, pageSize: 1, expectedNumOfPosts: 1}, // one per page\n\t\t{page: 2, pageSize: 1, expectedNumOfPosts: 1}, // one on second page\n\t\t{page: 1, pageSize: numOfPosts, expectedNumOfPosts: numOfPosts}, // all posts on single page\n\t\t{page: 12, pageSize: 1, expectedNumOfPosts: 0}, // empty page\n\t\t{page: 1, pageSize: numOfPosts + 1, expectedNumOfPosts: numOfPosts}, // page with fewer posts than its size\n\t\t{page: 5, pageSize: numOfPosts / 5, expectedNumOfPosts: 1}, // evenly distribute posts per page\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(ufmt.Sprintf(\"Page%d_Size%d\", tc.page, tc.pageSize), func(t *testing.T) {\n\t\t\tresult := m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), tc.page, tc.pageSize, \"DATE_CREATED\")\n\n\t\t\t// Count posts by how many times id: shows up in JSON string\n\t\t\tpostCount := strings.Count(result, `\"id\":\"`)\n\t\t\tuassert.Equal(t, tc.expectedNumOfPosts, postCount)\n\t\t})\n\t}\n}\n\nfunc TestGetPostsInRangeByTimestamp(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(), // end at latest post\n\t\t1, // first page\n\t\tnumOfPosts, // all memes on the page\n\t\t\"DATE_CREATED\", // sort by newest first\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if data is there\n\tfor _, expData := range memeData {\n\t\tcheck := strings.Contains(jsonStr, expData)\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s in the JSON string, but counld't find it\", expData))\n\t}\n\n\t// Check if ordering is correct, sort by created date\n\tfor i := 0; i \u003c len(memeData)-2; i++ {\n\t\tcheck := strings.Index(jsonStr, memeData[i]) \u003e= strings.Index(jsonStr, memeData[i+1])\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s, but was at %d, and %d\", memeData[i], memeData[i+1], i, i+1))\n\t}\n}\n\nfunc TestGetPostsInRangeByUpvote(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tmemeData1 := \"Meme #1\"\n\tmemeData2 := \"Meme #2\"\n\n\t// Create posts at specific times for testing\n\tid1 := m.PostMeme(memeData1, now.Unix())\n\tid2 := m.PostMeme(memeData2, now.Add(time.Minute).Unix())\n\n\tm.Upvote(id1)\n\tm.Upvote(id2)\n\n\t// Change caller so avoid double upvote panic\n\tstd.TestSetOrigCaller(testutils.TestAddress(\"alice\"))\n\tm.Upvote(id1)\n\n\t// Final upvote count:\n\t// Meme #1 - 2 upvote\n\t// Meme #2 - 1 upvotes\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-time.Minute)\n\tafterLatest := now.Add(time.Hour)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(), // end at latest post\n\t\t1, // first page\n\t\t2, // all memes on the page\n\t\t\"UPVOTES\", // sort by upvote\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if ordering is correct\n\tcheck := strings.Index(jsonStr, \"Meme #1\") \u003c= strings.Index(jsonStr, \"Meme #2\")\n\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s\", memeData1, memeData2))\n}\n\nfunc TestBadSortBy(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttests := []struct {\n\t\tname string\n\t\tsortBy string\n\t\twantPanic string\n\t}{\n\t\t{\n\t\t\tname: \"Empty sortBy\",\n\t\t\tsortBy: \"\",\n\t\t\twantPanic: \"runtime error: index out of range\",\n\t\t},\n\t\t{\n\t\t\tname: \"Wrong sortBy\",\n\t\t\tsortBy: \"random string\",\n\t\t\twantPanic: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\t// Panics should be caught\n\t\t\t_ = m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), 1, 1, tc.sortBy)\n\t\t})\n\t}\n}\n\nfunc TestNoPosts(t *testing.T) {\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\n\tjsonStr := m.GetPostsInRange(0, now, 1, 1, \"DATE_CREATED\")\n\n\tuassert.Equal(t, jsonStr, \"[]\")\n}\n\nfunc TestUpvote(t *testing.T) {\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\tpostID := m.PostMeme(\"Test meme data\", now)\n\n\t// Initial upvote count should be 0\n\tpost := m.getPost(postID)\n\tuassert.Equal(t, 0, post.UpvoteTracker.Size())\n\n\t// Upvote the post\n\tupvoteResult := m.Upvote(postID)\n\tuassert.Equal(t, \"upvote successful\", upvoteResult)\n\n\t// Retrieve the post again and check the upvote count\n\tpost = m.getPost(postID)\n\tuassert.Equal(t, 1, post.UpvoteTracker.Size())\n}\n\nfunc TestDelete(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tstd.TestSetOrigCaller(alice)\n\n\t// Alice is admin\n\tm := NewMemeland()\n\n\t// Set caller to Bob\n\tbob := testutils.TestAddress(\"bob\")\n\tstd.TestSetOrigCaller(bob)\n\n\t// Bob adds post to Memeland\n\tnow := time.Now()\n\tpostID := m.PostMeme(\"Meme #1\", now.Unix())\n\n\t// Alice removes Bob's post\n\tstd.TestSetOrigCaller(alice)\n\n\tid := m.RemovePost(postID)\n\tuassert.Equal(t, postID, id, \"post IDs not matching\")\n\tuassert.Equal(t, 0, len(m.Posts), \"there should be 0 posts after removing\")\n}\n\nfunc TestDeleteByNonAdmin(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tstd.TestSetOrigCaller(alice)\n\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now()\n\tpostID := m.PostMeme(\"Meme #1\", now.Unix())\n\n\t// Bob will try to delete meme posted by Alice, which should fail\n\tbob := testutils.TestAddress(\"bob\")\n\tstd.TestSetOrigCaller(bob)\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t}\n\t}()\n\n\t// Should panic - caught by defer\n\tm.RemovePost(postID)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"merkle","path":"gno.land/p/demo/merkle","files":[{"name":"README.md","body":"# p/demo/merkle\n\nThis package implement a merkle tree that is complient with [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n## [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n```javascript\nconst { MerkleTree } = require(\"merkletreejs\");\nconst SHA256 = require(\"crypto-js/sha256\");\n\nlet leaves = [];\nfor (let i = 0; i \u003c 10; i++) {\n leaves.push(SHA256(`node_${i}`));\n}\n\nconst tree = new MerkleTree(leaves, SHA256);\nconst root = tree.getRoot().toString(\"hex\");\n\nconsole.log(root); // cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\n```\n"},{"name":"merkle.gno","body":"package merkle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\ntype Hashable interface {\n\tBytes() []byte\n}\n\ntype nodes []Node\n\ntype Node struct {\n\thash []byte\n\n\tposition uint8\n}\n\nfunc NewNode(hash []byte, position uint8) Node {\n\treturn Node{\n\t\thash: hash,\n\t\tposition: position,\n\t}\n}\n\nfunc (n Node) Position() uint8 {\n\treturn n.position\n}\n\nfunc (n Node) Hash() string {\n\treturn hex.EncodeToString(n.hash[:])\n}\n\ntype Tree struct {\n\tlayers []nodes\n}\n\n// Root return the merkle root of the tree\nfunc (t *Tree) Root() string {\n\tfor _, l := range t.layers {\n\t\tif len(l) == 1 {\n\t\t\treturn l[0].Hash()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// NewTree create a new Merkle Tree\nfunc NewTree(data []Hashable) *Tree {\n\ttree := \u0026Tree{}\n\n\tleaves := make([]Node, len(data))\n\n\tfor i, d := range data {\n\t\thash := sha256.Sum256(d.Bytes())\n\t\tleaves[i] = Node{hash: hash[:]}\n\t}\n\n\ttree.layers = []nodes{nodes(leaves)}\n\n\tvar buff bytes.Buffer\n\tfor len(leaves) \u003e 1 {\n\t\tlevel := make([]Node, 0, len(leaves)/2+1)\n\t\tfor i := 0; i \u003c len(leaves); i += 2 {\n\t\t\tbuff.Reset()\n\n\t\t\tif i \u003c len(leaves)-1 {\n\t\t\t\tbuff.Write(leaves[i].hash)\n\t\t\t\tbuff.Write(leaves[i+1].hash)\n\t\t\t\thash := sha256.Sum256(buff.Bytes())\n\t\t\t\tlevel = append(level, Node{\n\t\t\t\t\thash: hash[:],\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tlevel = append(level, leaves[i])\n\t\t\t}\n\t\t}\n\t\tleaves = level\n\t\ttree.layers = append(tree.layers, level)\n\t}\n\treturn tree\n}\n\n// Proof return a MerkleProof\nfunc (t *Tree) Proof(data Hashable) ([]Node, error) {\n\ttargetHash := sha256.Sum256(data.Bytes())\n\ttargetIndex := -1\n\n\tfor i, layer := range t.layers[0] {\n\t\tif bytes.Equal(targetHash[:], layer.hash) {\n\t\t\ttargetIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetIndex == -1 {\n\t\treturn nil, errors.New(\"target not found\")\n\t}\n\n\tproofs := make([]Node, 0, len(t.layers))\n\n\tfor _, layer := range t.layers {\n\t\tvar pairIndex int\n\n\t\tif targetIndex%2 == 0 {\n\t\t\tpairIndex = targetIndex + 1\n\t\t} else {\n\t\t\tpairIndex = targetIndex - 1\n\t\t}\n\t\tif pairIndex \u003c len(layer) {\n\t\t\tproofs = append(proofs, Node{\n\t\t\t\thash: layer[pairIndex].hash,\n\t\t\t\tposition: uint8(targetIndex) % 2,\n\t\t\t})\n\t\t}\n\t\ttargetIndex /= 2\n\t}\n\treturn proofs, nil\n}\n\n// Verify if a merkle proof is valid\nfunc (t *Tree) Verify(leaf Hashable, proofs []Node) bool {\n\treturn Verify(t.Root(), leaf, proofs)\n}\n\n// Verify if a merkle proof is valid\nfunc Verify(root string, leaf Hashable, proofs []Node) bool {\n\thash := sha256.Sum256(leaf.Bytes())\n\n\tfor i := 0; i \u003c len(proofs); i += 1 {\n\t\tvar h []byte\n\t\tif proofs[i].position == 0 {\n\t\t\th = append(hash[:], proofs[i].hash...)\n\t\t} else {\n\t\t\th = append(proofs[i].hash, hash[:]...)\n\t\t}\n\t\thash = sha256.Sum256(h)\n\t}\n\treturn hex.EncodeToString(hash[:]) == root\n}\n"},{"name":"merkle_test.gno","body":"package merkle\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype testData struct {\n\tcontent string\n}\n\nfunc (d testData) Bytes() []byte {\n\treturn []byte(d.content)\n}\n\nfunc TestMerkleTree(t *testing.T) {\n\ttests := []struct {\n\t\tsize int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tsize: 1,\n\t\t\texpected: \"cf9f824bce7f5bc63d557b23591f58577f53fe29f974a615bdddbd0140f912f4\",\n\t\t},\n\t\t{\n\t\t\tsize: 3,\n\t\t\texpected: \"1a4a5f0fa267244bf9f74a63fdf2a87eed5e97e4bd104a9e94728c8fb5442177\",\n\t\t},\n\t\t{\n\t\t\tsize: 10,\n\t\t\texpected: \"cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\",\n\t\t},\n\t\t{\n\t\t\tsize: 1000,\n\t\t\texpected: \"fa533d2efdf12be26bc410dfa42936ac63361324e35e9b1ff54d422a1dd2388b\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar leaves []Hashable\n\t\tfor i := 0; i \u003c test.size; i++ {\n\t\t\tleaves = append(leaves, testData{fmt.Sprintf(\"node_%d\", i)})\n\t\t}\n\n\t\ttree := NewTree(leaves)\n\n\t\tif tree == nil {\n\t\t\tt.Error(\"Merkle tree creation failed\")\n\t\t}\n\n\t\troot := tree.Root()\n\n\t\tif root != test.expected {\n\t\t\tt.Fatalf(\"merkle.Tree.Root(), expected: %s; got: %s\", test.expected, root)\n\t\t}\n\n\t\tfor _, leaf := range leaves {\n\t\t\tproofs, err := tree.Proof(leaf)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to proof leaf: %v, on tree: %v\", leaf, test)\n\t\t\t}\n\n\t\t\tok := Verify(root, leaf, proofs)\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"failed to verify leaf: %v, on tree: %v\", leaf, tree)\n\t\t\t}\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"microblog","path":"gno.land/p/demo/microblog","files":[{"name":"microblog.gno","body":"package microblog\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrNotFound = errors.New(\"not found\")\n\tStatusNotFound = \"404\"\n)\n\ntype Microblog struct {\n\tTitle string\n\tPrefix string // i.e. r/gnoland/blog:\n\tPages avl.Tree // author (string) -\u003e Page\n}\n\nfunc NewMicroblog(title string, prefix string) (m *Microblog) {\n\treturn \u0026Microblog{\n\t\tTitle: title,\n\t\tPrefix: prefix,\n\t\tPages: avl.Tree{},\n\t}\n}\n\nfunc (m *Microblog) GetPages() []*Page {\n\tvar (\n\t\tpages = make([]*Page, m.Pages.Size())\n\t\tindex = 0\n\t)\n\n\tm.Pages.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpages[index] = value.(*Page)\n\t\tindex++\n\t\treturn false\n\t})\n\n\tsort.Sort(byLastPosted(pages))\n\n\treturn pages\n}\n\nfunc (m *Microblog) NewPost(text string) error {\n\tauthor := std.GetOrigCaller()\n\t_, found := m.Pages.Get(author.String())\n\tif !found {\n\t\t// make a new page for the new author\n\t\tm.Pages.Set(author.String(), \u0026Page{\n\t\t\tAuthor: author,\n\t\t\tCreatedAt: time.Now(),\n\t\t})\n\t}\n\n\tpage, err := m.GetPage(author.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn page.NewPost(text)\n}\n\nfunc (m *Microblog) GetPage(author string) (*Page, error) {\n\tsilo, found := m.Pages.Get(author)\n\tif !found {\n\t\treturn nil, ErrNotFound\n\t}\n\treturn silo.(*Page), nil\n}\n\ntype Page struct {\n\tID int\n\tAuthor std.Address\n\tCreatedAt time.Time\n\tLastPosted time.Time\n\tPosts avl.Tree // time -\u003e Post\n}\n\n// byLastPosted implements sort.Interface for []Page based on\n// the LastPosted field.\ntype byLastPosted []*Page\n\nfunc (a byLastPosted) Len() int { return len(a) }\nfunc (a byLastPosted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }\n\nfunc (p *Page) NewPost(text string) error {\n\tnow := time.Now()\n\tp.LastPosted = now\n\tp.Posts.Set(ufmt.Sprintf(\"%s%d\", now.Format(time.RFC3339), p.Posts.Size()), \u0026Post{\n\t\tID: p.Posts.Size(),\n\t\tText: text,\n\t\tCreatedAt: now,\n\t})\n\treturn nil\n}\n\nfunc (p *Page) GetPosts() []*Post {\n\tposts := make([]*Post, p.Posts.Size())\n\ti := 0\n\tp.Posts.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpostParsed := value.(*Post)\n\t\tposts[i] = postParsed\n\t\ti++\n\t\treturn false\n\t})\n\treturn posts\n}\n\n// Post lists the specific update\ntype Post struct {\n\tID int\n\tCreatedAt time.Time\n\tText string\n}\n\nfunc (p *Post) String() string {\n\treturn \"\u003e \" + strings.ReplaceAll(p.Text, \"\\n\", \"\\n\u003e\\n\u003e\") + \"\\n\u003e\\n\u003e *\" + p.CreatedAt.Format(time.RFC1123) + \"*\"\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"nestedpkg","path":"gno.land/p/demo/nestedpkg","files":[{"name":"nestedpkg.gno","body":"// Package nestedpkg provides helpers for package-path based access control.\n// It is useful for upgrade patterns relying on namespaces.\npackage nestedpkg\n\n// To test this from a realm and have std.CurrentRealm/PrevRealm work correctly,\n// this file is tested from gno.land/r/demo/tests/nestedpkg_test.gno\n// XXX: move test to ths directory once we support testing a package and\n// specifying values for both PrevRealm and CurrentRealm.\n\nimport (\n\t\"std\"\n\t\"strings\"\n)\n\n// IsCallerSubPath checks if the caller realm is located in a subfolder of the current realm.\nfunc IsCallerSubPath() bool {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(prev, cur)\n}\n\n// AssertCallerIsSubPath panics if IsCallerSubPath returns false.\nfunc AssertCallerIsSubPath() {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(prev, cur) {\n\t\tpanic(\"call restricted to nested packages. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// IsCallerParentPath checks if the caller realm is located in a parent location of the current realm.\nfunc IsCallerParentPath() bool {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(cur, prev)\n}\n\n// AssertCallerIsParentPath panics if IsCallerParentPath returns false.\nfunc AssertCallerIsParentPath() {\n\tvar (\n\t\tcur = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.PrevRealm().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(cur, prev) {\n\t\tpanic(\"call restricted to parent packages. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// IsSameNamespace checks if the caller realm and the current realm are in the same namespace.\nfunc IsSameNamespace() bool {\n\tvar (\n\t\tcur = nsFromPath(std.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(std.PrevRealm().PkgPath()) + \"/\"\n\t)\n\treturn cur == prev\n}\n\n// AssertIsSameNamespace panics if IsSameNamespace returns false.\nfunc AssertIsSameNamespace() {\n\tvar (\n\t\tcur = nsFromPath(std.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(std.PrevRealm().PkgPath()) + \"/\"\n\t)\n\tif cur != prev {\n\t\tpanic(\"call restricted to packages from the same namespace. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// nsFromPath extracts the namespace from a package path.\nfunc nsFromPath(pkgpath string) string {\n\tparts := strings.Split(pkgpath, \"/\")\n\n\t// Specifically for gno.land, potential paths are in the form of DOMAIN/r/NAMESPACE/...\n\t// XXX: Consider extra checks.\n\t// XXX: Support non gno.land domains, where p/ and r/ won't be enforced.\n\tif len(parts) \u003e= 3 {\n\t\treturn parts[2]\n\t}\n\treturn \"\"\n}\n\n// XXX: Consider adding IsCallerDirectlySubPath\n// XXX: Consider adding IsCallerDirectlyParentPath\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"authorizable","path":"gno.land/p/demo/ownable/exts/authorizable","files":[{"name":"authorizable.gno","body":"// Package authorizable is an extension of p/demo/ownable;\n// It allows the user to instantiate an Authorizable struct, which extends\n// p/demo/ownable with a list of users that are authorized for something.\n// By using authorizable, you have a superuser (ownable), as well as another\n// authorization level, which can be used for adding moderators or similar to your realm.\npackage authorizable\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Authorizable struct {\n\t*ownable.Ownable // owner in ownable is superuser\n\tauthorized *avl.Tree // std.Addr \u003e struct{}{}\n}\n\nfunc NewAuthorizable() *Authorizable {\n\ta := \u0026Authorizable{\n\t\townable.New(),\n\t\tavl.NewTree(),\n\t}\n\n\t// Add owner to auth list\n\ta.authorized.Set(a.Owner().String(), struct{}{})\n\treturn a\n}\n\nfunc NewAuthorizableWithAddress(addr std.Address) *Authorizable {\n\ta := \u0026Authorizable{\n\t\townable.NewWithAddress(addr),\n\t\tavl.NewTree(),\n\t}\n\n\t// Add owner to auth list\n\ta.authorized.Set(a.Owner().String(), struct{}{})\n\treturn a\n}\n\nfunc (a *Authorizable) AddToAuthList(addr std.Address) error {\n\tif err := a.CallerIsOwner(); err != nil {\n\t\treturn ErrNotSuperuser\n\t}\n\n\tif _, exists := a.authorized.Get(addr.String()); exists {\n\t\treturn ErrAlreadyInList\n\t}\n\n\ta.authorized.Set(addr.String(), struct{}{})\n\n\treturn nil\n}\n\nfunc (a *Authorizable) DeleteFromAuthList(addr std.Address) error {\n\tif err := a.CallerIsOwner(); err != nil {\n\t\treturn ErrNotSuperuser\n\t}\n\n\tif !a.authorized.Has(addr.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\n\tif _, removed := a.authorized.Remove(addr.String()); !removed {\n\t\tstr := ufmt.Sprintf(\"authorizable: could not remove %s from auth list\", addr.String())\n\t\tpanic(str)\n\t}\n\n\treturn nil\n}\n\nfunc (a Authorizable) CallerOnAuthList() error {\n\tcaller := std.PrevRealm().Addr()\n\n\tif !a.authorized.Has(caller.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\n\treturn nil\n}\n\nfunc (a Authorizable) AssertOnAuthList() {\n\tcaller := std.PrevRealm().Addr()\n\n\tif !a.authorized.Has(caller.String()) {\n\t\tpanic(ErrNotInAuthList)\n\t}\n}\n"},{"name":"authorizable_test.gno","body":"package authorizable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestNewAuthorizable(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // TODO(bug, issue #2371): should not be needed\n\n\ta := NewAuthorizable()\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestNewAuthorizableWithAddress(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestCallerOnAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.CallerOnAuthList(); err == ErrNotInAuthList {\n\t\tt.Fatalf(\"expected alice to be on the list\")\n\t}\n}\n\nfunc TestNotCallerOnAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\tif err := a.CallerOnAuthList(); err == nil {\n\t\tt.Fatalf(\"expected bob to not be on the list\")\n\t}\n}\n\nfunc TestAddToAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.AddToAuthList(bob); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\tif err := a.AddToAuthList(bob); err == nil {\n\t\tt.Fatalf(\"Expected AddToAuth to error while bob called it, but it didn't\")\n\t}\n}\n\nfunc TestDeleteFromList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.AddToAuthList(bob); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tif err := a.AddToAuthList(charlie); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\t// Try an unauthorized deletion\n\tif err := a.DeleteFromAuthList(alice); err == nil {\n\t\tt.Fatalf(\"Expected DelFromAuth to error with %v\", err)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\n\tif err := a.DeleteFromAuthList(charlie); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n}\n\nfunc TestAssertOnList(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice)\n\ta := NewAuthorizableWithAddress(alice)\n\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\tstd.TestSetOrigCaller(bob)\n\n\tuassert.PanicsWithMessage(t, ErrNotInAuthList.Error(), func() {\n\t\ta.AssertOnAuthList()\n\t})\n}\n"},{"name":"errors.gno","body":"package authorizable\n\nimport \"errors\"\n\nvar (\n\tErrNotInAuthList = errors.New(\"authorizable: caller is not in authorized list\")\n\tErrNotSuperuser = errors.New(\"authorizable: caller is not superuser\")\n\tErrAlreadyInList = errors.New(\"authorizable: address is already in authorized list\")\n)\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"pausable","path":"gno.land/p/demo/pausable","files":[{"name":"pausable.gno","body":"package pausable\n\nimport \"gno.land/p/demo/ownable\"\n\ntype Pausable struct {\n\t*ownable.Ownable\n\tpaused bool\n}\n\n// New returns a new Pausable struct with non-paused state as default\nfunc New() *Pausable {\n\treturn \u0026Pausable{\n\t\tOwnable: ownable.New(),\n\t\tpaused: false,\n\t}\n}\n\n// NewFromOwnable is the same as New, but with a pre-existing top-level ownable\nfunc NewFromOwnable(ownable *ownable.Ownable) *Pausable {\n\treturn \u0026Pausable{\n\t\tOwnable: ownable,\n\t\tpaused: false,\n\t}\n}\n\n// IsPaused checks if Pausable is paused\nfunc (p Pausable) IsPaused() bool {\n\treturn p.paused\n}\n\n// Pause sets the state of Pausable to true, meaning all pausable functions are paused\nfunc (p *Pausable) Pause() error {\n\tif err := p.CallerIsOwner(); err != nil {\n\t\treturn err\n\t}\n\n\tp.paused = true\n\treturn nil\n}\n\n// Unpause sets the state of Pausable to false, meaning all pausable functions are resumed\nfunc (p *Pausable) Unpause() error {\n\tif err := p.CallerIsOwner(); err != nil {\n\t\treturn err\n\t}\n\n\tp.paused = false\n\treturn nil\n}\n"},{"name":"pausable_test.gno","body":"package pausable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tfirstCaller = std.Address(\"g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de\")\n\tsecondCaller = std.Address(\"g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa\")\n)\n\nfunc TestNew(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\n\turequire.False(t, result.paused, \"Expected result to be unpaused\")\n\turequire.Equal(t, firstCaller.String(), result.Owner().String())\n}\n\nfunc TestNewFromOwnable(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\to := ownable.New()\n\n\tstd.TestSetOrigCaller(secondCaller)\n\tresult := NewFromOwnable(o)\n\n\turequire.Equal(t, firstCaller.String(), result.Owner().String())\n}\n\nfunc TestSetUnpaused(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\tresult.Unpause()\n\n\turequire.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n}\n\nfunc TestSetPaused(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\tresult.Pause()\n\n\turequire.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n\nfunc TestIsPaused(t *testing.T) {\n\tstd.TestSetOrigCaller(firstCaller)\n\n\tresult := New()\n\turequire.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n\n\tresult.Pause()\n\turequire.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"releases","path":"gno.land/p/demo/releases","files":[{"name":"changelog.gno","body":"package releases\n\ntype changelog struct {\n\tname string\n\treleases []release\n}\n\nfunc NewChangelog(name string) *changelog {\n\treturn \u0026changelog{\n\t\tname: name,\n\t\treleases: make([]release, 0),\n\t}\n}\n\nfunc (c *changelog) NewRelease(version, url, notes string) {\n\tif latest := c.Latest(); latest != nil {\n\t\tlatest.isLatest = false\n\t}\n\n\trelease := release{\n\t\t// manual\n\t\tversion: version,\n\t\turl: url,\n\t\tnotes: notes,\n\n\t\t// internal\n\t\tchangelog: c,\n\t\tisLatest: true,\n\t}\n\n\tc.releases = append(c.releases, release)\n}\n\nfunc (c *changelog) Render(path string) string {\n\tif path == \"\" {\n\t\toutput := \"# \" + c.name + \"\\n\\n\"\n\t\tmax := len(c.releases) - 1\n\t\tmin := 0\n\t\tif max-min \u003e 10 {\n\t\t\tmin = max - 10\n\t\t}\n\t\tfor i := max; i \u003e= min; i-- {\n\t\t\trelease := c.releases[i]\n\t\t\toutput += release.Render()\n\t\t}\n\t\treturn output\n\t}\n\n\trelease := c.ByVersion(path)\n\tif release != nil {\n\t\treturn release.Render()\n\t}\n\n\treturn \"no such release\"\n}\n\nfunc (c *changelog) Latest() *release {\n\tif len(c.releases) \u003e 0 {\n\t\tpos := len(c.releases) - 1\n\t\treturn \u0026c.releases[pos]\n\t}\n\treturn nil\n}\n\nfunc (c *changelog) ByVersion(version string) *release {\n\tfor _, release := range c.releases {\n\t\tif release.version == version {\n\t\t\treturn \u0026release\n\t\t}\n\t}\n\treturn nil\n}\n"},{"name":"release.gno","body":"package releases\n\ntype release struct {\n\t// manual\n\tversion string\n\turl string\n\tnotes string\n\n\t// internal\n\tisLatest bool\n\tchangelog *changelog\n}\n\nfunc (r *release) URL() string { return r.url }\nfunc (r *release) Version() string { return r.version }\nfunc (r *release) Notes() string { return r.notes }\nfunc (r *release) IsLatest() bool { return r.isLatest }\n\nfunc (r *release) Title() string {\n\toutput := r.changelog.name + \" \" + r.version\n\tif r.isLatest {\n\t\toutput += \" (latest)\"\n\t}\n\treturn output\n}\n\nfunc (r *release) Link() string {\n\treturn \"[\" + r.Title() + \"](\" + r.url + \")\"\n}\n\nfunc (r *release) Render() string {\n\toutput := \"\"\n\toutput += \"## \" + r.Link() + \"\\n\\n\"\n\tif r.notes != \"\" {\n\t\toutput += r.notes + \"\\n\\n\"\n\t}\n\treturn output\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"simpledao","path":"gno.land/p/demo/simpledao","files":[{"name":"dao.gno","body":"package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrInvalidExecutor = errors.New(\"invalid executor provided\")\n\tErrInsufficientProposalFunds = errors.New(\"insufficient funds for proposal\")\n\tErrInsufficientExecuteFunds = errors.New(\"insufficient funds for executing proposal\")\n\tErrProposalExecuted = errors.New(\"proposal already executed\")\n\tErrProposalInactive = errors.New(\"proposal is inactive\")\n\tErrProposalNotAccepted = errors.New(\"proposal is not accepted\")\n)\n\nvar (\n\tminProposalFeeValue int64 = 100 * 1_000_000 // minimum gnot required for a govdao proposal (100 GNOT)\n\tminExecuteFeeValue int64 = 500 * 1_000_000 // minimum gnot required for a govdao proposal (500 GNOT)\n\n\tminProposalFee = std.NewCoin(\"ugnot\", minProposalFeeValue)\n\tminExecuteFee = std.NewCoin(\"ugnot\", minExecuteFeeValue)\n)\n\n// SimpleDAO is a simple DAO implementation\ntype SimpleDAO struct {\n\tproposals *avl.Tree // seqid.ID -\u003e proposal\n\tmembStore membstore.MemberStore\n}\n\n// New creates a new instance of the simpledao DAO\nfunc New(membStore membstore.MemberStore) *SimpleDAO {\n\treturn \u0026SimpleDAO{\n\t\tproposals: avl.NewTree(),\n\t\tmembStore: membStore,\n\t}\n}\n\nfunc (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {\n\t// Make sure the executor is set\n\tif request.Executor == nil {\n\t\treturn 0, ErrInvalidExecutor\n\t}\n\n\tvar (\n\t\tcaller = getDAOCaller()\n\t\tsentCoins = std.GetOrigSend() // Get the sent coins, if any\n\t\tcanCoverFee = sentCoins.AmountOf(\"ugnot\") \u003e= minProposalFee.Amount\n\t)\n\n\t// Check if the proposal is valid\n\tif !s.membStore.IsMember(caller) \u0026\u0026 !canCoverFee {\n\t\treturn 0, ErrInsufficientProposalFunds\n\t}\n\n\t// Create the wrapped proposal\n\tprop := \u0026proposal{\n\t\tauthor: caller,\n\t\tdescription: request.Description,\n\t\texecutor: request.Executor,\n\t\tstatus: dao.Active,\n\t\ttally: newTally(),\n\t\tgetTotalVotingPowerFn: s.membStore.TotalPower,\n\t}\n\n\t// Add the proposal\n\tid, err := s.addProposal(prop)\n\tif err != nil {\n\t\treturn 0, ufmt.Errorf(\"unable to add proposal, %s\", err.Error())\n\t}\n\n\t// Emit the proposal added event\n\tdao.EmitProposalAdded(id, caller)\n\n\treturn id, nil\n}\n\nfunc (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error {\n\t// Verify the GOVDAO member\n\tcaller := getDAOCaller()\n\n\tmember, err := s.membStore.Member(caller)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get govdao member, %s\", err.Error())\n\t}\n\n\t// Check if the proposal exists\n\tpropRaw, err := s.ProposalByID(id)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get proposal %d, %s\", id, err.Error())\n\t}\n\n\tprop := propRaw.(*proposal)\n\n\t// Check the proposal status\n\tif prop.Status() == dao.ExecutionSuccessful ||\n\t\tprop.Status() == dao.ExecutionFailed {\n\t\t// Proposal was already executed, nothing to vote on anymore.\n\t\t//\n\t\t// In fact, the proposal should stop accepting\n\t\t// votes as soon as a 2/3+ majority is reached\n\t\t// on either option, but leaving the ability to vote still,\n\t\t// even if a proposal is accepted, or not accepted,\n\t\t// leaves room for \"principle\" vote decisions to be recorded\n\t\treturn ErrProposalInactive\n\t}\n\n\t// Cast the vote\n\tif err = prop.tally.castVote(member, option); err != nil {\n\t\treturn ufmt.Errorf(\"unable to vote on proposal %d, %s\", id, err.Error())\n\t}\n\n\t// Emit the vote cast event\n\tdao.EmitVoteAdded(id, caller, option)\n\n\t// Check the votes to see if quorum is reached\n\tvar (\n\t\ttotalPower = s.membStore.TotalPower()\n\t\tmajorityPower = (2 * totalPower) / 3\n\t)\n\n\tacceptProposal := func() {\n\t\tprop.status = dao.Accepted\n\n\t\tdao.EmitProposalAccepted(id)\n\t}\n\n\tdeclineProposal := func() {\n\t\tprop.status = dao.NotAccepted\n\n\t\tdao.EmitProposalNotAccepted(id)\n\t}\n\n\tswitch {\n\tcase prop.tally.yays \u003e majorityPower:\n\t\t// 2/3+ voted YES\n\t\tacceptProposal()\n\tcase prop.tally.nays \u003e majorityPower:\n\t\t// 2/3+ voted NO\n\t\tdeclineProposal()\n\tcase prop.tally.abstains \u003e majorityPower:\n\t\t// 2/3+ voted ABSTAIN\n\t\tdeclineProposal()\n\tcase prop.tally.yays+prop.tally.nays+prop.tally.abstains \u003e= totalPower:\n\t\t// Everyone voted, but it's undecided,\n\t\t// hence the proposal can't go through\n\t\tdeclineProposal()\n\tdefault:\n\t\t// Quorum not reached\n\t}\n\n\treturn nil\n}\n\nfunc (s *SimpleDAO) ExecuteProposal(id uint64) error {\n\tvar (\n\t\tcaller = getDAOCaller()\n\t\tsentCoins = std.GetOrigSend() // Get the sent coins, if any\n\t\tcanCoverFee = sentCoins.AmountOf(\"ugnot\") \u003e= minExecuteFee.Amount\n\t)\n\n\t// Check if the non-DAO member can cover the execute fee\n\tif !s.membStore.IsMember(caller) \u0026\u0026 !canCoverFee {\n\t\treturn ErrInsufficientExecuteFunds\n\t}\n\n\t// Check if the proposal exists\n\tpropRaw, err := s.ProposalByID(id)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get proposal %d, %s\", id, err.Error())\n\t}\n\n\tprop := propRaw.(*proposal)\n\n\t// Check if the proposal is executed\n\tif prop.Status() == dao.ExecutionSuccessful ||\n\t\tprop.Status() == dao.ExecutionFailed {\n\t\t// Proposal is already executed\n\t\treturn ErrProposalExecuted\n\t}\n\n\t// Check the proposal status\n\tif prop.Status() != dao.Accepted {\n\t\t// Proposal is not accepted, cannot be executed\n\t\treturn ErrProposalNotAccepted\n\t}\n\n\t// Emit an event when the execution finishes\n\tdefer dao.EmitProposalExecuted(id, prop.status)\n\n\t// Attempt to execute the proposal\n\tif err = prop.executor.Execute(); err != nil {\n\t\tprop.status = dao.ExecutionFailed\n\n\t\treturn ufmt.Errorf(\"error during proposal %d execution, %s\", id, err.Error())\n\t}\n\n\t// Update the proposal status\n\tprop.status = dao.ExecutionSuccessful\n\n\treturn nil\n}\n\n// getDAOCaller returns the DAO caller.\n// XXX: This is not a great way to determine the caller, and it is very unsafe.\n// However, the current MsgRun context does not persist escaping the main() scope.\n// Until a better solution is developed, this enables proposals to be made through a package deployment + init()\nfunc getDAOCaller() std.Address {\n\treturn std.GetOrigCaller()\n}\n"},{"name":"dao_test.gno","body":"package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateMembers generates dummy govdao members\nfunc generateMembers(t *testing.T, count int) []membstore.Member {\n\tt.Helper()\n\n\tmembers := make([]membstore.Member, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tmembers = append(members, membstore.Member{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"member %d\", i)),\n\t\t\tVotingPower: 10,\n\t\t})\n\t}\n\n\treturn members\n}\n\nfunc TestSimpleDAO_Propose(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"invalid executor\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\t_, err := s.Propose(dao.ProposalRequest{})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInvalidExecutor,\n\t\t)\n\t})\n\n\tt.Run(\"caller cannot cover fee\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue-1,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be lower\n\t\t// than the proposal fee\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\n\t\t_, err := s.Propose(dao.ProposalRequest{\n\t\t\tExecutor: ex,\n\t\t})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInsufficientProposalFunds,\n\t\t)\n\n\t\tuassert.False(t, called)\n\t})\n\n\tt.Run(\"proposal added\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\t\t\tdescription = \"Proposal description\"\n\n\t\t\tproposer = testutils.TestAddress(\"proposer\")\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue, // enough to cover\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(addr std.Address) bool {\n\t\t\t\t\treturn addr == proposer\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be enough\n\t\t// to cover the fee\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\t\tstd.TestSetOrigCaller(proposer)\n\n\t\t// Make sure the proposal was added\n\t\tid, err := s.Propose(dao.ProposalRequest{\n\t\t\tDescription: description,\n\t\t\tExecutor: ex,\n\t\t})\n\t\tuassert.NoError(t, err)\n\t\tuassert.False(t, called)\n\n\t\t// Make sure the proposal exists\n\t\tprop, err := s.ProposalByID(id)\n\t\tuassert.NoError(t, err)\n\n\t\tuassert.Equal(t, proposer.String(), prop.Author().String())\n\t\tuassert.Equal(t, description, prop.Description())\n\t\tuassert.Equal(t, dao.Active.String(), prop.Status().String())\n\n\t\tstats := prop.Stats()\n\n\t\tuassert.Equal(t, uint64(0), stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, uint64(0), stats.TotalVotingPower)\n\t})\n}\n\nfunc TestSimpleDAO_VoteOnProposal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"not govdao member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tfetchErr = errors.New(\"fetch error\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(_ std.Address) (membstore.Member, error) {\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, fetchErr\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(0, dao.YesVote),\n\t\t\tfetchErr.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(0, dao.YesVote),\n\t\t\tErrMissingProposal.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"proposal executed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.ExecutionSuccessful,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\tErrProposalInactive,\n\t\t)\n\t})\n\n\tt.Run(\"double vote on proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tmember = membstore.Member{\n\t\t\t\tAddress: voter,\n\t\t\t\tVotingPower: 10,\n\t\t\t}\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn member, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Cast the initial vote\n\t\turequire.NoError(t, prop.tally.castVote(member, dao.YesVote))\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\tErrAlreadyVoted.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"majority accepted\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was accepted\n\t\tuassert.Equal(t, dao.Accepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"majority rejected\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was not accepted\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"majority abstained\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.AbstainVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was not accepted\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"everyone voted, undecided\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// The first half votes yes\n\t\tfor _, m := range members[:len(members)/2] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// The other half votes no\n\t\tfor _, m := range members[len(members)/2:] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal is not active,\n\t\t// since everyone voted, and it was undecided\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"proposal undecided\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// The first quarter votes yes\n\t\tfor _, m := range members[:len(members)/4] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// The second quarter votes no\n\t\tfor _, m := range members[len(members)/4 : len(members)/2] {\n\t\t\tstd.TestSetOrigCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal is still active,\n\t\t// since there wasn't quorum reached on any decision\n\t\tuassert.Equal(t, dao.Active.String(), prop.status.String())\n\t})\n}\n\nfunc TestSimpleDAO_ExecuteProposal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller cannot cover fee\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminExecuteFeeValue-1,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be lower\n\t\t// than the execute fee\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(0),\n\t\t\tErrInsufficientExecuteFunds,\n\t\t)\n\t})\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminExecuteFeeValue,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be enough\n\t\t// so the execution can take place\n\t\tstd.TestSetOrigSend(sentCoins, std.Coins{})\n\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(0),\n\t\t\tErrMissingProposal.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"proposal not accepted\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.NotAccepted,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(id),\n\t\t\tErrProposalNotAccepted,\n\t\t)\n\t})\n\n\tt.Run(\"proposal already executed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestTable := []struct {\n\t\t\tname string\n\t\t\tstatus dao.ProposalStatus\n\t\t}{\n\t\t\t{\n\t\t\t\t\"execution was successful\",\n\t\t\t\tdao.ExecutionSuccessful,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"execution failed\",\n\t\t\t\tdao.ExecutionFailed,\n\t\t\t},\n\t\t}\n\n\t\tfor _, testCase := range testTable {\n\t\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tvar (\n\t\t\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\t\t\tms = \u0026mockMemberStore{\n\t\t\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ts = New(ms)\n\n\t\t\t\t\tprop = \u0026proposal{\n\t\t\t\t\t\tstatus: testCase.status,\n\t\t\t\t\t}\n\t\t\t\t)\n\n\t\t\t\tstd.TestSetOrigCaller(voter)\n\n\t\t\t\t// Add an initial proposal\n\t\t\t\tid, err := s.addProposal(prop)\n\t\t\t\turequire.NoError(t, err)\n\n\t\t\t\t// Attempt to vote on the proposal\n\t\t\t\tuassert.ErrorIs(\n\t\t\t\t\tt,\n\t\t\t\t\ts.ExecuteProposal(id),\n\t\t\t\t\tErrProposalExecuted,\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"execution error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\n\t\t\texecError = errors.New(\"exec error\")\n\n\t\t\tmockExecutor = \u0026mockExecutor{\n\t\t\t\texecuteFn: func() error {\n\t\t\t\t\treturn execError\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Accepted,\n\t\t\t\texecutor: mockExecutor,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(id),\n\t\t\texecError.Error(),\n\t\t)\n\n\t\tuassert.Equal(t, dao.ExecutionFailed.String(), prop.status.String())\n\t})\n\n\tt.Run(\"successful execution\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tcalled = false\n\t\t\tmockExecutor = \u0026mockExecutor{\n\t\t\t\texecuteFn: func() error {\n\t\t\t\t\tcalled = true\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Accepted,\n\t\t\t\texecutor: mockExecutor,\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.NoError(t, s.ExecuteProposal(id))\n\t\tuassert.Equal(t, dao.ExecutionSuccessful.String(), prop.status.String())\n\t\tuassert.True(t, called)\n\t})\n}\n"},{"name":"mock_test.gno","body":"package simpledao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/membstore\"\n)\n\ntype executeDelegate func() error\n\ntype mockExecutor struct {\n\texecuteFn executeDelegate\n}\n\nfunc (m *mockExecutor) Execute() error {\n\tif m.executeFn != nil {\n\t\treturn m.executeFn()\n\t}\n\n\treturn nil\n}\n\ntype (\n\tmembersDelegate func(uint64, uint64) []membstore.Member\n\tsizeDelegate func() int\n\tisMemberDelegate func(std.Address) bool\n\ttotalPowerDelegate func() uint64\n\tmemberDelegate func(std.Address) (membstore.Member, error)\n\taddMemberDelegate func(membstore.Member) error\n\tupdateMemberDelegate func(std.Address, membstore.Member) error\n)\n\ntype mockMemberStore struct {\n\tmembersFn membersDelegate\n\tsizeFn sizeDelegate\n\tisMemberFn isMemberDelegate\n\ttotalPowerFn totalPowerDelegate\n\tmemberFn memberDelegate\n\taddMemberFn addMemberDelegate\n\tupdateMemberFn updateMemberDelegate\n}\n\nfunc (m *mockMemberStore) Members(offset, count uint64) []membstore.Member {\n\tif m.membersFn != nil {\n\t\treturn m.membersFn(offset, count)\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockMemberStore) Size() int {\n\tif m.sizeFn != nil {\n\t\treturn m.sizeFn()\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockMemberStore) IsMember(address std.Address) bool {\n\tif m.isMemberFn != nil {\n\t\treturn m.isMemberFn(address)\n\t}\n\n\treturn false\n}\n\nfunc (m *mockMemberStore) TotalPower() uint64 {\n\tif m.totalPowerFn != nil {\n\t\treturn m.totalPowerFn()\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockMemberStore) Member(address std.Address) (membstore.Member, error) {\n\tif m.memberFn != nil {\n\t\treturn m.memberFn(address)\n\t}\n\n\treturn membstore.Member{}, nil\n}\n\nfunc (m *mockMemberStore) AddMember(member membstore.Member) error {\n\tif m.addMemberFn != nil {\n\t\treturn m.addMemberFn(member)\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockMemberStore) UpdateMember(address std.Address, member membstore.Member) error {\n\tif m.updateMemberFn != nil {\n\t\treturn m.updateMemberFn(address, member)\n\t}\n\n\treturn nil\n}\n"},{"name":"propstore.gno","body":"package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar ErrMissingProposal = errors.New(\"proposal is missing\")\n\n// maxRequestProposals is the maximum number of\n// paginated proposals that can be requested\nconst maxRequestProposals = 10\n\n// proposal is the internal simpledao proposal implementation\ntype proposal struct {\n\tauthor std.Address // initiator of the proposal\n\tdescription string // description of the proposal\n\n\texecutor dao.Executor // executor for the proposal\n\tstatus dao.ProposalStatus // status of the proposal\n\n\ttally *tally // voting tally\n\tgetTotalVotingPowerFn func() uint64 // callback for the total voting power\n}\n\nfunc (p *proposal) Author() std.Address {\n\treturn p.author\n}\n\nfunc (p *proposal) Description() string {\n\treturn p.description\n}\n\nfunc (p *proposal) Status() dao.ProposalStatus {\n\treturn p.status\n}\n\nfunc (p *proposal) Executor() dao.Executor {\n\treturn p.executor\n}\n\nfunc (p *proposal) Stats() dao.Stats {\n\t// Get the total voting power of the body\n\ttotalPower := p.getTotalVotingPowerFn()\n\n\treturn dao.Stats{\n\t\tYayVotes: p.tally.yays,\n\t\tNayVotes: p.tally.nays,\n\t\tAbstainVotes: p.tally.abstains,\n\t\tTotalVotingPower: totalPower,\n\t}\n}\n\nfunc (p *proposal) IsExpired() bool {\n\treturn false // this proposal never expires\n}\n\nfunc (p *proposal) Render() string {\n\t// Fetch the voting stats\n\tstats := p.Stats()\n\n\toutput := \"\"\n\toutput += ufmt.Sprintf(\"Author: %s\", p.Author().String())\n\toutput += \"\\n\\n\"\n\toutput += p.Description()\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Status: %s\", p.Status().String())\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\n\t\t\"Voting stats: YES %d (%d%%), NO %d (%d%%), ABSTAIN %d (%d%%), MISSING VOTE %d (%d%%)\",\n\t\tstats.YayVotes,\n\t\tstats.YayPercent(),\n\t\tstats.NayVotes,\n\t\tstats.NayPercent(),\n\t\tstats.AbstainVotes,\n\t\tstats.AbstainPercent(),\n\t\tstats.MissingVotes(),\n\t\tstats.MissingVotesPercent(),\n\t)\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Threshold met: %t\", stats.YayVotes \u003e (2*stats.TotalVotingPower)/3)\n\n\treturn output\n}\n\n// addProposal adds a new simpledao proposal to the store\nfunc (s *SimpleDAO) addProposal(proposal *proposal) (uint64, error) {\n\t// See what the next proposal number should be\n\tnextID := uint64(s.proposals.Size())\n\n\t// Save the proposal\n\ts.proposals.Set(getProposalID(nextID), proposal)\n\n\treturn nextID, nil\n}\n\nfunc (s *SimpleDAO) Proposals(offset, count uint64) []dao.Proposal {\n\t// Check the requested count\n\tif count \u003c 1 {\n\t\treturn []dao.Proposal{}\n\t}\n\n\t// Limit the maximum number of returned proposals\n\tif count \u003e maxRequestProposals {\n\t\tcount = maxRequestProposals\n\t}\n\n\tvar (\n\t\tstartIndex = offset\n\t\tendIndex = startIndex + count\n\n\t\tnumProposals = uint64(s.proposals.Size())\n\t)\n\n\t// Check if the current offset has any proposals\n\tif startIndex \u003e= numProposals {\n\t\treturn []dao.Proposal{}\n\t}\n\n\t// Check if the right bound is good\n\tif endIndex \u003e numProposals {\n\t\tendIndex = numProposals\n\t}\n\n\tprops := make([]dao.Proposal, 0)\n\ts.proposals.Iterate(\n\t\tgetProposalID(startIndex),\n\t\tgetProposalID(endIndex),\n\t\tfunc(_ string, val interface{}) bool {\n\t\t\tprop := val.(*proposal)\n\n\t\t\t// Save the proposal\n\t\t\tprops = append(props, prop)\n\n\t\t\treturn false\n\t\t},\n\t)\n\n\treturn props\n}\n\nfunc (s *SimpleDAO) ProposalByID(id uint64) (dao.Proposal, error) {\n\tprop, exists := s.proposals.Get(getProposalID(id))\n\tif !exists {\n\t\treturn nil, ErrMissingProposal\n\t}\n\n\treturn prop.(*proposal), nil\n}\n\nfunc (s *SimpleDAO) Size() int {\n\treturn s.proposals.Size()\n}\n\n// getProposalID generates a sequential proposal ID\n// from the given ID number\nfunc getProposalID(id uint64) string {\n\treturn seqid.ID(id).String()\n}\n"},{"name":"propstore_test.gno","body":"package simpledao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateProposals generates dummy proposals\nfunc generateProposals(t *testing.T, count int) []*proposal {\n\tt.Helper()\n\n\tvar (\n\t\tmembers = generateMembers(t, count)\n\t\tproposals = make([]*proposal, 0, count)\n\t)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tproposal := \u0026proposal{\n\t\t\tauthor: members[i].Address,\n\t\t\tdescription: ufmt.Sprintf(\"proposal %d\", i),\n\t\t\tstatus: dao.Active,\n\t\t\ttally: newTally(),\n\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t\texecutor: nil,\n\t\t}\n\n\t\tproposals = append(proposals, proposal)\n\t}\n\n\treturn proposals\n}\n\nfunc equalProposals(t *testing.T, p1, p2 dao.Proposal) {\n\tt.Helper()\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Author().String(),\n\t\tp2.Author().String(),\n\t)\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Description(),\n\t\tp2.Description(),\n\t)\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Status().String(),\n\t\tp2.Status().String(),\n\t)\n\n\tp1Stats := p1.Stats()\n\tp2Stats := p2.Stats()\n\n\tuassert.Equal(t, p1Stats.YayVotes, p2Stats.YayVotes)\n\tuassert.Equal(t, p1Stats.NayVotes, p2Stats.NayVotes)\n\tuassert.Equal(t, p1Stats.AbstainVotes, p2Stats.AbstainVotes)\n\tuassert.Equal(t, p1Stats.TotalVotingPower, p2Stats.TotalVotingPower)\n}\n\nfunc TestProposal_Data(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"author\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tauthor: testutils.TestAddress(\"address\"),\n\t\t}\n\n\t\tuassert.Equal(t, p.author, p.Author())\n\t})\n\n\tt.Run(\"description\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tdescription: \"example proposal description\",\n\t\t}\n\n\t\tuassert.Equal(t, p.description, p.Description())\n\t})\n\n\tt.Run(\"status\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tstatus: dao.ExecutionSuccessful,\n\t\t}\n\n\t\tuassert.Equal(t, p.status.String(), p.Status().String())\n\t})\n\n\tt.Run(\"executor\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumCalled = 0\n\t\t\tcb = func() error {\n\t\t\t\tnumCalled++\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\n\t\t\tp = \u0026proposal{\n\t\t\t\texecutor: ex,\n\t\t\t}\n\t\t)\n\n\t\turequire.NoError(t, p.executor.Execute())\n\t\turequire.NoError(t, p.Executor().Execute())\n\n\t\tuassert.Equal(t, 2, numCalled)\n\t})\n\n\tt.Run(\"no votes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\ttally: newTally(),\n\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t}\n\n\t\tstats := p.Stats()\n\n\t\tuassert.Equal(t, uint64(0), stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, uint64(0), stats.TotalVotingPower)\n\t})\n\n\tt.Run(\"existing votes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\t\t\ttotalPower = uint64(len(members)) * 10\n\n\t\t\tp = \u0026proposal{\n\t\t\t\ttally: newTally(),\n\t\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\t\treturn totalPower\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\tfor _, m := range members {\n\t\t\turequire.NoError(t, p.tally.castVote(m, dao.YesVote))\n\t\t}\n\n\t\tstats := p.Stats()\n\n\t\tuassert.Equal(t, totalPower, stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, totalPower, stats.TotalVotingPower)\n\t})\n}\n\nfunc TestSimpleDAO_GetProposals(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no proposals\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\tuassert.Equal(t, 0, s.Size())\n\t\tproposals := s.Proposals(0, 0)\n\n\t\tuassert.Equal(t, 0, len(proposals))\n\t})\n\n\tt.Run(\"proper pagination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumProposals = 20\n\t\t\thalfRange = numProposals / 2\n\n\t\t\ts = New(nil)\n\t\t\tproposals = generateProposals(t, numProposals)\n\t\t)\n\n\t\t// Add initial proposals\n\t\tfor _, proposal := range proposals {\n\t\t\t_, err := s.addProposal(proposal)\n\n\t\t\turequire.NoError(t, err)\n\t\t}\n\n\t\tuassert.Equal(t, numProposals, s.Size())\n\n\t\tfetchedProposals := s.Proposals(0, uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedProposals))\n\n\t\tfor index, fetchedProposal := range fetchedProposals {\n\t\t\tequalProposals(t, proposals[index], fetchedProposal)\n\t\t}\n\n\t\t// Fetch the other half\n\t\tfetchedProposals = s.Proposals(uint64(halfRange), uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedProposals))\n\n\t\tfor index, fetchedProposal := range fetchedProposals {\n\t\t\tequalProposals(t, proposals[index+halfRange], fetchedProposal)\n\t\t}\n\t})\n}\n\nfunc TestSimpleDAO_GetProposalByID(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\t_, err := s.ProposalByID(0)\n\t\tuassert.ErrorIs(t, err, ErrMissingProposal)\n\t})\n\n\tt.Run(\"proposal found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\ts = New(nil)\n\t\t\tproposal = generateProposals(t, 1)[0]\n\t\t)\n\n\t\t// Add the initial proposal\n\t\t_, err := s.addProposal(proposal)\n\t\turequire.NoError(t, err)\n\n\t\t// Fetch the proposal\n\t\tfetchedProposal, err := s.ProposalByID(0)\n\t\turequire.NoError(t, err)\n\n\t\tequalProposals(t, proposal, fetchedProposal)\n\t})\n}\n"},{"name":"votestore.gno","body":"package simpledao\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\nvar ErrAlreadyVoted = errors.New(\"vote already cast\")\n\n// tally is a simple vote tally system\ntype tally struct {\n\t// tally cache to keep track of active\n\t// yes / no / abstain votes\n\tyays uint64\n\tnays uint64\n\tabstains uint64\n\n\tvoters *avl.Tree // std.Address -\u003e dao.VoteOption\n}\n\n// newTally creates a new tally system instance\nfunc newTally() *tally {\n\treturn \u0026tally{\n\t\tvoters: avl.NewTree(),\n\t}\n}\n\n// castVote casts a single vote in the name of the given member\nfunc (t *tally) castVote(member membstore.Member, option dao.VoteOption) error {\n\t// Check if the member voted already\n\taddress := member.Address.String()\n\n\t_, voted := t.voters.Get(address)\n\tif voted {\n\t\treturn ErrAlreadyVoted\n\t}\n\n\t// convert option to upper-case, like the constants are.\n\toption = dao.VoteOption(strings.ToUpper(string(option)))\n\n\t// Update the tally\n\tswitch option {\n\tcase dao.YesVote:\n\t\tt.yays += member.VotingPower\n\tcase dao.AbstainVote:\n\t\tt.abstains += member.VotingPower\n\tcase dao.NoVote:\n\t\tt.nays += member.VotingPower\n\tdefault:\n\t\tpanic(\"invalid voting option: \" + option)\n\t}\n\n\t// Save the voting status\n\tt.voters.Set(address, option)\n\n\treturn nil\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"stack","path":"gno.land/p/demo/stack","files":[{"name":"stack.gno","body":"package stack\n\ntype Stack struct {\n\ttop *node\n\tlength int\n}\n\ntype node struct {\n\tvalue interface{}\n\tprev *node\n}\n\nfunc New() *Stack {\n\treturn \u0026Stack{nil, 0}\n}\n\nfunc (s *Stack) Len() int {\n\treturn s.length\n}\n\nfunc (s *Stack) Top() interface{} {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\treturn s.top.value\n}\n\nfunc (s *Stack) Pop() interface{} {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\n\tnode := s.top\n\ts.top = node.prev\n\ts.length -= 1\n\treturn node.value\n}\n\nfunc (s *Stack) Push(value interface{}) {\n\tnode := \u0026node{value, s.top}\n\ts.top = node\n\ts.length += 1\n}\n"},{"name":"stack_test.gno","body":"package stack\n\nimport \"testing\"\n\nfunc TestStack(t *testing.T) {\n\ts := New() // Empty stack\n\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n\n\ts.Push(1)\n\n\tif s.Len() != 1 {\n\t\tt.Errorf(\"s.Len(): expected 1; got %d\", s.Len())\n\t}\n\n\tif top := s.Top(); top.(int) != 1 {\n\t\tt.Errorf(\"s.Top(): expected 1; got %v\", top.(int))\n\t}\n\n\tif elem := s.Pop(); elem.(int) != 1 {\n\t\tt.Errorf(\"s.Pop(): expected 1; got %v\", elem.(int))\n\t}\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"subscription","path":"gno.land/p/demo/subscription","files":[{"name":"doc.gno","body":"// Package subscription provides a flexible system for managing both recurring and\n// lifetime subscriptions in Gno applications. It enables developers to handle\n// payment-based access control for services or products. The library supports\n// both subscriptions requiring periodic payments (recurring) and one-time payments\n// (lifetime). Subscriptions are tracked using an AVL tree for efficient management\n// of subscription statuses.\n//\n// Usage:\n//\n// Import the required sub-packages (`recurring` and/or `lifetime`) to manage specific\n// subscription types. The methods provided allow users to subscribe, check subscription\n// status, and manage payments.\n//\n// Recurring Subscription:\n//\n// Recurring subscriptions require periodic payments to maintain access.\n// Users pay to extend their access for a specific duration.\n//\n// Example:\n//\n//\t// Create a recurring subscription requiring 100 ugnot every 30 days\n//\trecSub := recurring.NewRecurringSubscription(time.Hour * 24 * 30, 100)\n//\n//\t// Process payment for the recurring subscription\n//\trecSub.Subscribe()\n//\n//\t// Gift a recurring subscription to another user\n//\trecSub.GiftSubscription(recipientAddress)\n//\n//\t// Check if a user has a valid subscription\n//\trecSub.HasValidSubscription(addr)\n//\n//\t// Get the expiration date of the subscription\n//\trecSub.GetExpiration(caller)\n//\n//\t// Update the subscription amount to 200 ugnot\n//\trecSub.UpdateAmount(200)\n//\n//\t// Get the current subscription amount\n//\trecSub.GetAmount()\n//\n// Lifetime Subscription:\n//\n// Lifetime subscriptions require a one-time payment for permanent access.\n// Once paid, users have indefinite access without further payments.\n//\n// Example:\n//\n//\t// Create a lifetime subscription costing 500 ugnot\n//\tlifeSub := lifetime.NewLifetimeSubscription(500)\n//\n//\t// Process payment for lifetime access\n//\tlifeSub.Subscribe()\n//\n//\t// Gift a lifetime subscription to another user\n//\tlifeSub.GiftSubscription(recipientAddress)\n//\n//\t// Check if a user has a valid subscription\n//\tlifeSub.HasValidSubscription(addr)\n//\n//\t// Update the lifetime subscription amount to 1000 ugnot\n//\tlifeSub.UpdateAmount(1000)\n//\n//\t// Get the current lifetime subscription amount\n//\tlifeSub.GetAmount()\npackage subscription\n"},{"name":"subscription.gno","body":"package subscription\n\nimport (\n\t\"std\"\n)\n\n// Subscription interface defines standard methods that all subscription types must implement.\ntype Subscription interface {\n\tHasValidSubscription(std.Address) error\n\tSubscribe() error\n\tUpdateAmount(newAmount int64) error\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"lifetime","path":"gno.land/p/demo/subscription/lifetime","files":[{"name":"errors.gno","body":"package lifetime\n\nimport \"errors\"\n\nvar (\n\tErrNoSub = errors.New(\"lifetime subscription: no active subscription found\")\n\tErrAmt = errors.New(\"lifetime subscription: payment amount does not match the required subscription amount\")\n\tErrAlreadySub = errors.New(\"lifetime subscription: this address already has an active lifetime subscription\")\n\tErrNotAuthorized = errors.New(\"lifetime subscription: action not authorized\")\n)\n"},{"name":"lifetime.gno","body":"package lifetime\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\n// LifetimeSubscription represents a subscription that requires only a one-time payment.\n// It grants permanent access to a service or product.\ntype LifetimeSubscription struct {\n\townable.Ownable\n\tamount int64\n\tsubs *avl.Tree // std.Address -\u003e bool\n}\n\n// NewLifetimeSubscription creates and returns a new lifetime subscription.\nfunc NewLifetimeSubscription(amount int64) *LifetimeSubscription {\n\treturn \u0026LifetimeSubscription{\n\t\tOwnable: *ownable.New(),\n\t\tamount: amount,\n\t\tsubs: avl.NewTree(),\n\t}\n}\n\n// processSubscription handles the subscription process for a given receiver.\nfunc (ls *LifetimeSubscription) processSubscription(receiver std.Address) error {\n\tamount := std.GetOrigSend()\n\n\tif amount.AmountOf(\"ugnot\") != ls.amount {\n\t\treturn ErrAmt\n\t}\n\n\t_, exists := ls.subs.Get(receiver.String())\n\n\tif exists {\n\t\treturn ErrAlreadySub\n\t}\n\n\tls.subs.Set(receiver.String(), true)\n\n\treturn nil\n}\n\n// Subscribe processes the payment for a lifetime subscription.\nfunc (ls *LifetimeSubscription) Subscribe() error {\n\tcaller := std.PrevRealm().Addr()\n\treturn ls.processSubscription(caller)\n}\n\n// GiftSubscription allows the caller to pay for a lifetime subscription for another user.\nfunc (ls *LifetimeSubscription) GiftSubscription(receiver std.Address) error {\n\treturn ls.processSubscription(receiver)\n}\n\n// HasValidSubscription checks if the given address has an active lifetime subscription.\nfunc (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error {\n\t_, exists := ls.subs.Get(addr.String())\n\n\tif !exists {\n\t\treturn ErrNoSub\n\t}\n\n\treturn nil\n}\n\n// UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price.\nfunc (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error {\n\tif err := ls.CallerIsOwner(); err != nil {\n\t\treturn ErrNotAuthorized\n\t}\n\n\tls.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current subscription price.\nfunc (ls *LifetimeSubscription) GetAmount() int64 {\n\treturn ls.amount\n}\n"},{"name":"lifetime_test.gno","body":"package lifetime\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestLifetimeSubscription(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed\")\n\n\terr = ls.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n}\n\nfunc TestLifetimeSubscriptionGift(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := ls.GiftSubscription(bob)\n\tuassert.NoError(t, err, \"Expected ProcessPaymentGift to succeed for Bob\")\n\n\terr = ls.HasValidSubscription(bob)\n\tuassert.NoError(t, err, \"Expected Bob to have access\")\n\n\terr = ls.HasValidSubscription(charlie)\n\tuassert.Error(t, err, \"Expected Charlie to fail access check\")\n}\n\nfunc TestUpdateAmountAuthorization(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\terr := ls.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tstd.TestSetOrigCaller(bob)\n\n\terr = ls.UpdateAmount(3000)\n\tuassert.Error(t, err, \"Expected Bob to fail when updating amount\")\n}\n\nfunc TestIncorrectPaymentAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}}, nil)\n\terr := ls.Subscribe()\n\tuassert.Error(t, err, \"Expected payment to fail with incorrect amount\")\n}\n\nfunc TestMultipleSubscriptionAttempts(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected first subscription to succeed\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = ls.Subscribe()\n\tuassert.Error(t, err, \"Expected second subscription to fail as Alice is already subscribed\")\n}\n\nfunc TestGiftSubscriptionWithIncorrectAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}}, nil)\n\terr := ls.GiftSubscription(bob)\n\tuassert.Error(t, err, \"Expected gift subscription to fail with incorrect amount\")\n\n\terr = ls.HasValidSubscription(bob)\n\tuassert.Error(t, err, \"Expected Bob to not have access after incorrect gift subscription\")\n}\n\nfunc TestUpdateAmountEffectiveness(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\terr := ls.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = ls.Subscribe()\n\tuassert.Error(t, err, \"Expected subscription to fail with old amount after update\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 2000}}, nil)\n\terr = ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected subscription to succeed with new amount\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"recurring","path":"gno.land/p/demo/subscription/recurring","files":[{"name":"errors.gno","body":"package recurring\n\nimport \"errors\"\n\nvar (\n\tErrNoSub = errors.New(\"recurring subscription: no active subscription found\")\n\tErrSubExpired = errors.New(\"recurring subscription: your subscription has expired\")\n\tErrAmt = errors.New(\"recurring subscription: payment amount does not match the required subscription amount\")\n\tErrAlreadySub = errors.New(\"recurring subscription: this address already has an active subscription\")\n\tErrNotAuthorized = errors.New(\"recurring subscription: action not authorized\")\n)\n"},{"name":"recurring.gno","body":"package recurring\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\n// RecurringSubscription represents a subscription that requires periodic payments.\n// It includes the duration of the subscription and the amount required per period.\ntype RecurringSubscription struct {\n\townable.Ownable\n\tduration time.Duration\n\tamount int64\n\tsubs *avl.Tree // std.Address -\u003e time.Time\n}\n\n// NewRecurringSubscription creates and returns a new recurring subscription.\nfunc NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription {\n\treturn \u0026RecurringSubscription{\n\t\tOwnable: *ownable.New(),\n\t\tduration: duration,\n\t\tamount: amount,\n\t\tsubs: avl.NewTree(),\n\t}\n}\n\n// HasValidSubscription verifies if the caller has an active recurring subscription.\nfunc (rs *RecurringSubscription) HasValidSubscription(addr std.Address) error {\n\texpTime, exists := rs.subs.Get(addr.String())\n\tif !exists {\n\t\treturn ErrNoSub\n\t}\n\n\tif time.Now().After(expTime.(time.Time)) {\n\t\treturn ErrSubExpired\n\t}\n\n\treturn nil\n}\n\n// processSubscription processes the payment for a given receiver and renews or adds their subscription.\nfunc (rs *RecurringSubscription) processSubscription(receiver std.Address) error {\n\tamount := std.GetOrigSend()\n\n\tif amount.AmountOf(\"ugnot\") != rs.amount {\n\t\treturn ErrAmt\n\t}\n\n\texpTime, exists := rs.subs.Get(receiver.String())\n\n\t// If the user is already a subscriber but his subscription has expired, authorize renewal\n\tif exists {\n\t\texpiration := expTime.(time.Time)\n\t\tif time.Now().Before(expiration) {\n\t\t\treturn ErrAlreadySub\n\t\t}\n\t}\n\n\t// Renew or add subscription\n\tnewExpiration := time.Now().Add(rs.duration)\n\trs.subs.Set(receiver.String(), newExpiration)\n\n\treturn nil\n}\n\n// Subscribe handles the payment for the caller's subscription.\nfunc (rs *RecurringSubscription) Subscribe() error {\n\tcaller := std.PrevRealm().Addr()\n\n\treturn rs.processSubscription(caller)\n}\n\n// GiftSubscription allows the user to pay for a subscription for another user (receiver).\nfunc (rs *RecurringSubscription) GiftSubscription(receiver std.Address) error {\n\treturn rs.processSubscription(receiver)\n}\n\n// GetExpiration returns the expiration date of the recurring subscription for a given caller.\nfunc (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, error) {\n\texpTime, exists := rs.subs.Get(addr.String())\n\tif !exists {\n\t\treturn time.Time{}, ErrNoSub\n\t}\n\n\treturn expTime.(time.Time), nil\n}\n\n// UpdateAmount allows the owner of the subscription contract to change the required subscription amount.\nfunc (rs *RecurringSubscription) UpdateAmount(newAmount int64) error {\n\tif err := rs.CallerIsOwner(); err != nil {\n\t\treturn ErrNotAuthorized\n\t}\n\n\trs.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current amount required for each subscription period.\nfunc (rs *RecurringSubscription) GetAmount() int64 {\n\treturn rs.amount\n}\n"},{"name":"recurring_test.gno","body":"package recurring\n\nimport (\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestRecurringSubscription(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n\n\texpiration, err := rs.GetExpiration(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected to get expiration for Alice\")\n}\n\nfunc TestRecurringSubscriptionGift(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.GiftSubscription(bob)\n\tuassert.NoError(t, err, \"Expected ProcessPaymentGift to succeed for Bob\")\n\n\terr = rs.HasValidSubscription(bob)\n\tuassert.NoError(t, err, \"Expected Bob to have access\")\n\n\terr = rs.HasValidSubscription(charlie)\n\tuassert.Error(t, err, \"Expected Charlie to fail access check\")\n}\n\nfunc TestRecurringSubscriptionExpiration(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n\n\texpiration := time.Now().Add(-time.Hour * 2)\n\trs.subs.Set(std.PrevRealm().Addr().String(), expiration)\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.Error(t, err, \"Expected Alice's subscription to be expired\")\n}\n\nfunc TestUpdateAmountAuthorization(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\terr := rs.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tstd.TestSetOrigCaller(bob)\n\terr = rs.UpdateAmount(3000)\n\tuassert.Error(t, err, \"Expected Bob to fail when updating amount\")\n}\n\nfunc TestGetAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tamount := rs.GetAmount()\n\tuassert.Equal(t, amount, int64(1000), \"Expected the initial amount to be 1000 ugnot\")\n\n\terr := rs.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tamount = rs.GetAmount()\n\tuassert.Equal(t, amount, int64(2000), \"Expected the updated amount to be 2000 ugnot\")\n}\n\nfunc TestIncorrectPaymentAmount(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}}, nil)\n\terr := rs.Subscribe()\n\tuassert.Error(t, err, \"Expected payment with incorrect amount to fail\")\n}\n\nfunc TestMultiplePaymentsForSameUser(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected first ProcessPayment to succeed for Alice\")\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = rs.Subscribe()\n\tuassert.Error(t, err, \"Expected second ProcessPayment to fail for Alice due to existing subscription\")\n}\n\nfunc TestRecurringSubscriptionWithMultiplePayments(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour, 1000)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected first ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access after first payment\")\n\n\texpiration := time.Now().Add(-time.Hour * 2)\n\trs.subs.Set(std.PrevRealm().Addr().String(), expiration)\n\n\tstd.TestSetOrigSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}}, nil)\n\terr = rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected second ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PrevRealm().Addr())\n\tuassert.NoError(t, err, \"Expected Alice to have access after second payment\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"svg","path":"gno.land/p/demo/svg","files":[{"name":"doc.gno","body":"/*\nPackage svg is a minimalist SVG generation library for Gno.\n\nThe svg package provides a simple and lightweight solution for programmatically generating SVG (Scalable Vector Graphics) markup in Gno. It allows you to create basic shapes like rectangles and circles, and output the generated SVG to a\n\nExample:\n\n\timport \"gno.land/p/demo/svg\"\"\n\n\tfunc Foo() string {\n\t canvas := svg.Canvas{Width: 200, Height: 200}\n\t canvas.DrawRectangle(50, 50, 100, 100, \"red\")\n\t canvas.DrawCircle(100, 100, 50, \"blue\")\n\t return canvas.String()\n\t}\n*/\npackage svg // import \"gno.land/p/demo/svg\"\n"},{"name":"svg.gno","body":"package svg\n\nimport \"gno.land/p/demo/ufmt\"\n\ntype Canvas struct {\n\tWidth int\n\tHeight int\n\tElems []Elem\n}\n\ntype Elem interface{ String() string }\n\nfunc (c Canvas) String() string {\n\toutput := \"\"\n\toutput += ufmt.Sprintf(`\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\"\u003e`, c.Width, c.Height)\n\tfor _, elem := range c.Elems {\n\t\toutput += elem.String()\n\t}\n\toutput += \"\u003c/svg\u003e\"\n\treturn output\n}\n\nfunc (c *Canvas) Append(elem Elem) {\n\tc.Elems = append(c.Elems, elem)\n}\n\ntype Circle struct {\n\tCX int // center X\n\tCY int // center Y\n\tR int // radius\n\tFill string\n}\n\nfunc (c Circle) String() string {\n\treturn ufmt.Sprintf(`\u003ccircle cx=\"%d\" cy=\"%d\" r=\"%d\" fill=\"%s\" /\u003e`, c.CX, c.CY, c.R, c.Fill)\n}\n\nfunc (c *Canvas) DrawCircle(cx, cy, r int, fill string) {\n\tc.Append(Circle{\n\t\tCX: cx,\n\t\tCY: cy,\n\t\tR: r,\n\t\tFill: fill,\n\t})\n}\n\ntype Rectangle struct {\n\tX, Y, Width, Height int\n\tFill string\n}\n\nfunc (c Rectangle) String() string {\n\treturn ufmt.Sprintf(`\u003crect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"%s\" /\u003e`, c.X, c.Y, c.Width, c.Height, c.Fill)\n}\n\nfunc (c *Canvas) DrawRectangle(x, y, width, height int, fill string) {\n\tc.Append(Rectangle{\n\t\tX: x,\n\t\tY: y,\n\t\tWidth: width,\n\t\tHeight: height,\n\t\tFill: fill,\n\t})\n}\n\ntype Text struct {\n\tX, Y int\n\tText, Fill string\n}\n\nfunc (c Text) String() string {\n\treturn ufmt.Sprintf(`\u003ctext x=\"%d\" y=\"%d\" fill=\"%s\"\u003e%s\u003c/text\u003e`, c.X, c.Y, c.Fill, c.Text)\n}\n\nfunc (c *Canvas) DrawText(x, y int, text, fill string) {\n\tc.Append(Text{\n\t\tX: x,\n\t\tY: y,\n\t\tText: text,\n\t\tFill: fill,\n\t})\n}\n"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/p/demo/svg_test\npackage svg_test\n\nimport \"gno.land/p/demo/svg\"\n\nfunc main() {\n\tcanvas := svg.Canvas{Width: 500, Height: 500}\n\tcanvas.DrawRectangle(50, 50, 100, 100, \"red\")\n\tcanvas.DrawCircle(100, 100, 50, \"blue\")\n\tcanvas.DrawText(100, 100, \"hello world!\", \"magenta\")\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"100\" cy=\"100\" r=\"50\" fill=\"blue\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n"},{"name":"z1_filetest.gno","body":"// PKGPATH: gno.land/p/demo/svg_test\npackage svg_test\n\nimport \"gno.land/p/demo/svg\"\n\nfunc main() {\n\tcanvas := svg.Canvas{\n\t\tWidth: 500, Height: 500,\n\t\tElems: []svg.Elem{\n\t\t\tsvg.Rectangle{50, 50, 100, 100, \"red\"},\n\t\t\tsvg.Circle{50, 50, 100, \"red\"},\n\t\t\tsvg.Text{100, 100, \"hello world!\", \"magenta\"},\n\t\t},\n\t}\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"50\" cy=\"50\" r=\"100\" fill=\"red\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tamagotchi","path":"gno.land/p/demo/tamagotchi","files":[{"name":"tamagotchi.gno","body":"package tamagotchi\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Tamagotchi structure\ntype Tamagotchi struct {\n\tname string\n\thunger int\n\thappiness int\n\thealth int\n\tage int\n\tmaxAge int\n\tsleepy int\n\tcreated time.Time\n\tlastUpdated time.Time\n}\n\nfunc New(name string) *Tamagotchi {\n\tnow := time.Now()\n\treturn \u0026Tamagotchi{\n\t\tname: name,\n\t\thunger: 50,\n\t\thappiness: 50,\n\t\thealth: 50,\n\t\tmaxAge: 100,\n\t\tlastUpdated: now,\n\t\tcreated: now,\n\t}\n}\n\nfunc (t *Tamagotchi) Name() string {\n\tt.update()\n\treturn t.name\n}\n\nfunc (t *Tamagotchi) Hunger() int {\n\tt.update()\n\treturn t.hunger\n}\n\nfunc (t *Tamagotchi) Happiness() int {\n\tt.update()\n\treturn t.happiness\n}\n\nfunc (t *Tamagotchi) Health() int {\n\tt.update()\n\treturn t.health\n}\n\nfunc (t *Tamagotchi) Age() int {\n\tt.update()\n\treturn t.age\n}\n\nfunc (t *Tamagotchi) Sleepy() int {\n\tt.update()\n\treturn t.sleepy\n}\n\n// Feed method for Tamagotchi\nfunc (t *Tamagotchi) Feed() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.hunger = bound(t.hunger-10, 0, 100)\n}\n\n// Play method for Tamagotchi\nfunc (t *Tamagotchi) Play() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.happiness = bound(t.happiness+10, 0, 100)\n}\n\n// Heal method for Tamagotchi\nfunc (t *Tamagotchi) Heal() {\n\tt.update()\n\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.health = bound(t.health+10, 0, 100)\n}\n\nfunc (t Tamagotchi) dead() bool { return t.health == 0 }\n\n// Update applies changes based on the duration since the last update\nfunc (t *Tamagotchi) update() {\n\tif t.dead() {\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\tif t.lastUpdated == now {\n\t\treturn\n\t}\n\n\tduration := now.Sub(t.lastUpdated)\n\telapsedMins := int(duration.Minutes())\n\n\tt.hunger = bound(t.hunger+elapsedMins, 0, 100)\n\tt.happiness = bound(t.happiness-elapsedMins, 0, 100)\n\tt.health = bound(t.health-elapsedMins, 0, 100)\n\tt.sleepy = bound(t.sleepy+elapsedMins, 0, 100)\n\n\t// age is hours since created\n\tt.age = int(now.Sub(t.created).Hours())\n\tif t.age \u003e t.maxAge {\n\t\tt.age = t.maxAge\n\t\tt.health = 0\n\t}\n\tif t.health == 0 {\n\t\tt.sleepy = 0\n\t\tt.happiness = 0\n\t\tt.hunger = 0\n\t}\n\n\tt.lastUpdated = now\n}\n\n// Face returns an ASCII art representation of the Tamagotchi's current state\nfunc (t *Tamagotchi) Face() string {\n\tt.update()\n\treturn t.face()\n}\n\nfunc (t *Tamagotchi) face() string {\n\tswitch {\n\tcase t.health == 0:\n\t\treturn \"😵\" // dead face\n\tcase t.health \u003c 30:\n\t\treturn \"😷\" // sick face\n\tcase t.happiness \u003c 30:\n\t\treturn \"😢\" // sad face\n\tcase t.hunger \u003e 70:\n\t\treturn \"😫\" // hungry face\n\tcase t.sleepy \u003e 70:\n\t\treturn \"😴\" // sleepy face\n\tdefault:\n\t\treturn \"😃\" // happy face\n\t}\n}\n\n// Markdown method for Tamagotchi\nfunc (t *Tamagotchi) Markdown() string {\n\tt.update()\n\treturn ufmt.Sprintf(`# %s %s\n\n* age: %d\n* hunger: %d\n* happiness: %d\n* health: %d\n* sleepy: %d`,\n\t\tt.name, t.Face(),\n\t\tt.age, t.hunger, t.happiness, t.health, t.sleepy,\n\t)\n}\n\nfunc bound(n, min, max int) int {\n\tif n \u003c min {\n\t\treturn min\n\t}\n\tif n \u003e max {\n\t\treturn max\n\t}\n\treturn n\n}\n"},{"name":"z0_filetest.gno","body":"package main\n\nimport (\n\t\"time\"\n\n\t\"internal/os_test\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n)\n\nfunc main() {\n\tt := tamagotchi.New(\"Gnome\")\n\n\tprintln(\"\\n-- INITIAL\\n\")\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos_test.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- FEEDx3, PLAYx2, HEALx4\\n\")\n\tt.Feed()\n\tt.Feed()\n\tt.Feed()\n\tt.Play()\n\tt.Play()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos_test.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos_test.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos_test.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n}\n\n// Output:\n// -- INITIAL\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 70\n// * happiness: 30\n// * health: 30\n// * sleepy: 20\n//\n// -- FEEDx3, PLAYx2, HEALx4\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 40\n// * happiness: 50\n// * health: 70\n// * sleepy: 20\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 60\n// * happiness: 30\n// * health: 50\n// * sleepy: 40\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"subtests","path":"gno.land/p/demo/tests/subtests","files":[{"name":"subtests.gno","body":"package subtests\n\nimport (\n\t\"std\"\n)\n\nfunc GetCurrentRealm() std.Realm {\n\treturn std.CurrentRealm()\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"subtests","path":"gno.land/r/demo/tests/subtests","files":[{"name":"subtests.gno","body":"package subtests\n\nimport (\n\t\"std\"\n)\n\nfunc GetCurrentRealm() std.Realm {\n\treturn std.CurrentRealm()\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc CallAssertOriginCall() {\n\tstd.AssertOriginCall()\n}\n\nfunc CallIsOriginCall() bool {\n\treturn std.IsOriginCall()\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tests","path":"gno.land/r/demo/tests","files":[{"name":"README.md","body":"Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n"},{"name":"interfaces.gno","body":"package tests\n\nimport (\n\t\"strconv\"\n)\n\ntype Stringer interface {\n\tString() string\n}\n\nvar stringers []Stringer\n\nfunc AddStringer(str Stringer) {\n\t// NOTE: this is ridiculous, a slice that will become too long\n\t// eventually. Don't do this in production programs; use\n\t// gno.land/p/demo/avl or similar structures.\n\tstringers = append(stringers, str)\n}\n\nfunc Render(path string) string {\n\tres := \"\"\n\t// NOTE: like the function above, this function too will eventually\n\t// become too expensive to call.\n\tfor i, stringer := range stringers {\n\t\tres += strconv.Itoa(i) + \": \" + stringer.String() + \"\\n\"\n\t}\n\treturn res\n}\n"},{"name":"nestedpkg_test.gno","body":"package tests\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\nfunc TestNestedPkg(t *testing.T) {\n\t// direct child\n\tcur := \"gno.land/r/demo/tests/foo\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif !IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// grand-grand-child\n\tcur = \"gno.land/r/demo/tests/foo/bar/baz\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif !IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// direct parent\n\tcur = \"gno.land/r/demo\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif !IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// fake parent (prefix)\n\tcur = \"gno.land/r/dem\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n\n\t// different namespace\n\tcur = \"gno.land/r/foo\"\n\tstd.TestSetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n}\n"},{"name":"realm_compositelit.gno","body":"package tests\n\ntype (\n\tWord uint\n\tnat []Word\n)\n\nvar zero = \u0026Int{\n\tneg: true,\n\tabs: []Word{0},\n}\n\n// structLit\ntype Int struct {\n\tneg bool\n\tabs nat\n}\n\nfunc GetZeroType() nat {\n\ta := zero.abs\n\treturn a\n}\n"},{"name":"realm_method38d.gno","body":"package tests\n\nvar abs nat\n\nfunc (n nat) Add() nat {\n\treturn []Word{0}\n}\n\nfunc GetAbs() nat {\n\tabs = []Word{0}\n\n\treturn abs\n}\n\nfunc AbsAdd() nat {\n\trt := GetAbs().Add()\n\n\treturn rt\n}\n"},{"name":"tests.gno","body":"package tests\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/nestedpkg\"\n\trsubtests \"gno.land/r/demo/tests/subtests\"\n)\n\nvar counter int\n\nfunc IncCounter() {\n\tcounter++\n}\n\nfunc Counter() int {\n\treturn counter\n}\n\nfunc CurrentRealmPath() string {\n\treturn std.CurrentRealm().PkgPath()\n}\n\nvar initOrigCaller = std.GetOrigCaller()\n\nfunc InitOrigCaller() std.Address {\n\treturn initOrigCaller\n}\n\nfunc CallAssertOriginCall() {\n\tstd.AssertOriginCall()\n}\n\nfunc CallIsOriginCall() bool {\n\treturn std.IsOriginCall()\n}\n\nfunc CallSubtestsAssertOriginCall() {\n\trsubtests.CallAssertOriginCall()\n}\n\nfunc CallSubtestsIsOriginCall() bool {\n\treturn rsubtests.CallIsOriginCall()\n}\n\n//----------------------------------------\n// Test structure to ensure cross-realm modification is prevented.\n\ntype TestRealmObject struct {\n\tField string\n}\n\nfunc ModifyTestRealmObject(t *TestRealmObject) {\n\tt.Field += \"_modified\"\n}\n\nfunc (t *TestRealmObject) Modify() {\n\tt.Field += \"_modified\"\n}\n\n//----------------------------------------\n// Test helpers to test a particular realm bug.\n\ntype TestNode struct {\n\tName string\n\tChild *TestNode\n}\n\nvar (\n\tgTestNode1 *TestNode\n\tgTestNode2 *TestNode\n\tgTestNode3 *TestNode\n)\n\nfunc InitTestNodes() {\n\tgTestNode1 = \u0026TestNode{Name: \"first\"}\n\tgTestNode2 = \u0026TestNode{Name: \"second\", Child: \u0026TestNode{Name: \"second's child\"}}\n}\n\nfunc ModTestNodes() {\n\ttmp := \u0026TestNode{}\n\ttmp.Child = gTestNode2.Child\n\tgTestNode3 = tmp // set to new-real\n\t// gTestNode1 = tmp.Child // set back to original is-real\n\tgTestNode3 = nil // delete.\n}\n\nfunc PrintTestNodes() {\n\tprintln(gTestNode2.Child.Name)\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc GetRSubtestsPrevRealm() std.Realm {\n\treturn rsubtests.GetPrevRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc IsCallerSubPath() bool {\n\treturn nestedpkg.IsCallerSubPath()\n}\n\nfunc IsCallerParentPath() bool {\n\treturn nestedpkg.IsCallerParentPath()\n}\n\nfunc HasCallerSameNamespace() bool {\n\treturn nestedpkg.IsSameNamespace()\n}\n"},{"name":"tests_test.gno","body":"package tests\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\nfunc TestAssertOriginCall(t *testing.T) {\n\t// CallAssertOriginCall(): no panic\n\tCallAssertOriginCall()\n\tif !CallIsOriginCall() {\n\t\tt.Errorf(\"expected IsOriginCall=true but got false\")\n\t}\n\n\t// CallAssertOriginCall() from a block: panic\n\texpectedReason := \"invalid non-origin call\"\n\tfunc() {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tif r == nil || r.(string) != expectedReason {\n\t\t\t\tt.Errorf(\"expected panic with '%v', got '%v'\", expectedReason, r)\n\t\t\t}\n\t\t}()\n\t\t// if called inside a function literal, this is no longer an origin call\n\t\t// because there's one additional frame (the function literal block).\n\t\tif CallIsOriginCall() {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\tCallAssertOriginCall()\n\t}()\n\n\t// CallSubtestsAssertOriginCall(): panic\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil || r.(string) != expectedReason {\n\t\t\tt.Errorf(\"expected panic with '%v', got '%v'\", expectedReason, r)\n\t\t}\n\t}()\n\tif CallSubtestsIsOriginCall() {\n\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t}\n\tCallSubtestsAssertOriginCall()\n}\n\nfunc TestPrevRealm(t *testing.T) {\n\tvar (\n\t\tuser1Addr = std.DerivePkgAddr(\"user1.gno\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\t// When a single realm in the frames, PrevRealm returns the user\n\tif addr := GetPrevRealm().Addr(); addr != user1Addr {\n\t\tt.Errorf(\"want GetPrevRealm().Addr==%s, got %s\", user1Addr, addr)\n\t}\n\t// When 2 or more realms in the frames, PrevRealm returns the second to last\n\tif addr := GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr {\n\t\tt.Errorf(\"want GetRSubtestsPrevRealm().Addr==%s, got %s\", rTestsAddr, addr)\n\t}\n}\n"},{"name":"z0_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall())\n\ttests.CallAssertOriginCall()\n\tprintln(\"tests.CallAssertOriginCall doesn't panic when called directly\")\n\n\t{\n\t\t// if called inside a block, this is no longer an origin call because\n\t\t// there's one additional frame (the block).\n\t\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall())\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tprintln(\"tests.AssertOriginCall panics if when called inside a function literal:\", r)\n\t\t}()\n\t\ttests.CallAssertOriginCall()\n\t}\n}\n\n// Output:\n// tests.CallIsOriginCall: true\n// tests.CallAssertOriginCall doesn't panic when called directly\n// tests.CallIsOriginCall: true\n// tests.AssertOriginCall panics if when called inside a function literal: undefined\n"},{"name":"z1_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(tests.Counter())\n\ttests.IncCounter()\n\tprintln(tests.Counter())\n}\n\n// Output:\n// 0\n// 1\n"},{"name":"z2_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\n)\n\n// When a single realm in the frames, PrevRealm returns the user\n// When 2 or more realms in the frames, PrevRealm returns the second to last\nfunc main() {\n\tvar (\n\t\teoa = testutils.TestAddress(\"someone\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\tstd.TestSetOrigCaller(eoa)\n\tprintln(\"tests.GetPrevRealm().Addr(): \", tests.GetPrevRealm().Addr())\n\tprintln(\"tests.GetRSubtestsPrevRealm().Addr(): \", tests.GetRSubtestsPrevRealm().Addr())\n}\n\n// Output:\n// tests.GetPrevRealm().Addr(): g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk\n// tests.GetRSubtestsPrevRealm().Addr(): g1gz4ycmx0s6ln2wdrsh4e00l9fsel2wskqa3snq\n"},{"name":"z3_filetest.gno","body":"// PKGPATH: gno.land/r/demo/test_test\npackage test_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tvar (\n\t\teoa = testutils.TestAddress(\"someone\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\tstd.TestSetOrigCaller(eoa)\n\t// Contrarily to z2_filetest.gno we EXPECT GetPrevRealms != eoa (#1704)\n\tif addr := tests.GetPrevRealm().Addr(); addr != eoa {\n\t\tprintln(\"want tests.GetPrevRealm().Addr ==\", eoa, \"got\", addr)\n\t}\n\t// When 2 or more realms in the frames, it is also different\n\tif addr := tests.GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr {\n\t\tprintln(\"want GetRSubtestsPrevRealm().Addr ==\", rTestsAddr, \"got\", addr)\n\t}\n}\n\n// Output:\n// want tests.GetPrevRealm().Addr == g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk got g1xufrdvnfk6zc9r0nqa23ld3tt2r5gkyvw76q63\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tests","path":"gno.land/p/demo/tests","files":[{"name":"README.md","body":"Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n"},{"name":"tests.gno","body":"package tests\n\nimport (\n\t\"std\"\n\n\tpsubtests \"gno.land/p/demo/tests/subtests\"\n\t\"gno.land/r/demo/tests\"\n\trtests \"gno.land/r/demo/tests\"\n)\n\nconst World = \"world\"\n\n// IncCounter demonstrates that it's possible to call a realm function from\n// a package. So a package can potentially write into the store, by calling\n// an other realm.\nfunc IncCounter() {\n\ttests.IncCounter()\n}\n\nfunc CurrentRealmPath() string {\n\treturn std.CurrentRealm().PkgPath()\n}\n\n//----------------------------------------\n// cross realm test vars\n\ntype TestRealmObject2 struct {\n\tField string\n}\n\nfunc (o2 *TestRealmObject2) Modify() {\n\to2.Field = \"modified\"\n}\n\nvar (\n\tsomevalue1 TestRealmObject2\n\tSomeValue2 TestRealmObject2\n\tSomeValue3 *TestRealmObject2\n)\n\nfunc init() {\n\tsomevalue1 = TestRealmObject2{Field: \"init\"}\n\tSomeValue2 = TestRealmObject2{Field: \"init\"}\n\tSomeValue3 = \u0026TestRealmObject2{Field: \"init\"}\n}\n\nfunc ModifyTestRealmObject2a() {\n\tsomevalue1.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2b() {\n\tSomeValue2.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2c() {\n\tSomeValue3.Field = \"modified\"\n}\n\nfunc GetPrevRealm() std.Realm {\n\treturn std.PrevRealm()\n}\n\nfunc GetPSubtestsPrevRealm() std.Realm {\n\treturn psubtests.GetPrevRealm()\n}\n\nfunc GetRTestsGetPrevRealm() std.Realm {\n\treturn rtests.GetPrevRealm()\n}\n\n// Warning: unsafe pattern.\nfunc Exec(fn func()) {\n\tfn()\n}\n"},{"name":"tests_test.gno","body":"package tests_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/tests\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar World = \"WORLD\"\n\nfunc TestGetHelloWorld(t *testing.T) {\n\t// tests.World is 'world'\n\ts := \"hello \" + tests.World + World\n\tconst want = \"hello worldWORLD\"\n\n\tuassert.Equal(t, want, s)\n}\n"},{"name":"z0_filetest.gno","body":"package main\n\nimport (\n\tptests \"gno.land/p/demo/tests\"\n\trtests \"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(rtests.Counter())\n\tptests.IncCounter()\n\tprintln(rtests.Counter())\n}\n\n// Output:\n// 0\n// 1\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"p_crossrealm","path":"gno.land/p/demo/tests/p_crossrealm","files":[{"name":"p_crossrealm.gno","body":"package p_crossrealm\n\ntype Stringer interface {\n\tString() string\n}\n\ntype Container struct {\n\tA int\n\tB Stringer\n}\n\nfunc (c *Container) Touch() *Container {\n\tc.A += 1\n\treturn c\n}\n\nfunc (c *Container) Print() {\n\tprintln(\"A:\", c.A)\n\tif c.B == nil {\n\t\tprintln(\"B: undefined\")\n\t} else {\n\t\tprintln(\"B:\", c.B.String())\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"todolist","path":"gno.land/p/demo/todolist","files":[{"name":"todolist.gno","body":"package todolist\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype TodoList struct {\n\tTitle string\n\tTasks *avl.Tree\n\tOwner std.Address\n}\n\ntype Task struct {\n\tTitle string\n\tDone bool\n}\n\nfunc NewTodoList(title string) *TodoList {\n\treturn \u0026TodoList{\n\t\tTitle: title,\n\t\tTasks: avl.NewTree(),\n\t\tOwner: std.GetOrigCaller(),\n\t}\n}\n\nfunc NewTask(title string) *Task {\n\treturn \u0026Task{\n\t\tTitle: title,\n\t\tDone: false,\n\t}\n}\n\nfunc (tl *TodoList) AddTask(id int, task *Task) {\n\ttl.Tasks.Set(strconv.Itoa(id), task)\n}\n\nfunc ToggleTaskStatus(task *Task) {\n\ttask.Done = !task.Done\n}\n\nfunc (tl *TodoList) RemoveTask(taskId string) {\n\ttl.Tasks.Remove(taskId)\n}\n\nfunc (tl *TodoList) GetTasks() []*Task {\n\ttasks := make([]*Task, 0, tl.Tasks.Size())\n\ttl.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttasks = append(tasks, value.(*Task))\n\t\treturn false\n\t})\n\treturn tasks\n}\n\nfunc (tl *TodoList) GetTodolistOwner() std.Address {\n\treturn tl.Owner\n}\n\nfunc (tl *TodoList) GetTodolistTitle() string {\n\treturn tl.Title\n}\n"},{"name":"todolist_test.gno","body":"package todolist\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttodoList := NewTodoList(title)\n\n\tuassert.Equal(t, title, todoList.GetTodolistTitle())\n\tuassert.Equal(t, 0, len(todoList.GetTasks()))\n\tuassert.Equal(t, std.GetOrigCaller().String(), todoList.GetTodolistOwner().String())\n}\n\nfunc TestNewTask(t *testing.T) {\n\ttitle := \"My Task\"\n\ttask := NewTask(title)\n\n\tuassert.Equal(t, title, task.Title)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is done\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\n\ttodoList.AddTask(1, task)\n\n\ttasks := todoList.GetTasks()\n\n\tuassert.Equal(t, 1, len(tasks))\n\tuassert.True(t, tasks[0] == task, \"Task does not match\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\ttask := NewTask(\"My Task\")\n\n\tToggleTaskStatus(task)\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not done\")\n\n\tToggleTaskStatus(task)\n\tuassert.False(t, task.Done, \"Expected task to be done, but it is not done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\ttodoList.AddTask(1, task)\n\n\ttodoList.RemoveTask(\"1\")\n\n\ttasks := todoList.GetTasks()\n\tuassert.Equal(t, 0, len(tasks))\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ui","path":"gno.land/p/demo/ui","files":[{"name":"ui.gno","body":"package ui\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype DOM struct {\n\t// metadata\n\tPrefix string\n\tTitle string\n\tWithComments bool\n\tClasses []string\n\n\t// elements\n\tHeader Element\n\tBody Element\n\tFooter Element\n}\n\nfunc (dom DOM) String() string {\n\tclasses := strings.Join(dom.Classes, \" \")\n\n\toutput := \"\"\n\n\tif classes != \"\" {\n\t\toutput += \"\u003cmain class='\" + classes + \"'\u003e\" + \"\\n\\n\"\n\t}\n\n\tif dom.Title != \"\" {\n\t\toutput += H1(dom.Title).String(dom) + \"\\n\"\n\t}\n\n\tif header := dom.Header.String(dom); header != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- header --\u003e\"\n\t\t}\n\t\toutput += header + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /header --\u003e\"\n\t\t}\n\t}\n\n\tif body := dom.Body.String(dom); body != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- body --\u003e\"\n\t\t}\n\t\toutput += body + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /body --\u003e\"\n\t\t}\n\t}\n\n\tif footer := dom.Footer.String(dom); footer != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- footer --\u003e\"\n\t\t}\n\t\toutput += footer + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /footer --\u003e\"\n\t\t}\n\t}\n\n\tif classes != \"\" {\n\t\toutput += \"\u003c/main\u003e\"\n\t}\n\n\t// TODO: cleanup double new-lines.\n\n\treturn output\n}\n\ntype Jumbotron []DomStringer\n\nfunc (j Jumbotron) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tfor _, elem := range j {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\toutput += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\treturn output\n}\n\n// XXX: rename Element to Div?\ntype Element []DomStringer\n\nfunc (e *Element) Append(elems ...DomStringer) {\n\t*e = append(*e, elems...)\n}\n\nfunc (e *Element) String(dom DOM) string {\n\toutput := \"\"\n\tfor _, elem := range *e {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\treturn output\n}\n\ntype Breadcrumb []DomStringer\n\nfunc (b *Breadcrumb) Append(elems ...DomStringer) {\n\t*b = append(*b, elems...)\n}\n\nfunc (b Breadcrumb) String(dom DOM) string {\n\toutput := \"\"\n\tfor idx, entry := range b {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" / \"\n\t\t}\n\t\toutput += entry.String(dom)\n\t}\n\treturn output\n}\n\ntype Columns struct {\n\tMaxWidth int\n\tColumns []Element\n}\n\nfunc (c *Columns) Append(elems ...Element) {\n\tc.Columns = append(c.Columns, elems...)\n}\n\nfunc (c Columns) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"columns-` + strconv.Itoa(c.MaxWidth) + `\"\u003e` + \"\\n\"\n\tfor _, entry := range c.Columns {\n\t\toutput += `\u003cdiv class=\"column\"\u003e` + \"\\n\\n\"\n\t\toutput += entry.String(dom)\n\t\toutput += \"\u003c/div\u003e\u003c!-- /column--\u003e\\n\"\n\t}\n\toutput += \"\u003c/div\u003e\u003c!-- /columns-\" + strconv.Itoa(c.MaxWidth) + \" --\u003e\\n\"\n\treturn output\n}\n\ntype Link struct {\n\tText string\n\tPath string\n\tURL string\n}\n\n// TODO: image\n\n// TODO: pager\n\nfunc (l Link) String(dom DOM) string {\n\turl := \"\"\n\tswitch {\n\tcase l.Path != \"\" \u0026\u0026 l.URL != \"\":\n\t\tpanic(\"a link should have a path or a URL, not both.\")\n\tcase l.Path != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.Path\n\t\t}\n\t\turl = dom.Prefix + l.Path\n\tcase l.URL != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.URL\n\t\t}\n\t\turl = l.URL\n\t}\n\n\treturn \"[\" + l.Text + \"](\" + url + \")\"\n}\n\ntype BulletList []DomStringer\n\nfunc (bl BulletList) String(dom DOM) string {\n\toutput := \"\"\n\n\tfor _, entry := range bl {\n\t\toutput += \"- \" + entry.String(dom) + \"\\n\"\n\t}\n\n\treturn output\n}\n\nfunc Text(s string) DomStringer {\n\treturn Raw{Content: s}\n}\n\ntype DomStringer interface {\n\tString(dom DOM) string\n}\n\ntype Raw struct {\n\tContent string\n}\n\nfunc (r Raw) String(_ DOM) string {\n\treturn r.Content\n}\n\ntype (\n\tH1 string\n\tH2 string\n\tH3 string\n\tH4 string\n\tH5 string\n\tH6 string\n\tBold string\n\tItalic string\n\tCode string\n\tParagraph string\n\tQuote string\n\tHR struct{}\n)\n\nfunc (text H1) String(_ DOM) string { return \"# \" + string(text) + \"\\n\" }\nfunc (text H2) String(_ DOM) string { return \"## \" + string(text) + \"\\n\" }\nfunc (text H3) String(_ DOM) string { return \"### \" + string(text) + \"\\n\" }\nfunc (text H4) String(_ DOM) string { return \"#### \" + string(text) + \"\\n\" }\nfunc (text H5) String(_ DOM) string { return \"##### \" + string(text) + \"\\n\" }\nfunc (text H6) String(_ DOM) string { return \"###### \" + string(text) + \"\\n\" }\nfunc (text Quote) String(_ DOM) string { return \"\u003e \" + string(text) + \"\\n\" }\nfunc (text Bold) String(_ DOM) string { return \"**\" + string(text) + \"**\" }\nfunc (text Italic) String(_ DOM) string { return \"_\" + string(text) + \"_\" }\nfunc (text Paragraph) String(_ DOM) string { return \"\\n\" + string(text) + \"\\n\" }\nfunc (_ HR) String(_ DOM) string { return \"\\n---\\n\" }\n\nfunc (text Code) String(_ DOM) string {\n\t// multiline\n\tif strings.Contains(string(text), \"\\n\") {\n\t\treturn \"\\n```\\n\" + string(text) + \"\\n```\\n\"\n\t}\n\n\t// single line\n\treturn \"`\" + string(text) + \"`\"\n}\n"},{"name":"ui_test.gno","body":"package ui\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"watchdog","path":"gno.land/p/demo/watchdog","files":[{"name":"watchdog.gno","body":"package watchdog\n\nimport \"time\"\n\ntype Watchdog struct {\n\tDuration time.Duration\n\tlastUpdate time.Time\n\tlastDown time.Time\n}\n\nfunc (w *Watchdog) Alive() {\n\tnow := time.Now()\n\tif !w.IsAlive() {\n\t\tw.lastDown = now\n\t}\n\tw.lastUpdate = now\n}\n\nfunc (w Watchdog) Status() string {\n\tif w.IsAlive() {\n\t\treturn \"OK\"\n\t}\n\treturn \"KO\"\n}\n\nfunc (w Watchdog) IsAlive() bool {\n\treturn time.Since(w.lastUpdate) \u003c w.Duration\n}\n\nfunc (w Watchdog) UpSince() time.Time {\n\treturn w.lastDown\n}\n\nfunc (w Watchdog) DownSince() time.Time {\n\tif !w.IsAlive() {\n\t\treturn w.lastUpdate\n\t}\n\treturn time.Time{}\n}\n"},{"name":"watchdog_test.gno","body":"package watchdog\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tw := Watchdog{Duration: 5 * time.Minute}\n\tuassert.False(t, w.IsAlive())\n\tw.Alive()\n\tuassert.True(t, w.IsAlive())\n\t// XXX: add more tests when we'll be able to \"skip time\".\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"executor","path":"gno.land/p/gov/executor","files":[{"name":"callback.gno","body":"package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar errInvalidCaller = errors.New(\"invalid executor caller\")\n\n// NewCallbackExecutor creates a new callback executor with the provided callback function\nfunc NewCallbackExecutor(callback func() error, path string) *CallbackExecutor {\n\treturn \u0026CallbackExecutor{\n\t\tcallback: callback,\n\t\tdaoPkgPath: path,\n\t}\n}\n\n// CallbackExecutor is an implementation of the dao.Executor interface,\n// based on a specific callback.\n// The given callback should verify the validity of the govdao call\ntype CallbackExecutor struct {\n\tcallback func() error // the callback to be executed\n\tdaoPkgPath string // the active pkg path of the govdao\n}\n\n// Execute runs the executor's callback function.\nfunc (exec *CallbackExecutor) Execute() error {\n\t// Verify the caller is an adequate Realm\n\tcaller := std.CurrentRealm().PkgPath()\n\tif caller != exec.daoPkgPath {\n\t\treturn errInvalidCaller\n\t}\n\n\tif exec.callback != nil {\n\t\treturn exec.callback()\n\t}\n\n\treturn nil\n}\n"},{"name":"context.gno","body":"package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/context\"\n)\n\ntype propContextKey string\n\nfunc (k propContextKey) String() string { return string(k) }\n\nconst (\n\tstatusContextKey = propContextKey(\"govdao-prop-status\")\n\tapprovedStatus = \"approved\"\n)\n\nvar errNotApproved = errors.New(\"not approved by govdao\")\n\n// CtxExecutor is an implementation of the dao.Executor interface,\n// based on the given context.\n// It utilizes the given context to assert the validity of the govdao call\ntype CtxExecutor struct {\n\tcallbackCtx func(ctx context.Context) error // the callback ctx fn, if any\n\tdaoPkgPath string // the active pkg path of the govdao\n}\n\n// NewCtxExecutor creates a new executor with the provided callback function.\nfunc NewCtxExecutor(callback func(ctx context.Context) error, path string) *CtxExecutor {\n\treturn \u0026CtxExecutor{\n\t\tcallbackCtx: callback,\n\t\tdaoPkgPath: path,\n\t}\n}\n\n// Execute runs the executor's callback function\nfunc (exec *CtxExecutor) Execute() error {\n\t// Verify the caller is an adequate Realm\n\tcaller := std.CurrentRealm().PkgPath()\n\tif caller != exec.daoPkgPath {\n\t\treturn errInvalidCaller\n\t}\n\n\t// Create the context\n\tctx := context.WithValue(\n\t\tcontext.Empty(),\n\t\tstatusContextKey,\n\t\tapprovedStatus,\n\t)\n\n\treturn exec.callbackCtx(ctx)\n}\n\n// IsApprovedByGovdaoContext asserts that the govdao approved the context\nfunc IsApprovedByGovdaoContext(ctx context.Context) bool {\n\tv := ctx.Value(statusContextKey)\n\tif v == nil {\n\t\treturn false\n\t}\n\n\tvs, ok := v.(string)\n\n\treturn ok \u0026\u0026 vs == approvedStatus\n}\n\n// AssertContextApprovedByGovDAO asserts the given context\n// was approved by GOVDAO\nfunc AssertContextApprovedByGovDAO(ctx context.Context) {\n\tif IsApprovedByGovdaoContext(ctx) {\n\t\treturn\n\t}\n\n\tpanic(errNotApproved)\n}\n"},{"name":"proposal_test.gno","body":"package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/context\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestExecutor_Callback(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"govdao not caller\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\te := NewCallbackExecutor(cb, \"gno.land/r/gov/dao\")\n\n\t\t// Execute as not the /r/gov/dao caller\n\t\tuassert.ErrorIs(t, e.Execute(), errInvalidCaller)\n\t\tuassert.False(t, called, \"expected proposal to not execute\")\n\t})\n\n\tt.Run(\"execution successful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCallbackExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\tuassert.NoError(t, e.Execute())\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n\n\tt.Run(\"execution unsuccessful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\texpectedErr = errors.New(\"unexpected\")\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn expectedErr\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCallbackExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\tuassert.ErrorIs(t, e.Execute(), expectedErr)\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n}\n\nfunc TestExecutor_Context(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"govdao not caller\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\te := NewCtxExecutor(cb, \"gno.land/r/gov/dao\")\n\n\t\t// Execute as not the /r/gov/dao caller\n\t\tuassert.ErrorIs(t, e.Execute(), errInvalidCaller)\n\t\tuassert.False(t, called, \"expected proposal to not execute\")\n\t})\n\n\tt.Run(\"execution successful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCtxExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\turequire.NoError(t, e.Execute())\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n\n\tt.Run(\"execution unsuccessful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\texpectedErr = errors.New(\"unexpected\")\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn expectedErr\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCtxExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\tstd.TestSetRealm(r)\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\terr := e.Execute()\n\n\t\t\tuassert.ErrorIs(t, err, expectedErr)\n\t\t})\n\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"helplink","path":"gno.land/p/moul/helplink","files":[{"name":"helplink.gno","body":"// Package helplink provides utilities for creating help page links compatible\n// with Gnoweb, Gnobro, and other clients that support the Gno contracts'\n// flavored Markdown format.\n//\n// This package simplifies the generation of dynamic, context-sensitive help\n// links, enabling users to navigate relevant documentation seamlessly within\n// the Gno ecosystem.\n//\n// For a more lightweight alternative, consider using p/moul/txlink.\n//\n// The primary functions — Func, FuncURL, and Home — are intended for use with\n// the \"relative realm\". When specifying a custom Realm, you can create links\n// that utilize either the current realm path or a fully qualified path to\n// another realm.\npackage helplink\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/moul/txlink\"\n)\n\nconst chainDomain = \"gno.land\" // XXX: std.ChainDomain (#2911)\n\n// Func returns a markdown link for the specific function with optional\n// key-value arguments, for the current realm.\nfunc Func(title string, fn string, args ...string) string {\n\treturn Realm(\"\").Func(title, fn, args...)\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc FuncURL(fn string, args ...string) string {\n\treturn Realm(\"\").FuncURL(fn, args...)\n}\n\n// Home returns the URL for the help homepage of the current realm.\nfunc Home() string {\n\treturn Realm(\"\").Home()\n}\n\n// Realm represents a specific realm for generating help links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// local realm -\u003e /realm\n\trealm := string(r)\n\tif strings.Contains(realm, chainDomain) {\n\t\treturn strings.TrimPrefix(realm, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + string(r)\n}\n\n// Func returns a markdown link for the specified function with optional\n// key-value arguments.\nfunc (r Realm) Func(title string, fn string, args ...string) string {\n\t// XXX: escape title\n\treturn \"[\" + title + \"](\" + r.FuncURL(fn, args...) + \")\"\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments.\nfunc (r Realm) FuncURL(fn string, args ...string) string {\n\ttlr := txlink.Realm(r)\n\treturn tlr.URL(fn, args...)\n}\n\n// Home returns the base help URL for the specified realm.\nfunc (r Realm) Home() string {\n\treturn r.prefix() + \"$help\"\n}\n"},{"name":"helplink_test.gno","body":"package helplink\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestFunc(t *testing.T) {\n\ttests := []struct {\n\t\ttitle string\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Example]($help\u0026func=foo\u0026bar=1\u0026baz=2)\", \"\"},\n\t\t{\"Realm Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Realm Example](/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2)\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"Single Arg\", \"testFunc\", []string{\"key\", \"value\"}, \"[Single Arg]($help\u0026func=testFunc\u0026key=value)\", \"\"},\n\t\t{\"No Args\", \"noArgsFunc\", []string{}, \"[No Args]($help\u0026func=noArgsFunc)\", \"\"},\n\t\t{\"Odd Args\", \"oddArgsFunc\", []string{\"key\"}, \"[Odd Args]($help\u0026func=oddArgsFunc)\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.title, func(t *testing.T) {\n\t\t\tgot := tt.realm.Func(tt.title, tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestFuncURL(t *testing.T) {\n\ttests := []struct {\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"$help\u0026func=oddArgsFunc\", \"\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.land/r/lorem/ipsum\"},\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=testFunc\u0026key=value\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"noArgsFunc\", []string{}, \"https://gno.world/r/lorem/ipsum$help\u0026func=noArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"https://gno.world/r/lorem/ipsum$help\u0026func=oddArgsFunc\", \"gno.world/r/lorem/ipsum\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tgot := tt.realm.FuncURL(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestHome(t *testing.T) {\n\ttests := []struct {\n\t\trealm Realm\n\t\twant string\n\t}{\n\t\t{\"\", \"$help\"},\n\t\t{\"gno.land/r/lorem/ipsum\", \"/r/lorem/ipsum$help\"},\n\t\t{\"gno.world/r/lorem/ipsum\", \"https://gno.world/r/lorem/ipsum$help\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.realm), func(t *testing.T) {\n\t\t\tgot := tt.realm.Home()\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"printfdebugging","path":"gno.land/p/demo/printfdebugging","files":[{"name":"color.gno","body":"package printfdebugging\n\n// consts copied from https://github.com/fatih/color/blob/main/color.go\n\n// Attribute defines a single SGR Code\ntype Attribute int\n\nconst Escape = \"\\x1b\"\n\n// Base attributes\nconst (\n\tReset Attribute = iota\n\tBold\n\tFaint\n\tItalic\n\tUnderline\n\tBlinkSlow\n\tBlinkRapid\n\tReverseVideo\n\tConcealed\n\tCrossedOut\n)\n\nconst (\n\tResetBold Attribute = iota + 22\n\tResetItalic\n\tResetUnderline\n\tResetBlinking\n\t_\n\tResetReversed\n\tResetConcealed\n\tResetCrossedOut\n)\n\n// Foreground text colors\nconst (\n\tFgBlack Attribute = iota + 30\n\tFgRed\n\tFgGreen\n\tFgYellow\n\tFgBlue\n\tFgMagenta\n\tFgCyan\n\tFgWhite\n)\n\n// Foreground Hi-Intensity text colors\nconst (\n\tFgHiBlack Attribute = iota + 90\n\tFgHiRed\n\tFgHiGreen\n\tFgHiYellow\n\tFgHiBlue\n\tFgHiMagenta\n\tFgHiCyan\n\tFgHiWhite\n)\n\n// Background text colors\nconst (\n\tBgBlack Attribute = iota + 40\n\tBgRed\n\tBgGreen\n\tBgYellow\n\tBgBlue\n\tBgMagenta\n\tBgCyan\n\tBgWhite\n)\n\n// Background Hi-Intensity text colors\nconst (\n\tBgHiBlack Attribute = iota + 100\n\tBgHiRed\n\tBgHiGreen\n\tBgHiYellow\n\tBgHiBlue\n\tBgHiMagenta\n\tBgHiCyan\n\tBgHiWhite\n)\n"},{"name":"printfdebugging.gno","body":"// this package is a joke... or not.\npackage printfdebugging\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc BigRedLine(args ...string) {\n\tprintln(ufmt.Sprintf(\"%s[%dm####################################%s[%dm %s\",\n\t\tEscape, int(BgRed), Escape, int(Reset),\n\t\tstrings.Join(args, \" \"),\n\t))\n}\n\nfunc Success() {\n\tprintln(\" \\033[31mS\\033[33mU\\033[32mC\\033[36mC\\033[34mE\\033[35mS\\033[31mS\\033[0m \")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"validators","path":"gno.land/p/sys/validators","files":[{"name":"types.gno","body":"package validators\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\n// ValsetProtocol defines the validator set protocol (PoA / PoS / PoC / ?)\ntype ValsetProtocol interface {\n\t// AddValidator adds a new validator to the validator set.\n\t// If the validator is already present, the method should error out\n\t//\n\t// TODO: This API is not ideal -- the address should be derived from\n\t// the public key, and not be passed in as such, but currently Gno\n\t// does not support crypto address derivation\n\tAddValidator(address std.Address, pubKey string, power uint64) (Validator, error)\n\n\t// RemoveValidator removes the given validator from the set.\n\t// If the validator is not present in the set, the method should error out\n\tRemoveValidator(address std.Address) (Validator, error)\n\n\t// IsValidator returns a flag indicating if the given\n\t// bech32 address is part of the validator set\n\tIsValidator(address std.Address) bool\n\n\t// GetValidator returns the validator using the given address\n\tGetValidator(address std.Address) (Validator, error)\n\n\t// GetValidators returns the currently active validator set\n\tGetValidators() []Validator\n}\n\n// Validator represents a single chain validator\ntype Validator struct {\n\tAddress std.Address // bech32 address\n\tPubKey string // bech32 representation of the public key\n\tVotingPower uint64\n}\n\nconst (\n\tValidatorAddedEvent = \"ValidatorAdded\" // emitted when a validator was added to the set\n\tValidatorRemovedEvent = \"ValidatorRemoved\" // emitted when a validator was removed from the set\n)\n\nvar (\n\t// ErrValidatorExists is returned when the validator is already in the set\n\tErrValidatorExists = errors.New(\"validator already exists\")\n\n\t// ErrValidatorMissing is returned when the validator is not in the set\n\tErrValidatorMissing = errors.New(\"validator doesn't exist\")\n)\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"poa","path":"gno.land/p/nt/poa","files":[{"name":"option.gno","body":"package poa\n\nimport \"gno.land/p/sys/validators\"\n\ntype Option func(*PoA)\n\n// WithInitialSet sets the initial PoA validator set\nfunc WithInitialSet(validators []validators.Validator) Option {\n\treturn func(p *PoA) {\n\t\tfor _, validator := range validators {\n\t\t\tp.validators.Set(validator.Address.String(), validator)\n\t\t}\n\t}\n}\n"},{"name":"poa.gno","body":"package poa\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar ErrInvalidVotingPower = errors.New(\"invalid voting power\")\n\n// PoA specifies the Proof of Authority validator set, with simple add / remove constraints.\n//\n// To add:\n// - proposed validator must not be part of the set already\n// - proposed validator voting power must be \u003e 0\n//\n// To remove:\n// - proposed validator must be part of the set already\ntype PoA struct {\n\tvalidators *avl.Tree // std.Address -\u003e validators.Validator\n}\n\n// NewPoA creates a new empty Proof of Authority validator set\nfunc NewPoA(opts ...Option) *PoA {\n\t// Create the empty set\n\tp := \u0026PoA{\n\t\tvalidators: avl.NewTree(),\n\t}\n\n\t// Apply the options\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\n\treturn p\n}\n\nfunc (p *PoA) AddValidator(address std.Address, pubKey string, power uint64) (validators.Validator, error) {\n\t// Validate that the operation is a valid call.\n\t// Check if the validator is already in the set\n\tif p.IsValidator(address) {\n\t\treturn validators.Validator{}, validators.ErrValidatorExists\n\t}\n\n\t// Make sure the voting power \u003e 0\n\tif power == 0 {\n\t\treturn validators.Validator{}, ErrInvalidVotingPower\n\t}\n\n\tv := validators.Validator{\n\t\tAddress: address,\n\t\tPubKey: pubKey, // TODO: in the future, verify the public key\n\t\tVotingPower: power,\n\t}\n\n\t// Add the validator to the set\n\tp.validators.Set(address.String(), v)\n\n\treturn v, nil\n}\n\nfunc (p *PoA) RemoveValidator(address std.Address) (validators.Validator, error) {\n\t// Validate that the operation is a valid call\n\t// Fetch the validator\n\tvalidator, err := p.GetValidator(address)\n\tif err != nil {\n\t\treturn validators.Validator{}, err\n\t}\n\n\t// Remove the validator from the set\n\tp.validators.Remove(address.String())\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) IsValidator(address std.Address) bool {\n\t_, exists := p.validators.Get(address.String())\n\n\treturn exists\n}\n\nfunc (p *PoA) GetValidator(address std.Address) (validators.Validator, error) {\n\tvalidatorRaw, exists := p.validators.Get(address.String())\n\tif !exists {\n\t\treturn validators.Validator{}, validators.ErrValidatorMissing\n\t}\n\n\tvalidator := validatorRaw.(validators.Validator)\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) GetValidators() []validators.Validator {\n\tvals := make([]validators.Validator, 0, p.validators.Size())\n\n\tp.validators.Iterate(\"\", \"\", func(_ string, value interface{}) bool {\n\t\tvalidator := value.(validators.Validator)\n\t\tvals = append(vals, validator)\n\n\t\treturn false\n\t})\n\n\treturn vals\n}\n"},{"name":"poa_test.gno","body":"package poa\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/sys/validators\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// generateTestValidators generates a dummy validator set\nfunc generateTestValidators(count int) []validators.Validator {\n\tvals := make([]validators.Validator, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tval := validators.Validator{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"%d\", i)),\n\t\t\tPubKey: \"public-key\",\n\t\t\tVotingPower: 1,\n\t\t}\n\n\t\tvals = append(vals, val)\n\t}\n\n\treturn vals\n}\n\nfunc TestPoA_AddValidator_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validator already in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tproposalKey = \"public-key\"\n\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = proposalAddress\n\t\tinitialSet[0].PubKey = proposalKey\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Attempt to add the validator\n\t\t_, err := p.AddValidator(proposalAddress, proposalKey, 1)\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorExists)\n\t})\n\n\tt.Run(\"invalid voting power\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tproposalKey = \"public-key\"\n\t\t)\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to add the validator\n\t\t_, err := p.AddValidator(proposalAddress, proposalKey, 0)\n\t\tuassert.ErrorIs(t, err, ErrInvalidVotingPower)\n\t})\n}\n\nfunc TestPoA_AddValidator(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\tproposalKey = \"public-key\"\n\t)\n\n\t// Create the protocol with no initial set\n\tp := NewPoA()\n\n\t// Attempt to add the validator\n\t_, err := p.AddValidator(proposalAddress, proposalKey, 1)\n\tuassert.NoError(t, err)\n\n\t// Make sure the validator is added\n\tif !p.IsValidator(proposalAddress) || p.validators.Size() != 1 {\n\t\tt.Fatal(\"address is not validator\")\n\t}\n}\n\nfunc TestPoA_RemoveValidator_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"proposed removal not in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = proposalAddress\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Attempt to remove the validator\n\t\t_, err := p.RemoveValidator(testutils.TestAddress(\"totally random\"))\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorMissing)\n\t})\n}\n\nfunc TestPoA_RemoveValidator(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tproposalAddress = testutils.TestAddress(\"caller\")\n\t\tinitialSet = generateTestValidators(1)\n\t)\n\n\tinitialSet[0].Address = proposalAddress\n\n\t// Create the protocol with an initial set\n\tp := NewPoA(WithInitialSet(initialSet))\n\n\t// Attempt to remove the validator\n\t_, err := p.RemoveValidator(proposalAddress)\n\turequire.NoError(t, err)\n\n\t// Make sure the validator is removed\n\tif p.IsValidator(proposalAddress) || p.validators.Size() != 0 {\n\t\tt.Fatal(\"address is validator\")\n\t}\n}\n\nfunc TestPoA_GetValidator(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validator not in set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to get the voting power\n\t\t_, err := p.GetValidator(testutils.TestAddress(\"caller\"))\n\t\tuassert.ErrorIs(t, err, validators.ErrValidatorMissing)\n\t})\n\n\tt.Run(\"validator fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\taddress = testutils.TestAddress(\"caller\")\n\t\t\tpubKey = \"public-key\"\n\t\t\tvotingPower = uint64(10)\n\n\t\t\tinitialSet = generateTestValidators(1)\n\t\t)\n\n\t\tinitialSet[0].Address = address\n\t\tinitialSet[0].PubKey = pubKey\n\t\tinitialSet[0].VotingPower = votingPower\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Get the validator\n\t\tval, err := p.GetValidator(address)\n\t\turequire.NoError(t, err)\n\n\t\t// Validate the address\n\t\tif val.Address != address {\n\t\t\tt.Fatal(\"invalid address\")\n\t\t}\n\n\t\t// Validate the voting power\n\t\tif val.VotingPower != votingPower {\n\t\t\tt.Fatal(\"invalid voting power\")\n\t\t}\n\n\t\t// Validate the public key\n\t\tif val.PubKey != pubKey {\n\t\t\tt.Fatal(\"invalid public key\")\n\t\t}\n\t})\n}\n\nfunc TestPoA_GetValidators(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty set\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the protocol with no initial set\n\t\tp := NewPoA()\n\n\t\t// Attempt to get the voting power\n\t\tvals := p.GetValidators()\n\n\t\tif len(vals) != 0 {\n\t\t\tt.Fatal(\"validator set is not empty\")\n\t\t}\n\t})\n\n\tt.Run(\"validator set fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tinitialSet := generateTestValidators(10)\n\n\t\t// Create the protocol with an initial set\n\t\tp := NewPoA(WithInitialSet(initialSet))\n\n\t\t// Get the validator set\n\t\tvals := p.GetValidators()\n\n\t\tif len(vals) != len(initialSet) {\n\t\t\tt.Fatal(\"returned validator set mismatch\")\n\t\t}\n\n\t\tfor _, val := range vals {\n\t\t\tfor _, initialVal := range initialSet {\n\t\t\t\tif val.Address != initialVal.Address {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Validate the voting power\n\t\t\t\tuassert.Equal(t, val.VotingPower, initialVal.VotingPower)\n\n\t\t\t\t// Validate the public key\n\t\t\t\tuassert.Equal(t, val.PubKey, initialVal.PubKey)\n\t\t\t}\n\t\t}\n\t})\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"gnoface","path":"gno.land/r/demo/art/gnoface","files":[{"name":"gnoface.gno","body":"package gnoface\n\nimport (\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Render(path string) string {\n\tseed := uint64(entropy.New().Value())\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\ts, err := strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tseed = uint64(s)\n\t}\n\n\toutput := ufmt.Sprintf(\"Gnoface #%d\\n\", seed)\n\toutput += \"```\\n\" + Draw(seed) + \"```\\n\"\n\treturn output\n}\n\nfunc Draw(seed uint64) string {\n\tvar (\n\t\thairs = []string{\n\t\t\t\" s\",\n\t\t\t\" .......\",\n\t\t\t\" s s s\",\n\t\t\t\" /\\\\ /\\\\\",\n\t\t\t\" |||||||\",\n\t\t}\n\t\theadtop = []string{\n\t\t\t\" /-------\\\\\",\n\t\t\t\" /~~~~~~~\\\\\",\n\t\t\t\" /|||||||\\\\\",\n\t\t\t\" ////////\\\\\",\n\t\t\t\" |||||||||\",\n\t\t\t\" /\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\n\t\t}\n\t\theadspace = []string{\n\t\t\t\" | |\",\n\t\t}\n\t\teyebrow = []string{\n\t\t\t\"~\",\n\t\t\t\"*\",\n\t\t\t\"_\",\n\t\t\t\".\",\n\t\t}\n\t\tear = []string{\n\t\t\t\"o\",\n\t\t\t\" \",\n\t\t\t\"D\",\n\t\t\t\"O\",\n\t\t\t\"\u003c\",\n\t\t\t\"\u003e\",\n\t\t\t\".\",\n\t\t\t\"|\",\n\t\t\t\")\",\n\t\t\t\"(\",\n\t\t}\n\t\teyesmiddle = []string{\n\t\t\t\"| o o |\",\n\t\t\t\"| o _ |\",\n\t\t\t\"| _ o |\",\n\t\t\t\"| . . |\",\n\t\t\t\"| O O |\",\n\t\t\t\"| v v |\",\n\t\t\t\"| X X |\",\n\t\t\t\"| x X |\",\n\t\t\t\"| X D |\",\n\t\t\t\"| ~ ~ |\",\n\t\t}\n\t\tnose = []string{\n\t\t\t\" | o |\",\n\t\t\t\" | O |\",\n\t\t\t\" | V |\",\n\t\t\t\" | L |\",\n\t\t\t\" | C |\",\n\t\t\t\" | ~ |\",\n\t\t\t\" | . . |\",\n\t\t\t\" | . |\",\n\t\t}\n\t\tmouth = []string{\n\t\t\t\" | __/ |\",\n\t\t\t\" | \\\\_/ |\",\n\t\t\t\" | . |\",\n\t\t\t\" | ___ |\",\n\t\t\t\" | ~~~ |\",\n\t\t\t\" | === |\",\n\t\t\t\" | \u003c=\u003e |\",\n\t\t}\n\t\theadbottom = []string{\n\t\t\t\" \\\\-------/\",\n\t\t\t\" \\\\~~~~~~~/\",\n\t\t\t\" \\\\_______/\",\n\t\t}\n\t)\n\n\tr := rand.New(rand.NewPCG(seed, 0xdeadbeef))\n\n\treturn pick(r, hairs) + \"\\n\" +\n\t\tpick(r, headtop) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\t\" | \" + pick(r, eyebrow) + \" \" + pick(r, eyebrow) + \" |\\n\" +\n\t\tpick(r, ear) + pick(r, eyesmiddle) + pick(r, ear) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, nose) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, mouth) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, headbottom) + \"\\n\"\n}\n\nfunc pick(r *rand.Rand, slice []string) string {\n\treturn slice[r.IntN(len(slice))]\n}\n\n// based on https://github.com/moul/pipotron/blob/master/dict/ascii-face.yml\n"},{"name":"gnoface_test.gno","body":"package gnoface\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestDraw(t *testing.T) {\n\tcases := []struct {\n\t\tseed uint64\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tseed: 42,\n\t\t\texpected: `\n |||||||\n |||||||||\n | |\n | . ~ |\n)| v v |O\n | |\n | L |\n | |\n | ___ |\n | |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 1337,\n\t\t\texpected: `\n .......\n |||||||||\n | |\n | . _ |\nD| x X |O\n | |\n | ~ |\n | |\n | ~~~ |\n | |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 123456789,\n\t\t\texpected: `\n .......\n ////////\\\n | |\n | ~ * |\n|| x X |o\n | |\n | V |\n | |\n | . |\n | |\n \\-------/\n`[1:],\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tname := ufmt.Sprintf(\"%d\", tc.seed)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Draw(tc.seed)\n\t\t\tuassert.Equal(t, string(tc.expected), got)\n\t\t})\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"42\",\n\t\t\texpected: \"Gnoface #42\\n```\" + `\n |||||||\n |||||||||\n | |\n | . ~ |\n)| v v |O\n | |\n | L |\n | |\n | ___ |\n | |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"1337\",\n\t\t\texpected: \"Gnoface #1337\\n```\" + `\n .......\n |||||||||\n | |\n | . _ |\nD| x X |O\n | |\n | ~ |\n | |\n | ~~~ |\n | |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"123456789\",\n\t\t\texpected: \"Gnoface #123456789\\n```\" + `\n .......\n ////////\\\n | |\n | ~ * |\n|| x X |o\n | |\n | V |\n | |\n | . |\n | |\n \\-------/\n` + \"```\\n\",\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"millipede","path":"gno.land/r/demo/art/millipede","files":[{"name":"millipede.gno","body":"package millipede\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tminSize = 1\n\tdefaultSize = 20\n\tmaxSize = 100\n)\n\nfunc Draw(size int) string {\n\tif size \u003c minSize || size \u003e maxSize {\n\t\tpanic(\"invalid millipede size\")\n\t}\n\tpaddings := []string{\" \", \" \", \"\", \" \", \" \", \" \", \" \", \" \", \" \"}\n\tvar b strings.Builder\n\tb.WriteString(\" ╚⊙ ⊙╝\\n\")\n\tfor i := 0; i \u003c size; i++ {\n\t\tb.WriteString(paddings[i%9] + \"╚═(███)═╝\\n\")\n\t}\n\treturn b.String()\n}\n\nfunc Render(path string) string {\n\tsize := defaultSize\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\tvar err error\n\t\tsize, err = strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\toutput := \"```\\n\" + Draw(size) + \"```\\n\"\n\tif size \u003e minSize {\n\t\toutput += ufmt.Sprintf(\"[%d](/r/demo/art/millipede:%d)\u003c \", size-1, size-1)\n\t}\n\tif size \u003c maxSize {\n\t\toutput += ufmt.Sprintf(\" \u003e[%d](/r/demo/art/millipede:%d)\", size+1, size+1)\n\t}\n\treturn output\n}\n\n// based on https://github.com/getmillipede/millipede-go/blob/977f046c39c35a650eac0fd30245e96b22c7803c/main.go\n"},{"name":"millipede_test.gno","body":"package millipede\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"\",\n\t\t\texpected: \"```\" + `\n ╚⊙ ⊙╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[19](/r/demo/art/millipede:19)\u003c \u003e[21](/r/demo/art/millipede:21)\",\n\t\t},\n\t\t{\n\t\t\tpath: \"4\",\n\t\t\texpected: \"```\" + `\n ╚⊙ ⊙╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[3](/r/demo/art/millipede:3)\u003c \u003e[5](/r/demo/art/millipede:5)\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"banktest","path":"gno.land/r/demo/banktest","files":[{"name":"README.md","body":"This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.go](/r/demo/banktest/banktest.go) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n \"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e Self explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n caller std.Address\n sent std.Coins\n returned std.Coins\n time time.Time\n}\n\nfunc (act *activity) String() string {\n return act.caller.String() + \" \" +\n act.sent.String() + \" sent, \" +\n act.returned.String() + \" returned, at \" +\n act.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract. Notice that the \"latest\" variable is defined \"globally\" within the context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package are encapsulated within this \"data realm\", where the data is mutated based on transactions that can potentially cross many realm and non-realm package boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n std.AssertOriginCall()\n caller := std.GetOrigCaller()\n send := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named \"Deposit\". `std.AssertOriginCall() asserts that this function was called by a gno transactional Message. The caller is the user who signed off on this transactional message. Send is the amount of deposit sent along with this message.\n\n```go\n // record activity\n act := \u0026activity{\n caller: caller,\n sent: std.GetOrigSend(),\n returned: send,\n time: time.Now(),\n }\n for i := len(latest) - 2; i \u003e= 0; i-- {\n latest[i+1] = latest[i] // shift by +1.\n }\n latest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n // return if any.\n if returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n banker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n pkgaddr := std.GetOrigPkgAddr()\n // TODO: use std.Coins constructors, this isn't generally safe.\n banker.SendCoins(pkgaddr, caller, send)\n return \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n // get realm coins.\n banker := std.GetBanker(std.BankerTypeReadonly)\n coins := banker.GetCoins(std.GetOrigPkgAddr())\n\n // render\n res := \"\"\n res += \"## recent activity\\n\"\n res += \"\\n\"\n for _, act := range latest {\n if act == nil {\n break\n }\n res += \" * \" + act.String() + \"\\n\"\n }\n res += \"\\n\"\n res += \"## total deposits\\n\"\n res += coins.String()\n return res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:gnolang/4).\n"},{"name":"banktest.gno","body":"package banktest\n\nimport (\n\t\"std\"\n\t\"time\"\n)\n\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime time.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tact.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: time.Now(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n\t// return if any.\n\tif returnAmount \u003e 0 {\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n\t} else {\n\t\treturn \"thank you!\"\n\t}\n}\n\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n"},{"name":"z_0_filetest.gno","body":"// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\n// SEND: 100000000ugnot\n\npackage bank1\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\t// set up main address and banktest addr.\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\tstd.TestSetOrigCaller(mainaddr)\n\tstd.TestSetOrigPkgAddr(banktestAddr)\n\n\t// get and print balance of mainaddr.\n\t// with the SEND, + 200 gnot given by the TestContext, main should have 300gnot.\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\t// simulate a Deposit call. use Send + OrigSend to simulate -send.\n\tbanker.SendCoins(mainaddr, banktestAddr, std.Coins{{\"ugnot\", 100_000_000}})\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 100_000_000}}, nil)\n\tres := banktest.Deposit(\"ugnot\", 50_000_000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n\n\t// simulate a Render(). banker should have given back all coins.\n\tres = banktest.Render(\"\")\n\tprintln(res)\n}\n\n// Output:\n// main before: 300000000ugnot\n// Deposit(): returned!\n// main after: 250000000ugnot\n// ## recent activity\n//\n// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 50000000ugnot\n"},{"name":"z_1_filetest.gno","body":"// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\t// simulate a Deposit call.\n\tstd.TestSetOrigPkgAddr(banktestAddr)\n\tstd.TestIssueCoins(banktestAddr, std.Coins{{\"ugnot\", 100000000}})\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 100000000}}, nil)\n\tres := banktest.Deposit(\"ugnot\", 101000000)\n\tprintln(res)\n}\n\n// Error:\n// cannot send \"101000000ugnot\", limit \"100000000ugnot\" exceeded with \"\" already spent\n"},{"name":"z_2_filetest.gno","body":"// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\t// print main balance before.\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal) // plus OrigSend equals 300.\n\n\t// simulate a Deposit call.\n\tstd.TestSetOrigPkgAddr(banktestAddr)\n\tstd.TestIssueCoins(banktestAddr, std.Coins{{\"ugnot\", 100000000}})\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 100000000}}, nil)\n\tres := banktest.Deposit(\"ugnot\", 55000000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal) // now 255.\n\n\t// simulate a Render().\n\tres = banktest.Render(\"\")\n\tprintln(res)\n}\n\n// Output:\n// main before: 200000000ugnot\n// Deposit(): returned!\n// main after: 255000000ugnot\n// ## recent activity\n//\n// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 45000000ugnot\n"},{"name":"z_3_filetest.gno","body":"// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tsend := std.Coins{{\"ugnot\", 123}}\n\tbanker.SendCoins(banktestAddr, mainaddr, send)\n\n}\n\n// Error:\n// can only send coins from realm that created banker \"g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk\", not \"g1dv3435088tlrgggf745kaud0ptrkc9v42k8llz\"\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"bar20","path":"gno.land/r/demo/bar20","files":[{"name":"bar20.gno","body":"// Package bar20 is similar to gno.land/r/demo/foo20 but exposes a safe-object\n// that can be used by `maketx run`, another contract importing foo20, and in\n// the future when we'll support `maketx call Token.XXX`.\npackage bar20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tbanker *grc20.Banker // private banker.\n\tToken grc20.Token // public safe-object.\n)\n\nfunc init() {\n\tbanker = grc20.NewBanker(\"Bar\", \"BAR\", 4)\n\tToken = banker.Token()\n}\n\nfunc Faucet() string {\n\tcaller := std.PrevRealm().Addr()\n\tif err := banker.Mint(caller, 1_000_000); err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\treturn \"OK\"\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn banker.RenderHome() // XXX: should be Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := std.Address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n"},{"name":"bar20_test.gno","body":"package bar20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestPackage(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\tstd.TestSetOrigCaller(alice) // XXX: should not need this\n\n\turequire.Equal(t, Token.BalanceOf(alice), uint64(0))\n\turequire.Equal(t, Faucet(), \"OK\")\n\turequire.Equal(t, Token.BalanceOf(alice), uint64(1_000_000))\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"counter","path":"gno.land/r/demo/counter","files":[{"name":"counter.gno","body":"package counter\n\nimport \"strconv\"\n\nvar counter int\n\nfunc Increment() int {\n\tcounter++\n\treturn counter\n}\n\nfunc Render(_ string) string {\n\treturn strconv.Itoa(counter)\n}\n"},{"name":"counter_test.gno","body":"package counter\n\nimport \"testing\"\n\nfunc TestIncrement(t *testing.T) {\n\tcounter = 0\n\tval := Increment()\n\tif val != 1 {\n\t\tt.Fatalf(\"result from Increment(): %d != 1\", val)\n\t}\n\tif counter != val {\n\t\tt.Fatalf(\"counter (%d) != val (%d)\", counter, val)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcounter = 1337\n\tres := Render(\"\")\n\tif res != \"1337\" {\n\t\tt.Fatalf(\"render result %q != %q\", res, \"1337\")\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"govdao","path":"gno.land/r/gov/dao/v2","files":[{"name":"dao.gno","body":"package govdao\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/simpledao\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\td *simpledao.SimpleDAO // the current active DAO implementation\n\tmembers membstore.MemberStore // the member store\n)\n\nfunc init() {\n\tvar (\n\t\tset = []membstore.Member{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"), // Jae\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"), // Manfred\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1e6gxg5tvc55mwsn7t7dymmlasratv7mkv0rap2\"), // Milos\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1jazghxvvgz3egnr2fc8uf72z4g0l03596y9ls7\"), // Nemanja\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1qhskthp2uycmg4zsdc9squ2jds7yv3t0qyrlnp\"), // Petar\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g18amm3fc00t43dcxsys6udug0czyvqt9e7p23rd\"), // Marc\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1dfr24yhk5ztwtqn2a36m8f6ud8cx5hww4dkjfl\"), // Antonio\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g19p3yzr3cuhzqa02j0ce6kzvyjqfzwemw3vam0x\"), // Guilhem\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1mx4pum9976th863jgry4sdjzfwu03qan5w2v9j\"), // Ray\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g127l4gkhk0emwsx5tmxe96sp86c05h8vg5tufzq\"), // Maxwell\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1cpx59z5r8vzeww2fm4ezpz7yvjs7kptywkm864\"), // Morgan\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1ker4vvggvsyatexxn3hkthp2hu80pkhrwmuczr\"), // Sergio\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g18x425qmujg99cfz3q97y4uep5pxjq3z8lmpt25\"), // Antoine\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t// GNO DEVX\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g16tfrrul20g4jzt3z303raqw8vs8s2pqqh5clwu\"), // Ilker\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun\"), // Jerónimo\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g15ruzptpql4dpuyzej0wkt5rq6r26kw4nxu9fwd\"), // Denis\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1dnllrdzwfhxv3evyk09y48mgn5phfjvtyrlzm7\"), // Danny\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1778y2yphxs2wpuaflsy5y9qwcd4gttn4g5yjx5\"), // Michelle\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1mq7g0jszdmn4qdpc9tq94w0gyex37su892n80m\"), // Alan\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g197q5e9v00vuz256ly7fq7v3ekaun5cr7wmjgfh\"), // Salvo\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1mpkp5lm8lwpm0pym4388836d009zfe4maxlqsq\"), // Alexis\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"), // Leon\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1whzkakk4hzjkvy60d5pwfk484xu67ar2cl62h2\"), // Kirk\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t// AiB\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr\"), // Albert\n\t\t\t\tVotingPower: 20,\n\t\t\t},\n\t\t\t// ONBLOC\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g12vx7dn3dqq89mz550zwunvg4qw6epq73d9csay\"), // Dongwon\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1r04aw56fgvzy859fachr8hzzhqkulkaemltr76\"), // Blake\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g17n4y745s08awwq4e0a38lagsgtntna0749tnxe\"), // Jinwoo\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1ckae7tc5sez8ul3ssne75sk4muwgttp6ks2ky9\"), // ByeongJun\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t// TERITORI\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a\"), // Norman\n\t\t\t\tVotingPower: 5,\n\t\t\t},\n\t\t\t// BERTY\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g1qynsu9dwj9lq0m5fkje7jh6qy3md80ztqnshhm\"), // Rémi\n\t\t\t\tVotingPower: 5,\n\t\t\t},\n\t\t\t// FLIPPANDO / ZENTASKTIC\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3\"), // Dragos\n\t\t\t\tVotingPower: 5,\n\t\t\t},\n\t\t}\n\t)\n\n\t// Set the member store\n\tmembers = membstore.NewMembStore(membstore.WithInitialMembers(set))\n\n\t// Set the DAO implementation\n\td = simpledao.New(members)\n}\n\n// Propose is designed to be called by another contract or with\n// `maketx run`, not by a `maketx call`.\nfunc Propose(request dao.ProposalRequest) uint64 {\n\tidx, err := d.Propose(request)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn idx\n}\n\n// VoteOnProposal casts a vote for the given proposal\nfunc VoteOnProposal(id uint64, option dao.VoteOption) {\n\tif err := d.VoteOnProposal(id, option); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// ExecuteProposal executes the proposal\nfunc ExecuteProposal(id uint64) {\n\tif err := d.ExecuteProposal(id); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetPropStore returns the active proposal store\nfunc GetPropStore() dao.PropStore {\n\treturn d\n}\n\n// GetMembStore returns the active member store\nfunc GetMembStore() membstore.MemberStore {\n\treturn members\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tnumProposals := d.Size()\n\n\t\tif numProposals == 0 {\n\t\t\treturn \"No proposals found :(\" // corner case\n\t\t}\n\n\t\toutput := \"\"\n\n\t\toffset := uint64(0)\n\t\tif numProposals \u003e= 10 {\n\t\t\toffset = uint64(numProposals) - 10\n\t\t}\n\n\t\t// Fetch the last 10 proposals\n\t\tfor idx, prop := range d.Proposals(offset, uint64(10)) {\n\t\t\toutput += ufmt.Sprintf(\n\t\t\t\t\"- [Proposal #%d](%s:%d) - (**%s**)(by %s)\\n\",\n\t\t\t\tidx,\n\t\t\t\t\"/r/gov/dao/v2\",\n\t\t\t\tidx,\n\t\t\t\tprop.Status().String(),\n\t\t\t\tprop.Author().String(),\n\t\t\t)\n\t\t}\n\n\t\treturn output\n\t}\n\n\t// Display the detailed proposal\n\tidx, err := strconv.Atoi(path)\n\tif err != nil {\n\t\treturn \"404: Invalid proposal ID\"\n\t}\n\n\t// Fetch the proposal\n\tprop, err := d.ProposalByID(uint64(idx))\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"unable to fetch proposal, %s\", err.Error())\n\t}\n\n\t// Render the proposal\n\toutput := \"\"\n\toutput += ufmt.Sprintf(\"# Prop #%d\", idx)\n\toutput += \"\\n\\n\"\n\toutput += prop.Render()\n\toutput += \"\\n\\n\"\n\n\treturn output\n}\n"},{"name":"poc.gno","body":"package govdao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/combinederr\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/gov/executor\"\n)\n\nvar errNoChangesProposed = errors.New(\"no set changes proposed\")\n\n// NewGovDAOExecutor creates the govdao wrapped callback executor\nfunc NewGovDAOExecutor(cb func() error) dao.Executor {\n\tif cb == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\treturn executor.NewCallbackExecutor(\n\t\tcb,\n\t\tstd.CurrentRealm().PkgPath(),\n\t)\n}\n\n// NewMemberPropExecutor returns the GOVDAO member change executor\nfunc NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor {\n\tif changesFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\terrs := \u0026combinederr.CombinedError{}\n\t\tcbMembers := changesFn()\n\n\t\tfor _, member := range cbMembers {\n\t\t\tswitch {\n\t\t\tcase !members.IsMember(member.Address):\n\t\t\t\t// Addition request\n\t\t\t\terr := members.AddMember(member)\n\n\t\t\t\terrs.Add(err)\n\t\t\tcase member.VotingPower == 0:\n\t\t\t\t// Remove request\n\t\t\t\terr := members.UpdateMember(member.Address, membstore.Member{\n\t\t\t\t\tAddress: member.Address,\n\t\t\t\t\tVotingPower: 0, // 0 indicated removal\n\t\t\t\t})\n\n\t\t\t\terrs.Add(err)\n\t\t\tdefault:\n\t\t\t\t// Update request\n\t\t\t\terr := members.UpdateMember(member.Address, member)\n\n\t\t\t\terrs.Add(err)\n\t\t\t}\n\t\t}\n\n\t\t// Check if there were any execution errors\n\t\tif errs.Size() == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errs\n\t}\n\n\treturn NewGovDAOExecutor(callback)\n}\n\nfunc NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor {\n\tif changeFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\tsetMembStoreImpl(changeFn())\n\n\t\treturn nil\n\t}\n\n\treturn NewGovDAOExecutor(callback)\n}\n\n// setMembStoreImpl sets a new dao.MembStore implementation\nfunc setMembStoreImpl(impl membstore.MemberStore) {\n\tif impl == nil {\n\t\tpanic(\"invalid member store\")\n\t}\n\n\tmembers = impl\n}\n"},{"name":"prop1_filetest.gno","body":"// Please note that this package is intended for demonstration purposes only.\n// You could execute this code (the init part) by running a `maketx run` command\n// or by uploading a similar package to a personal namespace.\n//\n// For the specific case of validators, a `r/gnoland/valopers` will be used to\n// organize the lifecycle of validators (register, etc), and this more complex\n// contract will be responsible to generate proposals.\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\tpVals \"gno.land/p/sys/validators\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n\tvalidators \"gno.land/r/sys/validators/v2\"\n)\n\nfunc init() {\n\tchangesFn := func() []pVals.Validator {\n\t\treturn []pVals.Validator{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g12345678\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 10, // add a new validator\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g000000000\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 10, // add a new validator\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g000000000\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 0, // remove an existing validator\n\t\t\t},\n\t\t}\n\t}\n\n\t// Wraps changesFn to emit a certified event only if executed from a\n\t// complete governance proposal process.\n\texecutor := validators.NewPropExecutor(changesFn)\n\n\t// Create a proposal\n\tdescription := \"manual valset changes proposal example\"\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\tgovdao.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.VoteOnProposal(0, dao.YesVote)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(validators.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(validators.Render(\"\"))\n}\n\n// Output:\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// manual valset changes proposal example\n//\n// Status: active\n//\n// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)\n//\n// Threshold met: false\n//\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// manual valset changes proposal example\n//\n// Status: accepted\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// No valset changes to apply.\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// manual valset changes proposal example\n//\n// Status: execution successful\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// Valset changes:\n// - #123: g12345678 (10)\n// - #123: g000000000 (10)\n// - #123: g000000000 (0)\n"},{"name":"prop2_filetest.gno","body":"package main\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/dao\"\n\tgnoblog \"gno.land/r/gnoland/blog\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tex := gnoblog.NewPostExecutor(\n\t\t\"hello-from-govdao\", // slug\n\t\t\"Hello from GovDAO!\", // title\n\t\t\"This post was published by a GovDAO proposal.\", // body\n\t\ttime.Now().Format(time.RFC3339), // publication date\n\t\t\"moul\", // authors\n\t\t\"govdao,example\", // tags\n\t)\n\n\t// Create a proposal\n\tdescription := \"post a new blogpost about govdao\"\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: ex,\n\t}\n\n\tgovdao.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(gnoblog.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(gnoblog.Render(\"\"))\n}\n\n// Output:\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// post a new blogpost about govdao\n//\n// Status: active\n//\n// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)\n//\n// Threshold met: false\n//\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// post a new blogpost about govdao\n//\n// Status: accepted\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// # Gnoland's Blog\n//\n// No posts.\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// post a new blogpost about govdao\n//\n// Status: execution successful\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// # Gnoland's Blog\n//\n// \u003cdiv class='columns-3'\u003e\u003cdiv\u003e\n//\n// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao)\n// 13 Feb 2009\n// \u003c/div\u003e\u003c/div\u003e\n"},{"name":"prop3_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tmemberFn := func() []membstore.Member {\n\t\treturn []membstore.Member{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g123\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g456\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g789\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Create a proposal\n\tdescription := \"add new members to the govdao\"\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: govdao.NewMemberPropExecutor(memberFn),\n\t}\n\n\tgovdao.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.GetMembStore().Size())\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.GetMembStore().Size())\n}\n\n// Output:\n// --\n// 1\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// add new members to the govdao\n//\n// Status: active\n//\n// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)\n//\n// Threshold met: false\n//\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// add new members to the govdao\n//\n// Status: accepted\n//\n// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)\n//\n// Threshold met: true\n//\n//\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// --\n// # Prop #0\n//\n// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n//\n// add new members to the govdao\n//\n// Status: execution successful\n//\n// Voting stats: YES 10 (25%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 30 (75%)\n//\n// Threshold met: false\n//\n//\n// --\n// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)\n//\n// --\n// 4\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"bridge","path":"gno.land/r/gov/dao/bridge","files":[{"name":"bridge.gno","body":"package bridge\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\nconst initialOwner = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\") // @moul\n\nvar b *Bridge\n\n// Bridge is the active GovDAO\n// implementation bridge\ntype Bridge struct {\n\t*ownable.Ownable\n\n\tdao DAO\n}\n\n// init constructs the initial GovDAO implementation\nfunc init() {\n\tb = \u0026Bridge{\n\t\tOwnable: ownable.NewWithAddress(initialOwner),\n\t\tdao: \u0026govdaoV2{},\n\t}\n}\n\n// SetDAO sets the currently active GovDAO implementation\nfunc SetDAO(dao DAO) {\n\tb.AssertCallerIsOwner()\n\n\tb.dao = dao\n}\n\n// GovDAO returns the current GovDAO implementation\nfunc GovDAO() DAO {\n\treturn b.dao\n}\n"},{"name":"bridge_test.gno","body":"package bridge\n\nimport (\n\t\"testing\"\n\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestBridge_DAO(t *testing.T) {\n\tvar (\n\t\tproposalID = uint64(10)\n\t\tmockDAO = \u0026mockDAO{\n\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\treturn proposalID\n\t\t\t},\n\t\t}\n\t)\n\n\tb.dao = mockDAO\n\n\tuassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{}))\n}\n\nfunc TestBridge_SetDAO(t *testing.T) {\n\tt.Run(\"invalid owner\", func(t *testing.T) {\n\t\t// Attempt to set a new DAO implementation\n\t\tuassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() {\n\t\t\tSetDAO(\u0026mockDAO{})\n\t\t})\n\t})\n\n\tt.Run(\"valid owner\", func(t *testing.T) {\n\t\tvar (\n\t\t\taddr = testutils.TestAddress(\"owner\")\n\n\t\t\tproposalID = uint64(10)\n\t\t\tmockDAO = \u0026mockDAO{\n\t\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\t\treturn proposalID\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\tstd.TestSetOrigCaller(addr)\n\n\t\tb.Ownable = ownable.NewWithAddress(addr)\n\n\t\turequire.NotPanics(t, func() {\n\t\t\tSetDAO(mockDAO)\n\t\t})\n\n\t\tuassert.Equal(\n\t\t\tt,\n\t\t\tmockDAO.Propose(dao.ProposalRequest{}),\n\t\t\tGovDAO().Propose(dao.ProposalRequest{}),\n\t\t)\n\t})\n}\n"},{"name":"doc.gno","body":"// Package bridge represents a GovDAO implementation wrapper, used by other Realms and Packages to\n// always fetch the most active GovDAO implementation, instead of directly referencing it, and having to\n// update it each time the GovDAO implementation changes\npackage bridge\n"},{"name":"mock_test.gno","body":"package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\ntype (\n\tproposeDelegate func(dao.ProposalRequest) uint64\n\tvoteOnProposalDelegate func(uint64, dao.VoteOption)\n\texecuteProposalDelegate func(uint64)\n\tgetPropStoreDelegate func() dao.PropStore\n\tgetMembStoreDelegate func() membstore.MemberStore\n\tnewGovDAOExecutorDelegate func(func() error) dao.Executor\n)\n\ntype mockDAO struct {\n\tproposeFn proposeDelegate\n\tvoteOnProposalFn voteOnProposalDelegate\n\texecuteProposalFn executeProposalDelegate\n\tgetPropStoreFn getPropStoreDelegate\n\tgetMembStoreFn getMembStoreDelegate\n\tnewGovDAOExecutorFn newGovDAOExecutorDelegate\n}\n\nfunc (m *mockDAO) Propose(request dao.ProposalRequest) uint64 {\n\tif m.proposeFn != nil {\n\t\treturn m.proposeFn(request)\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockDAO) VoteOnProposal(id uint64, option dao.VoteOption) {\n\tif m.voteOnProposalFn != nil {\n\t\tm.voteOnProposalFn(id, option)\n\t}\n}\n\nfunc (m *mockDAO) ExecuteProposal(id uint64) {\n\tif m.executeProposalFn != nil {\n\t\tm.executeProposalFn(id)\n\t}\n}\n\nfunc (m *mockDAO) GetPropStore() dao.PropStore {\n\tif m.getPropStoreFn != nil {\n\t\treturn m.getPropStoreFn()\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockDAO) GetMembStore() membstore.MemberStore {\n\tif m.getMembStoreFn != nil {\n\t\treturn m.getMembStoreFn()\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockDAO) NewGovDAOExecutor(cb func() error) dao.Executor {\n\tif m.newGovDAOExecutorFn != nil {\n\t\treturn m.newGovDAOExecutorFn(cb)\n\t}\n\n\treturn nil\n}\n"},{"name":"types.gno","body":"package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\n// DAO abstracts the commonly used DAO interface\ntype DAO interface {\n\tPropose(dao.ProposalRequest) uint64\n\tVoteOnProposal(uint64, dao.VoteOption)\n\tExecuteProposal(uint64)\n\tGetPropStore() dao.PropStore\n\tGetMembStore() membstore.MemberStore\n\n\tNewGovDAOExecutor(func() error) dao.Executor\n}\n"},{"name":"v2.gno","body":"package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\n// govdaoV2 is a wrapper for interacting with the /r/gov/dao/v2 Realm\ntype govdaoV2 struct{}\n\nfunc (g *govdaoV2) Propose(request dao.ProposalRequest) uint64 {\n\treturn govdao.Propose(request)\n}\n\nfunc (g *govdaoV2) VoteOnProposal(id uint64, option dao.VoteOption) {\n\tgovdao.VoteOnProposal(id, option)\n}\n\nfunc (g *govdaoV2) ExecuteProposal(id uint64) {\n\tgovdao.ExecuteProposal(id)\n}\n\nfunc (g *govdaoV2) GetPropStore() dao.PropStore {\n\treturn govdao.GetPropStore()\n}\n\nfunc (g *govdaoV2) GetMembStore() membstore.MemberStore {\n\treturn govdao.GetMembStore()\n}\n\nfunc (g *govdaoV2) NewGovDAOExecutor(cb func() error) dao.Executor {\n\treturn govdao.NewGovDAOExecutor(cb)\n}\n\nfunc (g *govdaoV2) NewMemberPropExecutor(cb func() []membstore.Member) dao.Executor {\n\treturn govdao.NewMemberPropExecutor(cb)\n}\n\nfunc (g *govdaoV2) NewMembStoreImplExecutor(cb func() membstore.MemberStore) dao.Executor {\n\treturn govdao.NewMembStoreImplExecutor(cb)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"daoweb","path":"gno.land/r/demo/daoweb","files":[{"name":"daoweb.gno","body":"package daoweb\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/json\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\n// Proposals returns the paginated GovDAO proposals\nfunc Proposals(offset, count uint64) string {\n\tvar (\n\t\tpropStore = bridge.GovDAO().GetPropStore()\n\t\tsize = propStore.Size()\n\t)\n\n\t// Get the props\n\tprops := propStore.Proposals(offset, count)\n\n\tresp := ProposalsResponse{\n\t\tProposals: make([]Proposal, 0, count),\n\t\tTotal: uint64(size),\n\t}\n\n\tfor _, p := range props {\n\t\tprop := Proposal{\n\t\t\tAuthor: p.Author(),\n\t\t\tDescription: p.Description(),\n\t\t\tStatus: p.Status(),\n\t\t\tStats: p.Stats(),\n\t\t\tIsExpired: p.IsExpired(),\n\t\t}\n\n\t\tresp.Proposals = append(resp.Proposals, prop)\n\t}\n\n\t// Encode the response into JSON\n\tencodedProps, err := json.Marshal(encodeProposalsResponse(resp))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encodedProps)\n}\n\n// ProposalByID fetches the proposal using the given ID\nfunc ProposalByID(id uint64) string {\n\tpropStore := bridge.GovDAO().GetPropStore()\n\n\tp, err := propStore.ProposalByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Encode the response into JSON\n\tprop := Proposal{\n\t\tAuthor: p.Author(),\n\t\tDescription: p.Description(),\n\t\tStatus: p.Status(),\n\t\tStats: p.Stats(),\n\t\tIsExpired: p.IsExpired(),\n\t}\n\n\tencodedProp, err := json.Marshal(encodeProposal(prop))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encodedProp)\n}\n\n// encodeProposal encodes a proposal into a json node\nfunc encodeProposal(p Proposal) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"author\": json.StringNode(\"author\", p.Author.String()),\n\t\t\"description\": json.StringNode(\"description\", p.Description),\n\t\t\"status\": json.StringNode(\"status\", p.Status.String()),\n\t\t\"stats\": json.ObjectNode(\"stats\", map[string]*json.Node{\n\t\t\t\"yay_votes\": json.NumberNode(\"yay_votes\", float64(p.Stats.YayVotes)),\n\t\t\t\"nay_votes\": json.NumberNode(\"nay_votes\", float64(p.Stats.NayVotes)),\n\t\t\t\"abstain_votes\": json.NumberNode(\"abstain_votes\", float64(p.Stats.AbstainVotes)),\n\t\t\t\"total_voting_power\": json.NumberNode(\"total_voting_power\", float64(p.Stats.TotalVotingPower)),\n\t\t}),\n\t\t\"is_expired\": json.BoolNode(\"is_expired\", p.IsExpired),\n\t})\n}\n\n// encodeProposalsResponse encodes a proposal response into a JSON node\nfunc encodeProposalsResponse(props ProposalsResponse) *json.Node {\n\tproposals := make([]*json.Node, 0, len(props.Proposals))\n\n\tfor _, p := range props.Proposals {\n\t\tproposals = append(proposals, encodeProposal(p))\n\t}\n\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"proposals\": json.ArrayNode(\"proposals\", proposals),\n\t\t\"total\": json.NumberNode(\"total\", float64(props.Total)),\n\t})\n}\n\n// ProposalsResponse is a paginated proposal response\ntype ProposalsResponse struct {\n\tProposals []Proposal `json:\"proposals\"`\n\tTotal uint64 `json:\"total\"`\n}\n\n// Proposal is a single GovDAO proposal\ntype Proposal struct {\n\tAuthor std.Address `json:\"author\"`\n\tDescription string `json:\"description\"`\n\tStatus dao.ProposalStatus `json:\"status\"`\n\tStats dao.Stats `json:\"stats\"`\n\tIsExpired bool `json:\"is_expired\"`\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"deep","path":"gno.land/r/demo/deep/very/deep","files":[{"name":"render.gno","body":"package deep\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn \"it works!\"\n\t} else {\n\t\treturn \"hi \" + path\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"foo20","path":"gno.land/r/demo/grc20factory","files":[{"name":"grc20factory.gno","body":"package foo20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar instances avl.Tree // symbol -\u003e instance\n\nfunc New(name, symbol string, decimals uint, initialMint, faucet uint64) {\n\tcaller := std.PrevRealm().Addr()\n\tNewWithAdmin(name, symbol, decimals, initialMint, faucet, caller)\n}\n\nfunc NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64, admin std.Address) {\n\texists := instances.Has(symbol)\n\tif exists {\n\t\tpanic(\"token already exists\")\n\t}\n\n\tbanker := grc20.NewBanker(name, symbol, decimals)\n\tif initialMint \u003e 0 {\n\t\tbanker.Mint(admin, initialMint)\n\t}\n\n\tinst := instance{\n\t\tbanker: banker,\n\t\tadmin: ownable.NewWithAddress(admin),\n\t\tfaucet: faucet,\n\t}\n\n\tinstances.Set(symbol, \u0026inst)\n}\n\ntype instance struct {\n\tbanker *grc20.Banker\n\tadmin *ownable.Ownable\n\tfaucet uint64 // per-request amount. disabled if 0.\n}\n\nfunc (inst instance) Token() grc20.Token { return inst.banker.Token() }\n\nfunc TotalSupply(symbol string) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.Token().TotalSupply()\n}\n\nfunc BalanceOf(symbol string, owner std.Address) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.Token().BalanceOf(owner)\n}\n\nfunc Allowance(symbol string, owner, spender std.Address) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.Token().Allowance(owner, spender)\n}\n\nfunc Transfer(symbol string, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.Token().Transfer(to, amount))\n}\n\nfunc Approve(symbol string, spender std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.Token().Approve(spender, amount))\n}\n\nfunc TransferFrom(symbol string, from, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.Token().TransferFrom(from, to, amount))\n}\n\n// faucet.\nfunc Faucet(symbol string) {\n\tinst := mustGetInstance(symbol)\n\tif inst.faucet == 0 {\n\t\tpanic(\"faucet disabled for this token\")\n\t}\n\t// FIXME: add limits?\n\t// FIXME: add payment in gnot?\n\tcaller := std.PrevRealm().Addr()\n\tcheckErr(inst.banker.Mint(caller, inst.faucet))\n}\n\nfunc Mint(symbol string, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertCallerIsOwner()\n\tcheckErr(inst.banker.Mint(to, amount))\n}\n\nfunc Burn(symbol string, from std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertCallerIsOwner()\n\tcheckErr(inst.banker.Burn(from, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn \"TODO: list existing tokens and admins\"\n\tcase c == 1:\n\t\tsymbol := parts[0]\n\t\tinst := mustGetInstance(symbol)\n\t\treturn inst.banker.RenderHome()\n\tcase c == 3 \u0026\u0026 parts[1] == \"balance\":\n\t\tsymbol := parts[0]\n\t\tinst := mustGetInstance(symbol)\n\t\towner := std.Address(parts[2])\n\t\tbalance := inst.Token().BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\nfunc mustGetInstance(symbol string) *instance {\n\tt, exists := instances.Get(symbol)\n\tif !exists {\n\t\tpanic(\"token instance does not exist\")\n\t}\n\treturn t.(*instance)\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"grc20factory_test.gno","body":"package foo20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestReadOnlyPublicMethods(t *testing.T) {\n\tadmin := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\tmanfred := std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\tunknown := std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\") // valid but never used.\n\tNewWithAdmin(\"Foo\", \"FOO\", 4, 10_000*1_000_000, 0, admin)\n\tNewWithAdmin(\"Bar\", \"BAR\", 4, 10_000*1_000, 0, admin)\n\tmustGetInstance(\"FOO\").banker.Mint(manfred, 100_000_000)\n\n\ttype test struct {\n\t\tname string\n\t\tbalance uint64\n\t\tfn func() uint64\n\t}\n\n\t// check balances #1.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_100_000_000, func() uint64 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(manfred)\", 100_000_000, func() uint64 { return BalanceOf(\"FOO\", manfred) }},\n\t\t\t{\"Allowance(admin, manfred)\", 0, func() uint64 { return Allowance(\"FOO\", admin, manfred) }},\n\t\t\t{\"BalanceOf(unknown)\", 0, func() uint64 { return BalanceOf(\"FOO\", unknown) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tuassert.Equal(t, tc.balance, tc.fn(), \"balance does not match\")\n\t\t}\n\t}\n\treturn\n\n\t// unknown uses the faucet.\n\tstd.TestSetOrigCaller(unknown)\n\tFaucet(\"FOO\")\n\n\t// check balances #2.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_110_000_000, func() uint64 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(manfred)\", 100_000_000, func() uint64 { return BalanceOf(\"FOO\", manfred) }},\n\t\t\t{\"Allowance(admin, manfred)\", 0, func() uint64 { return Allowance(\"FOO\", admin, manfred) }},\n\t\t\t{\"BalanceOf(unknown)\", 10_000_000, func() uint64 { return BalanceOf(\"FOO\", unknown) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tuassert.Equal(t, tc.balance, tc.fn(), \"balance does not match\")\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"disperse","path":"gno.land/r/demo/disperse","files":[{"name":"disperse.gno","body":"package disperse\n\nimport (\n\t\"std\"\n\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\n// Get address of Disperse realm\nvar realmAddr = std.CurrentRealm().Addr()\n\n// DisperseUgnot parses receivers and amounts and sends out ugnot\n// The function will send out the coins to the addresses and return the leftover coins to the caller\n// if there are any to return\nfunc DisperseUgnot(addresses []std.Address, coins std.Coins) {\n\tcoinSent := std.GetOrigSend()\n\tcaller := std.PrevRealm().Addr()\n\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n\n\tif len(addresses) != len(coins) {\n\t\tpanic(ErrNumAddrValMismatch)\n\t}\n\n\tfor _, coin := range coins {\n\t\tif coin.Amount \u003c= 0 {\n\t\t\tpanic(ErrNegativeCoinAmount)\n\t\t}\n\n\t\tif banker.GetCoins(realmAddr).AmountOf(coin.Denom) \u003c coin.Amount {\n\t\t\tpanic(ErrMismatchBetweenSentAndParams)\n\t\t}\n\t}\n\n\t// Send coins\n\tfor i, _ := range addresses {\n\t\tbanker.SendCoins(realmAddr, addresses[i], std.NewCoins(coins[i]))\n\t}\n\n\t// Return possible leftover coins\n\tfor _, coin := range coinSent {\n\t\tleftoverAmt := banker.GetCoins(realmAddr).AmountOf(coin.Denom)\n\t\tif leftoverAmt \u003e 0 {\n\t\t\tsend := std.Coins{std.NewCoin(coin.Denom, leftoverAmt)}\n\t\t\tbanker.SendCoins(realmAddr, caller, send)\n\t\t}\n\t}\n}\n\n// DisperseGRC20 disperses tokens to multiple addresses\n// Note that it is necessary to approve the realm to spend the tokens before calling this function\n// see the corresponding filetests for examples\nfunc DisperseGRC20(addresses []std.Address, amounts []uint64, symbols []string) {\n\tcaller := std.PrevRealm().Addr()\n\n\tif (len(addresses) != len(amounts)) || (len(amounts) != len(symbols)) {\n\t\tpanic(ErrArgLenAndSentLenMismatch)\n\t}\n\n\tfor i := 0; i \u003c len(addresses); i++ {\n\t\ttokens.TransferFrom(symbols[i], caller, addresses[i], amounts[i])\n\t}\n}\n\n// DisperseGRC20String receives a string of addresses and a string of tokens\n// and parses them to be used in DisperseGRC20\nfunc DisperseGRC20String(addresses string, tokens string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, parsedSymbols, err := parseTokens(tokens)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tDisperseGRC20(parsedAddresses, parsedAmounts, parsedSymbols)\n}\n\n// DisperseUgnotString receives a string of addresses and a string of amounts\n// and parses them to be used in DisperseUgnot\nfunc DisperseUgnotString(addresses string, amounts string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, err := parseAmounts(amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcoins := make(std.Coins, len(parsedAmounts))\n\tfor i, amount := range parsedAmounts {\n\t\tcoins[i] = std.NewCoin(\"ugnot\", amount)\n\t}\n\n\tDisperseUgnot(parsedAddresses, coins)\n}\n"},{"name":"doc.gno","body":"// Package disperse provides methods to disperse coins or GRC20 tokens among multiple addresses.\n//\n// The disperse package is an implementation of an existing service that allows users to send coins or GRC20 tokens to multiple addresses\n// on the Ethereum blockchain.\n//\n// Usage:\n// To use disperse, you can either use `DisperseUgnot` to send coins or `DisperseGRC20` to send GRC20 tokens to multiple addresses.\n//\n// Example:\n// Dispersing 200 coins to two addresses:\n// - DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n// Dispersing 200 worth of a GRC20 token \"TEST\" to two addresses:\n// - DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n//\n// Reference:\n// - [the original dispere app](https://disperse.app/)\n// - [the original disperse app on etherscan](https://etherscan.io/address/0xd152f549545093347a162dce210e7293f1452150#code)\n// - [the gno disperse web app](https://gno-disperse.netlify.app/)\npackage disperse // import \"gno.land/r/demo/disperse\"\n"},{"name":"errors.gno","body":"package disperse\n\nimport \"errors\"\n\nvar (\n\tErrNotEnoughCoin = errors.New(\"disperse: not enough coin sent in\")\n\tErrNumAddrValMismatch = errors.New(\"disperse: number of addresses and values to send doesn't match\")\n\tErrInvalidAddress = errors.New(\"disperse: invalid address\")\n\tErrNegativeCoinAmount = errors.New(\"disperse: coin amount cannot be negative\")\n\tErrMismatchBetweenSentAndParams = errors.New(\"disperse: mismatch between coins sent and params called\")\n\tErrArgLenAndSentLenMismatch = errors.New(\"disperse: mismatch between coins sent and args called\")\n)\n"},{"name":"util.gno","body":"package disperse\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nfunc parseAddresses(addresses string) ([]std.Address, error) {\n\tvar ret []std.Address\n\n\tfor _, str := range strings.Split(addresses, \",\") {\n\t\taddr := std.Address(str)\n\t\tif !addr.IsValid() {\n\t\t\treturn nil, ErrInvalidAddress\n\t\t}\n\n\t\tret = append(ret, addr)\n\t}\n\n\treturn ret, nil\n}\n\nfunc splitString(input string) (string, string) {\n\tvar pos int\n\tfor i, char := range input {\n\t\tif !unicode.IsDigit(char) {\n\t\t\tpos = i\n\t\t\tbreak\n\t\t}\n\t}\n\treturn input[:pos], input[pos:]\n}\n\nfunc parseTokens(tokens string) ([]uint64, []string, error) {\n\tvar amounts []uint64\n\tvar symbols []string\n\n\tfor _, token := range strings.Split(tokens, \",\") {\n\t\tamountStr, symbol := splitString(token)\n\t\tamount, _ := strconv.Atoi(amountStr)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tamounts = append(amounts, uint64(amount))\n\t\tsymbols = append(symbols, symbol)\n\t}\n\n\treturn amounts, symbols, nil\n}\n\nfunc parseAmounts(amounts string) ([]int64, error) {\n\tvar ret []int64\n\n\tfor _, amt := range strings.Split(amounts, \",\") {\n\t\tamount, _ := strconv.Atoi(amt)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tret = append(ret, int64(amount))\n\t}\n\n\treturn ret, nil\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\n// SEND: 200ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 200}})\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n}\n\n// Output:\n// main before: 200000200ugnot\n// main after: 200000000ugnot\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 300}})\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n}\n\n// Output:\n// main before: 200000300ugnot\n// main after: 200000100ugnot\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 100}})\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n}\n\n// Error:\n// disperse: mismatch between coins sent and params called\n"},{"name":"z_3_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\tbeneficiary1 := std.Address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := std.Address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\ttokens.New(\"test\", \"TEST\", 4, 0, 0)\n\ttokens.Mint(\"TEST\", mainaddr, 200)\n\n\tmainbal := tokens.BalanceOf(\"TEST\", mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\ttokens.Approve(\"TEST\", disperseAddr, 200)\n\n\tdisperse.DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n\n\tmainbal = tokens.BalanceOf(\"TEST\", mainaddr)\n\tprintln(\"main after:\", mainbal)\n\tben1bal := tokens.BalanceOf(\"TEST\", beneficiary1)\n\tprintln(\"beneficiary1:\", ben1bal)\n\tben2bal := tokens.BalanceOf(\"TEST\", beneficiary2)\n\tprintln(\"beneficiary2:\", ben2bal)\n}\n\n// Output:\n// main before: 200\n// main after: 0\n// beneficiary1: 150\n// beneficiary2: 50\n"},{"name":"z_4_filetest.gno","body":"// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/disperse\"\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\tbeneficiary1 := std.Address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := std.Address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\tstd.TestSetOrigPkgAddr(disperseAddr)\n\tstd.TestSetOrigCaller(mainaddr)\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\n\ttokens.New(\"test1\", \"TEST1\", 4, 0, 0)\n\ttokens.Mint(\"TEST1\", mainaddr, 200)\n\ttokens.New(\"test2\", \"TEST2\", 4, 0, 0)\n\ttokens.Mint(\"TEST2\", mainaddr, 200)\n\n\tmainbal := tokens.BalanceOf(\"TEST1\", mainaddr) + tokens.BalanceOf(\"TEST2\", mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\ttokens.Approve(\"TEST1\", disperseAddr, 200)\n\ttokens.Approve(\"TEST2\", disperseAddr, 200)\n\n\tdisperse.DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"200TEST1,200TEST2\")\n\n\tmainbal = tokens.BalanceOf(\"TEST1\", mainaddr) + tokens.BalanceOf(\"TEST2\", mainaddr)\n\tprintln(\"main after:\", mainbal)\n\tben1bal := tokens.BalanceOf(\"TEST1\", beneficiary1) + tokens.BalanceOf(\"TEST2\", beneficiary1)\n\tprintln(\"beneficiary1:\", ben1bal)\n\tben2bal := tokens.BalanceOf(\"TEST1\", beneficiary2) + tokens.BalanceOf(\"TEST2\", beneficiary2)\n\tprintln(\"beneficiary2:\", ben2bal)\n}\n\n// Output:\n// main before: 400\n// main after: 0\n// beneficiary1: 200\n// beneficiary2: 200\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"echo","path":"gno.land/r/demo/echo","files":[{"name":"echo.gno","body":"package echo\n\n/*\n * This realm echoes the `path` argument it received.\n * Can be used by developers as a simple endpoint to test\n * forbidden characters, for pentesting or simply to\n * test it works.\n *\n * See also r/demo/print (to print various thing like user address)\n */\nfunc Render(path string) string {\n\treturn path\n}\n"},{"name":"echo_test.gno","body":"package echo\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc Test(t *testing.T) {\n\turequire.Equal(t, \"aa\", Render(\"aa\"))\n\turequire.Equal(t, \"\", Render(\"\"))\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"event","path":"gno.land/r/demo/event","files":[{"name":"event.gno","body":"package event\n\nimport (\n\t\"std\"\n)\n\nfunc Emit(value string) {\n\tstd.Emit(\"TAG\", \"key\", value)\n}\n"},{"name":"z1_filetest.gno","body":"package main\n\nimport \"gno.land/r/demo/event\"\n\nfunc main() {\n\tevent.Emit(\"foo\")\n\tevent.Emit(\"bar\")\n}\n\n// Events:\n// [\n// {\n// \"type\": \"TAG\",\n// \"attrs\": [\n// {\n// \"key\": \"key\",\n// \"value\": \"foo\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/demo/event\",\n// \"func\": \"Emit\"\n// },\n// {\n// \"type\": \"TAG\",\n// \"attrs\": [\n// {\n// \"key\": \"key\",\n// \"value\": \"bar\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/demo/event\",\n// \"func\": \"Emit\"\n// }\n// ]\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"foo1155","path":"gno.land/r/demo/foo1155","files":[{"name":"foo1155.gno","body":"package foo1155\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nvar (\n\tdummyURI = \"ipfs://xyz\"\n\tadmin std.Address = \"g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530\"\n\tfoo = grc1155.NewBasicGRC1155Token(dummyURI)\n)\n\nfunc init() {\n\tmintGRC1155Token(admin) // @administrator (10)\n}\n\nfunc mintGRC1155Token(owner std.Address) {\n\tfor i := 1; i \u003c= 10; i++ {\n\t\ttid := grc1155.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.SafeMint(owner, tid, 100)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user pusers.AddressOrName, tid grc1155.TokenID) uint64 {\n\tbalance, err := foo.BalanceOf(users.Resolve(user), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc BalanceOfBatch(ul []pusers.AddressOrName, batch []grc1155.TokenID) []uint64 {\n\tvar usersResolved []std.Address\n\n\tfor i := 0; i \u003c len(ul); i++ {\n\t\tusersResolved[i] = users.Resolve(ul[i])\n\t}\n\tbalanceBatch, err := foo.BalanceOfBatch(usersResolved, batch)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balanceBatch\n}\n\nfunc IsApprovedForAll(owner, user pusers.AddressOrName) bool {\n\treturn foo.IsApprovedForAll(users.Resolve(owner), users.Resolve(user))\n}\n\n// Setters\n\nfunc SetApprovalForAll(user pusers.AddressOrName, approved bool) {\n\terr := foo.SetApprovalForAll(users.Resolve(user), approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, tid grc1155.TokenID, amount uint64) {\n\terr := foo.SafeTransferFrom(users.Resolve(from), users.Resolve(to), tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BatchTransferFrom(from, to pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) {\n\terr := foo.SafeBatchTransferFrom(users.Resolve(from), users.Resolve(to), batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(to pusers.AddressOrName, tid grc1155.TokenID, amount uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeMint(users.Resolve(to), tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc MintBatch(to pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeBatchMint(users.Resolve(to), batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(from pusers.AddressOrName, tid grc1155.TokenID, amount uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(users.Resolve(from), tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BurnBatch(from pusers.AddressOrName, batch []grc1155.TokenID, amounts []uint64) {\n\tcaller := std.GetOrigCaller()\n\tassertIsAdmin(caller)\n\terr := foo.BatchBurn(users.Resolve(from), batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address std.Address) {\n\tif address != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n"},{"name":"foo1155_test.gno","body":"package foo1155\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/users\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := users.AddressOrName(\"g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530\")\n\tbob := users.AddressOrName(\"g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw\")\n\ttid1 := grc1155.TokenID(\"1\")\n\ttid2 := grc1155.TokenID(\"2\")\n\n\tfor i, tc := range []struct {\n\t\tname string\n\t\texpected interface{}\n\t\tfn func() interface{}\n\t}{\n\t\t{\"BalanceOf(admin, tid1)\", uint64(100), func() interface{} { return BalanceOf(admin, tid1) }},\n\t\t{\"BalanceOf(bob, tid1)\", uint64(0), func() interface{} { return BalanceOf(bob, tid1) }},\n\t\t{\"IsApprovedForAll(admin, bob)\", false, func() interface{} { return IsApprovedForAll(admin, bob) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"foo20","path":"gno.land/r/demo/foo20","files":[{"name":"foo20.gno","body":"// foo20 is a GRC20 token contract where all the GRC20 methods are proxified\n// with top-level functions. see also gno.land/r/demo/bar20.\npackage foo20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\tpusers \"gno.land/p/demo/users\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbanker *grc20.Banker\n\tadmin *ownable.Ownable\n\ttoken grc20.Token\n)\n\nfunc init() {\n\tadmin = ownable.NewWithAddress(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\") // @manfred\n\tbanker = grc20.NewBanker(\"Foo\", \"FOO\", 4)\n\tbanker.Mint(admin.Owner(), 1000000*10000) // @administrator (1M)\n\ttoken = banker.Token()\n}\n\nfunc TotalSupply() uint64 { return token.TotalSupply() }\n\nfunc BalanceOf(owner pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\treturn token.BalanceOf(ownerAddr)\n}\n\nfunc Allowance(owner, spender pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\tspenderAddr := users.Resolve(spender)\n\treturn token.Allowance(ownerAddr, spenderAddr)\n}\n\nfunc Transfer(to pusers.AddressOrName, amount uint64) {\n\ttoAddr := users.Resolve(to)\n\tcheckErr(token.Transfer(toAddr, amount))\n}\n\nfunc Approve(spender pusers.AddressOrName, amount uint64) {\n\tspenderAddr := users.Resolve(spender)\n\tcheckErr(token.Approve(spenderAddr, amount))\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, amount uint64) {\n\tfromAddr := users.Resolve(from)\n\ttoAddr := users.Resolve(to)\n\tcheckErr(token.TransferFrom(fromAddr, toAddr, amount))\n}\n\n// Faucet is distributing foo20 tokens without restriction (unsafe).\n// For a real token faucet, you should take care of setting limits are asking payment.\nfunc Faucet() {\n\tcaller := std.PrevRealm().Addr()\n\tamount := uint64(1_000 * 10_000) // 1k\n\tcheckErr(banker.Mint(caller, amount))\n}\n\nfunc Mint(to pusers.AddressOrName, amount uint64) {\n\tadmin.AssertCallerIsOwner()\n\ttoAddr := users.Resolve(to)\n\tcheckErr(banker.Mint(toAddr, amount))\n}\n\nfunc Burn(from pusers.AddressOrName, amount uint64) {\n\tadmin.AssertCallerIsOwner()\n\tfromAddr := users.Resolve(from)\n\tcheckErr(banker.Burn(fromAddr, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn banker.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := pusers.AddressOrName(parts[1])\n\t\townerAddr := users.Resolve(owner)\n\t\tbalance := banker.BalanceOf(ownerAddr)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"foo20_test.gno","body":"package foo20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\tpusers \"gno.land/p/demo/users\"\n\t\"gno.land/r/demo/users\"\n)\n\nfunc TestReadOnlyPublicMethods(t *testing.T) {\n\tvar (\n\t\tadmin = pusers.AddressOrName(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\t\talice = pusers.AddressOrName(testutils.TestAddress(\"alice\"))\n\t\tbob = pusers.AddressOrName(testutils.TestAddress(\"bob\"))\n\t)\n\n\ttype test struct {\n\t\tname string\n\t\tbalance uint64\n\t\tfn func() uint64\n\t}\n\n\t// check balances #1.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_000_000_000, func() uint64 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint64 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint64 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 0, func() uint64 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tuassert.Equal(t, got, tc.balance)\n\t\t}\n\t}\n\n\t// bob uses the faucet.\n\tstd.TestSetOrigCaller(users.Resolve(bob))\n\tFaucet()\n\n\t// check balances #2.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_010_000_000, func() uint64 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint64 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint64 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 10_000_000, func() uint64 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tuassert.Equal(t, got, tc.balance)\n\t\t}\n\t}\n}\n\nfunc TestErrConditions(t *testing.T) {\n\tvar (\n\t\tadmin = pusers.AddressOrName(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\t\talice = pusers.AddressOrName(testutils.TestAddress(\"alice\"))\n\t\tempty = pusers.AddressOrName(\"\")\n\t)\n\n\ttype test struct {\n\t\tname string\n\t\tmsg string\n\t\tfn func()\n\t}\n\n\tstd.TestSetOrigCaller(users.Resolve(admin))\n\t{\n\t\ttests := []test{\n\t\t\t{\"Transfer(admin, 1)\", \"cannot send transfer to self\", func() { Transfer(admin, 1) }},\n\t\t\t{\"Approve(empty, 1))\", \"invalid address\", func() { Approve(empty, 1) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tuassert.PanicsWithMessage(t, tc.msg, tc.fn)\n\t\t\t})\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"foo721","path":"gno.land/r/demo/foo721","files":[{"name":"foo721.gno","body":"package foo721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nvar (\n\tadmin std.Address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"\n\tfoo = grc721.NewBasicNFT(\"FooNFT\", \"FNFT\")\n)\n\nfunc init() {\n\tmintNNFT(admin, 10) // @administrator (10)\n\tmintNNFT(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", 5) // @hariom (5)\n}\n\nfunc mintNNFT(owner std.Address, n uint64) {\n\tcount := foo.TokenCount()\n\tfor i := count; i \u003c count+n; i++ {\n\t\ttid := grc721.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.Mint(owner, tid)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user pusers.AddressOrName) uint64 {\n\tbalance, err := foo.BalanceOf(users.Resolve(user))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc OwnerOf(tid grc721.TokenID) std.Address {\n\towner, err := foo.OwnerOf(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn owner\n}\n\nfunc IsApprovedForAll(owner, user pusers.AddressOrName) bool {\n\treturn foo.IsApprovedForAll(users.Resolve(owner), users.Resolve(user))\n}\n\nfunc GetApproved(tid grc721.TokenID) std.Address {\n\taddr, err := foo.GetApproved(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn addr\n}\n\n// Setters\n\nfunc Approve(user pusers.AddressOrName, tid grc721.TokenID) {\n\terr := foo.Approve(users.Resolve(user), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc SetApprovalForAll(user pusers.AddressOrName, approved bool) {\n\terr := foo.SetApprovalForAll(users.Resolve(user), approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) {\n\terr := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(to pusers.AddressOrName, tid grc721.TokenID) {\n\tcaller := std.PrevRealm().Addr()\n\tassertIsAdmin(caller)\n\terr := foo.Mint(users.Resolve(to), tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(tid grc721.TokenID) {\n\tcaller := std.PrevRealm().Addr()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address std.Address) {\n\tif address != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n"},{"name":"foo721_test.gno","body":"package foo721\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/r/demo/users\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := pusers.AddressOrName(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\thariom := pusers.AddressOrName(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tfor i, tc := range []struct {\n\t\tname string\n\t\texpected interface{}\n\t\tfn func() interface{}\n\t}{\n\t\t{\"BalanceOf(admin)\", uint64(10), func() interface{} { return BalanceOf(admin) }},\n\t\t{\"BalanceOf(hariom)\", uint64(5), func() interface{} { return BalanceOf(hariom) }},\n\t\t{\"OwnerOf(0)\", users.Resolve(admin), func() interface{} { return OwnerOf(grc721.TokenID(\"0\")) }},\n\t\t{\"IsApprovedForAll(admin, hariom)\", false, func() interface{} { return IsApprovedForAll(admin, hariom) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"dice_roller","path":"gno.land/r/demo/games/dice_roller","files":[{"name":"dice_roller.gno","body":"package dice_roller\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n)\n\ntype (\n\t// game represents a Dice Roller game between two players\n\tgame struct {\n\t\tplayer1, player2 std.Address\n\t\troll1, roll2 int\n\t}\n\n\t// player holds the information about each player including their stats\n\tplayer struct {\n\t\taddr std.Address\n\t\twins, losses, draws, points int\n\t}\n\n\t// leaderBoard is a slice of players, used to sort players by rank\n\tleaderBoard []player\n)\n\nconst (\n\t// Constants to represent game result outcomes\n\tongoing = iota\n\twin\n\tdraw\n\tloss\n)\n\nvar (\n\tgames avl.Tree // AVL tree for storing game states\n\tgameId seqid.ID // Sequence ID for games\n\n\tplayers avl.Tree // AVL tree for storing player data\n\n\tseed = uint64(entropy.New().Seed())\n\tr = rand.New(rand.NewPCG(seed, 0xdeadbeef))\n)\n\n// rollDice generates a random dice roll between 1 and 6\nfunc rollDice() int {\n\treturn r.IntN(6) + 1\n}\n\n// NewGame initializes a new game with the provided opponent's address\nfunc NewGame(addr std.Address) int {\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid opponent's address\")\n\t}\n\n\tgames.Set(gameId.Next().String(), \u0026game{\n\t\tplayer1: std.PrevRealm().Addr(),\n\t\tplayer2: addr,\n\t})\n\n\treturn int(gameId)\n}\n\n// Play allows a player to roll the dice and updates the game state accordingly\nfunc Play(idx int) int {\n\tg, err := getGame(idx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\troll := rollDice() // Random the player's dice roll\n\n\t// Play the game and update the player's roll\n\tif err := g.play(std.PrevRealm().Addr(), roll); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// If both players have rolled, update the results and leaderboard\n\tif g.isFinished() {\n\t\t// If the player is playing against themselves, no points are awarded\n\t\tif g.player1 == g.player2 {\n\t\t\treturn roll\n\t\t}\n\n\t\tplayer1 := getPlayer(g.player1)\n\t\tplayer2 := getPlayer(g.player2)\n\n\t\tif g.roll1 \u003e g.roll2 {\n\t\t\tplayer1.updateStats(win)\n\t\t\tplayer2.updateStats(loss)\n\t\t} else if g.roll2 \u003e g.roll1 {\n\t\t\tplayer2.updateStats(win)\n\t\t\tplayer1.updateStats(loss)\n\t\t} else {\n\t\t\tplayer1.updateStats(draw)\n\t\t\tplayer2.updateStats(draw)\n\t\t}\n\t}\n\n\treturn roll\n}\n\n// play processes a player's roll and updates their score\nfunc (g *game) play(player std.Address, roll int) error {\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\n\tif g.isFinished() {\n\t\treturn errors.New(\"game over\")\n\t}\n\n\tif player == g.player1 \u0026\u0026 g.roll1 == 0 {\n\t\tg.roll1 = roll\n\t\treturn nil\n\t}\n\n\tif player == g.player2 \u0026\u0026 g.roll2 == 0 {\n\t\tg.roll2 = roll\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"already played\")\n}\n\n// isFinished checks if the game has ended\nfunc (g *game) isFinished() bool {\n\treturn g.roll1 != 0 \u0026\u0026 g.roll2 != 0\n}\n\n// checkResult returns the game status as a formatted string\nfunc (g *game) status() string {\n\tif !g.isFinished() {\n\t\treturn resultIcon(ongoing) + \" Game still in progress\"\n\t}\n\n\tif g.roll1 \u003e g.roll2 {\n\t\treturn resultIcon(win) + \" Player1 Wins !\"\n\t} else if g.roll2 \u003e g.roll1 {\n\t\treturn resultIcon(win) + \" Player2 Wins !\"\n\t} else {\n\t\treturn resultIcon(draw) + \" It's a Draw !\"\n\t}\n}\n\n// Render provides a summary of the current state of games and leader board\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(`# 🎲 **Dice Roller Game**\n\nWelcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score !\n\n---\n\n## **How to Play**:\n1. **Create a game**: Challenge an opponent using [NewGame](./dice_roller$help\u0026func=NewGame)\n2. **Roll the dice**: Play your turn by rolling a dice using [Play](./dice_roller$help\u0026func=Play)\n\n---\n\n## **Scoring Rules**:\n- **Win** 🏆: +3 points\n- **Draw** 🤝: +1 point each\n- **Lose** ❌: No points\n- **Playing against yourself**: No points or stats changes for you\n\n---\n\n## **Recent Games**:\nBelow are the results from the most recent games. Up to 10 recent games are displayed\n\n| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | 🏆 Winner |\n|------|----------|-----------|----------|-----------|-----------|\n`)\n\n\tmaxGames := 10\n\tfor n := int(gameId); n \u003e 0 \u0026\u0026 int(gameId)-n \u003c maxGames; n-- {\n\t\tg, err := getGame(n)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tsb.WriteString(strconv.Itoa(n) + \" | \" +\n\t\t\t\"\u003cspan title=\\\"\" + string(g.player1) + \"\\\"\u003e\" + shortName(g.player1) + \"\u003c/span\u003e\" + \" | \" + diceIcon(g.roll1) + \" | \" +\n\t\t\t\"\u003cspan title=\\\"\" + string(g.player2) + \"\\\"\u003e\" + shortName(g.player2) + \"\u003c/span\u003e\" + \" | \" + diceIcon(g.roll2) + \" | \" +\n\t\t\tg.status() + \"\\n\")\n\t}\n\n\tsb.WriteString(`\n---\n\n## **Leaderboard**:\nThe top players are ranked by performance. Games played against oneself are not counted in the leaderboard\n\n| Rank | Player | Wins | Losses | Draws | Points |\n|------|-----------------------|------|--------|-------|--------|\n`)\n\n\tfor i, player := range getLeaderBoard() {\n\t\tsb.WriteString(ufmt.Sprintf(\"| %s | \u003cspan title=\\\"\"+string(player.addr)+\"\\\"\u003e**%s**\u003c/span\u003e | %d | %d | %d | %d |\\n\",\n\t\t\trankIcon(i+1),\n\t\t\tshortName(player.addr),\n\t\t\tplayer.wins,\n\t\t\tplayer.losses,\n\t\t\tplayer.draws,\n\t\t\tplayer.points,\n\t\t))\n\t}\n\n\tsb.WriteString(\"\\n---\\n**Good luck and have fun !** 🎉\")\n\treturn sb.String()\n}\n\n// shortName returns a shortened name for the given address\nfunc shortName(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user != nil {\n\t\treturn user.Name\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n\n// getGame retrieves the game state by its ID\nfunc getGame(idx int) (*game, error) {\n\tv, ok := games.Get(seqid.ID(idx).String())\n\tif !ok {\n\t\treturn nil, errors.New(\"game not found\")\n\t}\n\treturn v.(*game), nil\n}\n\n// updateResult updates the player's stats and points based on the game outcome\nfunc (p *player) updateStats(result int) {\n\tswitch result {\n\tcase win:\n\t\tp.wins++\n\t\tp.points += 3\n\tcase loss:\n\t\tp.losses++\n\tcase draw:\n\t\tp.draws++\n\t\tp.points++\n\t}\n}\n\n// getPlayer retrieves a player or initializes a new one if they don't exist\nfunc getPlayer(addr std.Address) *player {\n\tv, ok := players.Get(addr.String())\n\tif !ok {\n\t\tplayer := \u0026player{\n\t\t\taddr: addr,\n\t\t}\n\t\tplayers.Set(addr.String(), player)\n\t\treturn player\n\t}\n\n\treturn v.(*player)\n}\n\n// getLeaderBoard generates a leaderboard sorted by points\nfunc getLeaderBoard() leaderBoard {\n\tboard := leaderBoard{}\n\tplayers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tplayer := value.(*player)\n\t\tboard = append(board, *player)\n\t\treturn false\n\t})\n\n\tsort.Sort(board)\n\n\treturn board\n}\n\n// Methods for sorting the leaderboard\nfunc (r leaderBoard) Len() int {\n\treturn len(r)\n}\n\nfunc (r leaderBoard) Less(i, j int) bool {\n\tif r[i].points != r[j].points {\n\t\treturn r[i].points \u003e r[j].points\n\t}\n\n\tif r[i].wins != r[j].wins {\n\t\treturn r[i].wins \u003e r[j].wins\n\t}\n\n\tif r[i].draws != r[j].draws {\n\t\treturn r[i].draws \u003e r[j].draws\n\t}\n\n\treturn false\n}\n\nfunc (r leaderBoard) Swap(i, j int) {\n\tr[i], r[j] = r[j], r[i]\n}\n"},{"name":"dice_roller_test.gno","body":"package dice_roller\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tplayer1 = testutils.TestAddress(\"alice\")\n\tplayer2 = testutils.TestAddress(\"bob\")\n\tunknownPlayer = testutils.TestAddress(\"unknown\")\n)\n\n// resetGameState resets the game state for testing\nfunc resetGameState() {\n\tgames = avl.Tree{}\n\tgameId = seqid.ID(0)\n\tplayers = avl.Tree{}\n}\n\n// TestNewGame tests the initialization of a new game\nfunc TestNewGame(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Verify that the game has been correctly initialized\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\turequire.Equal(t, player1.String(), g.player1.String())\n\turequire.Equal(t, player2.String(), g.player2.String())\n\turequire.Equal(t, 0, g.roll1)\n\turequire.Equal(t, 0, g.roll2)\n}\n\n// TestPlay tests the dice rolling functionality for both players\nfunc TestPlay(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\n\t// Simulate rolling dice for player 1\n\troll1 := Play(gameID)\n\n\t// Verify player 1's roll\n\turequire.NotEqual(t, 0, g.roll1)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet\n\n\t// Simulate rolling dice for player 2\n\tstd.TestSetOrigCaller(player2)\n\troll2 := Play(gameID)\n\n\t// Verify player 2's roll\n\turequire.NotEqual(t, 0, g.roll2)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, g.roll2, roll2)\n}\n\n// TestPlayAgainstSelf tests the scenario where a player plays against themselves\nfunc TestPlayAgainstSelf(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player1)\n\n\t// Simulate rolling dice twice by the same player\n\troll1 := Play(gameID)\n\troll2 := Play(gameID)\n\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, g.roll2, roll2)\n}\n\n// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play\nfunc TestPlayInvalidPlayer(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player1)\n\n\t// Attempt to play as an invalid player\n\tstd.TestSetOrigCaller(unknownPlayer)\n\turequire.PanicsWithMessage(t, \"invalid player\", func() {\n\t\tPlay(gameID)\n\t})\n}\n\n// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing\nfunc TestPlayAlreadyPlayed(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Player 1 rolls\n\tPlay(gameID)\n\n\t// Player 1 tries to roll again\n\turequire.PanicsWithMessage(t, \"already played\", func() {\n\t\tPlay(gameID)\n\t})\n}\n\n// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails\nfunc TestPlayBeyondGameEnd(t *testing.T) {\n\tresetGameState()\n\n\tstd.TestSetOrigCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Play for both players\n\tstd.TestSetOrigCaller(player1)\n\tPlay(gameID)\n\tstd.TestSetOrigCaller(player2)\n\tPlay(gameID)\n\n\t// Check if the game is over\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\n\t// Attempt to play more should fail\n\tstd.TestSetOrigCaller(player1)\n\turequire.PanicsWithMessage(t, \"game over\", func() {\n\t\tPlay(gameID)\n\t})\n}\n"},{"name":"icon.gno","body":"package dice_roller\n\nimport (\n\t\"strconv\"\n)\n\n// diceIcon returns an icon of the dice roll\nfunc diceIcon(roll int) string {\n\tswitch roll {\n\tcase 1:\n\t\treturn \"🎲1\"\n\tcase 2:\n\t\treturn \"🎲2\"\n\tcase 3:\n\t\treturn \"🎲3\"\n\tcase 4:\n\t\treturn \"🎲4\"\n\tcase 5:\n\t\treturn \"🎲5\"\n\tcase 6:\n\t\treturn \"🎲6\"\n\tdefault:\n\t\treturn \"❓\"\n\t}\n}\n\n// resultIcon returns the icon representing the result of a game\nfunc resultIcon(result int) string {\n\tswitch result {\n\tcase ongoing:\n\t\treturn \"🔄\"\n\tcase win:\n\t\treturn \"🏆\"\n\tcase loss:\n\t\treturn \"❌\"\n\tcase draw:\n\t\treturn \"🤝\"\n\tdefault:\n\t\treturn \"❓\"\n\t}\n}\n\n// rankIcon returns the icon for a player's rank\nfunc rankIcon(rank int) string {\n\tswitch rank {\n\tcase 1:\n\t\treturn \"🥇\"\n\tcase 2:\n\t\treturn \"🥈\"\n\tcase 3:\n\t\treturn \"🥉\"\n\tdefault:\n\t\treturn strconv.Itoa(rank)\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"shifumi","path":"gno.land/r/demo/games/shifumi","files":[{"name":"shifumi.gno","body":"package shifumi\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nconst (\n\tempty = iota\n\trock\n\tpaper\n\tscissors\n\tlast\n)\n\ntype game struct {\n\tplayer1, player2 std.Address // shifumi is a 2 players game\n\tmove1, move2 int // can be empty, rock, paper, or scissors\n}\n\nvar games avl.Tree\nvar id seqid.ID\n\nfunc (g *game) play(player std.Address, move int) error {\n\tif !(move \u003e empty \u0026\u0026 move \u003c last) {\n\t\treturn errors.New(\"invalid move\")\n\t}\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\tif player == g.player1 \u0026\u0026 g.move1 == empty {\n\t\tg.move1 = move\n\t\treturn nil\n\t}\n\tif player == g.player2 \u0026\u0026 g.move2 == empty {\n\t\tg.move2 = move\n\t\treturn nil\n\t}\n\treturn errors.New(\"already played\")\n}\n\nfunc (g *game) winner() int {\n\tif g.move1 == empty || g.move2 == empty {\n\t\treturn -1\n\t}\n\tif g.move1 == g.move2 {\n\t\treturn 0\n\t}\n\tif g.move1 == rock \u0026\u0026 g.move2 == scissors ||\n\t\tg.move1 == paper \u0026\u0026 g.move2 == rock ||\n\t\tg.move1 == scissors \u0026\u0026 g.move2 == paper {\n\t\treturn 1\n\t}\n\treturn 2\n}\n\n// NewGame creates a new game where player1 is the caller and player2 the argument.\n// A new game index is returned.\nfunc NewGame(player std.Address) int {\n\tgames.Set(id.Next().String(), \u0026game{player1: std.PrevRealm().Addr(), player2: player})\n\treturn int(id)\n}\n\n// Play executes a move for the game at index idx, where move can be:\n// 1 (rock), 2 (paper), 3 (scissors).\nfunc Play(idx, move int) {\n\tv, ok := games.Get(seqid.ID(idx).String())\n\tif !ok {\n\t\tpanic(\"game not found\")\n\t}\n\tif err := v.(*game).play(std.PrevRealm().Addr(), move); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tmov1 := []string{\"\", \" 🤜 \", \" 🫱 \", \" 👉 \"}\n\tmov2 := []string{\"\", \" 🤛 \", \" 🫲 \", \" 👈 \"}\n\twin := []string{\"pending\", \"draw\", \"player1\", \"player2\"}\n\n\toutput := `# 👊 ✋ ✌️ Shifumi\nActions:\n* [NewGame](shifumi$help\u0026func=NewGame) opponentAddress\n* [Play](shifumi$help\u0026func=Play) gameIndex move (1=rock, 2=paper, 3=scissors)\n\n game | player1 | | player2 | | win \n --- | --- | --- | --- | --- | ---\n`\n\t// Output the 100 most recent games.\n\tmaxGames := 100\n\tfor n := int(id); n \u003e 0 \u0026\u0026 int(id)-n \u003c maxGames; n-- {\n\t\tv, ok := games.Get(seqid.ID(n).String())\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tg := v.(*game)\n\t\toutput += strconv.Itoa(n) + \" | \" +\n\t\t\tshortName(g.player1) + \" | \" + mov1[g.move1] + \" | \" +\n\t\t\tshortName(g.player2) + \" | \" + mov2[g.move2] + \" | \" +\n\t\t\twin[g.winner()+1] + \"\\n\"\n\t}\n\treturn output\n}\n\nfunc shortName(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user != nil {\n\t\treturn user.Name\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"groups","path":"gno.land/r/demo/groups","files":[{"name":"README.md","body":"### - test package\n\n ./build/gno test examples/gno.land/r/demo/groups/\n\n### - add pkg\n\n ./build/gnokey maketx addpkg -pkgdir \"examples/gno.land/r/demo/groups\" -deposit 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1 \n\n### - create group\n\n ./build/gnokey maketx call -func \"CreateGroup\" -args \"dao_trinity_ngo\" -gas-fee \"1000000ugnot\" -gas-wanted 4000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1 \n\n### - add member\n\n ./build/gnokey maketx call -func \"AddMember\" -args \"1\" -args \"g1hd3gwzevxlqmd3jsf64mpfczag8a8e5j2wdn3c\" -args 12 -args \"i am new user\" -gas-fee \"1000000ugnot\" -gas-wanted \"4000000\" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1\n\n### - delete member\n\n ./build/gnokey maketx call -func \"DeleteMember\" -args \"1\" -args \"0\" -gas-fee \"1000000ugnot\" -gas-wanted \"4000000\" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1\n\n### - delete group\n\n ./build/gnokey maketx call -func \"DeleteGroup\" -args \"1\" -gas-fee \"1000000ugnot\" -gas-wanted \"4000000\" -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath \"gno.land/r/demo/groups\" test1\n\n"},{"name":"group.gno","body":"package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype GroupID uint64\n\nfunc (gid GroupID) String() string {\n\treturn strconv.Itoa(int(gid))\n}\n\ntype Group struct {\n\tid GroupID\n\turl string\n\tname string\n\tlastMemberID MemberID\n\tmembers avl.Tree\n\tcreator std.Address\n\tcreatedAt time.Time\n}\n\nfunc newGroup(url string, name string, creator std.Address) *Group {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\tif gGroupsByName.Has(name) {\n\t\tpanic(\"Group with such name already exists\")\n\t}\n\treturn \u0026Group{\n\t\tid: incGetGroupID(),\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\tmembers: avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) newMember(id MemberID, address std.Address, weight int, metadata string) *Member {\n\tif group.members.Has(address.String()) {\n\t\tpanic(\"this member for this group already exists\")\n\t}\n\treturn \u0026Member{\n\t\tid: id,\n\t\taddress: address,\n\t\tweight: weight,\n\t\tmetadata: metadata,\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) HasPermission(addr std.Address, perm Permission) bool {\n\tif group.creator != addr {\n\t\treturn false\n\t}\n\treturn isValidPermission(perm)\n}\n\nfunc (group *Group) RenderGroup() string {\n\tstr := \"Group ID: \" + groupIDKey(group.id) + \"\\n\\n\" +\n\t\t\"Group Name: \" + group.name + \"\\n\\n\" +\n\t\t\"Group Creator: \" + usernameOf(group.creator) + \"\\n\\n\" +\n\t\t\"Group createdAt: \" + group.createdAt.String() + \"\\n\\n\" +\n\t\t\"Group Last MemberID: \" + memberIDKey(group.lastMemberID) + \"\\n\\n\"\n\n\tstr += \"Group Members: \\n\\n\"\n\tgroup.members.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tmember := value.(*Member)\n\t\tstr += member.getMemberStr()\n\t\treturn false\n\t})\n\treturn str\n}\n\nfunc (group *Group) deleteGroup() {\n\tgidkey := groupIDKey(group.id)\n\t_, gGroupsRemoved := gGroups.Remove(gidkey)\n\tif !gGroupsRemoved {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\tgGroupsByName.Remove(group.name)\n}\n\nfunc (group *Group) deleteMember(mid MemberID) {\n\tgidkey := groupIDKey(group.id)\n\tif !gGroups.Has(gidkey) {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\n\tg := getGroup(group.id)\n\tmidkey := memberIDKey(mid)\n\tg.members.Remove(midkey)\n}\n"},{"name":"groups.gno","body":"package groups\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgGroups avl.Tree // id -\u003e *Group\n\tgGroupsCtr int // increments Group.id\n\tgGroupsByName avl.Tree // name -\u003e *Group\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n"},{"name":"member.gno","body":"package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype MemberID uint64\n\ntype Member struct {\n\tid MemberID\n\taddress std.Address\n\tweight int\n\tmetadata string\n\tcreatedAt time.Time\n}\n\nfunc (mid MemberID) String() string {\n\treturn strconv.Itoa(int(mid))\n}\n\nfunc (member *Member) getMemberStr() string {\n\tmemberDataStr := \"\"\n\tmemberDataStr += \"\\t\\t\\t[\" + memberIDKey(member.id) + \", \" + member.address.String() + \", \" + strconv.Itoa(member.weight) + \", \" + member.metadata + \", \" + member.createdAt.String() + \"],\\n\\n\"\n\treturn memberDataStr\n}\n"},{"name":"misc.gno","body":"package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/demo/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getGroup(gid GroupID) *Group {\n\tgidkey := groupIDKey(gid)\n\tgroup_, exists := gGroups.Get(gidkey)\n\tif !exists {\n\t\tpanic(\"group id (\" + gid.String() + \") does not exists\")\n\t}\n\tgroup := group_.(*Group)\n\treturn group\n}\n\nfunc incGetGroupID() GroupID {\n\tgGroupsCtr++\n\treturn GroupID(gGroupsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\" \", length-len(str)) + str\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\"0\", length-len(str)) + str\n}\n\nfunc groupIDKey(gid GroupID) string {\n\treturn padZero(uint64(gid), 10)\n}\n\nfunc memberIDKey(mid MemberID) string {\n\treturn padZero(uint64(mid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/r/demo/users:\" + addr.String() + \")\"\n\t}\n\treturn \"[@\" + user.Name + \"](/r/demo/users:\" + user.Name + \")\"\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\tpanic(\"user not found\")\n\t}\n\treturn user.Name\n}\n\nfunc isValidPermission(perm Permission) bool {\n\treturn perm == EditPermission || perm == DeletePermission\n}\n"},{"name":"public.gno","body":"package groups\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/users\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetGroupIDFromName(name string) (GroupID, bool) {\n\tgroupI, exists := gGroupsByName.Get(name)\n\tif !exists {\n\t\treturn 0, false\n\t}\n\treturn groupI.(*Group).id, true\n}\n\nfunc CreateGroup(name string) GroupID {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tusernameOf(caller)\n\turl := \"/r/demo/groups:\" + name\n\tgroup := newGroup(url, name, caller)\n\tgidkey := groupIDKey(group.id)\n\tgGroups.Set(gidkey, group)\n\tgGroupsByName.Set(name, group)\n\treturn group.id\n}\n\nfunc AddMember(gid GroupID, address string, weight int, metadata string) MemberID {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tusernameOf(caller)\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, EditPermission) {\n\t\tpanic(\"unauthorized to edit group\")\n\t}\n\tuser := users.GetUserByAddress(std.Address(address))\n\tif user == nil {\n\t\tpanic(\"unknown address \" + address)\n\t}\n\tmid := group.lastMemberID\n\tmember := group.newMember(mid, std.Address(address), weight, metadata)\n\tmidkey := memberIDKey(mid)\n\tgroup.members.Set(midkey, member)\n\tmid++\n\tgroup.lastMemberID = mid\n\treturn member.id\n}\n\nfunc DeleteGroup(gid GroupID) {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete group\")\n\t}\n\tgroup.deleteGroup()\n}\n\nfunc DeleteMember(gid GroupID, mid MemberID) {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete member\")\n\t}\n\tgroup.deleteMember(mid)\n}\n"},{"name":"render.gno","body":"package groups\n\nimport (\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderGroup(gid GroupID) string {\n\tgroup := getGroup(gid)\n\tif group == nil {\n\t\treturn \"missing Group\"\n\t}\n\treturn group.RenderGroup()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"List of all Groups:\\n\\n\"\n\t\tgGroups.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tgroup := value.(*Group)\n\t\t\tstr += \" * [\" + group.name + \"](\" + group.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/demo/groups:Group_NAME\n\t\tname := parts[0]\n\t\tgroupI, exists := gGroupsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"Group does not exist: \" + name\n\t\t}\n\t\treturn groupI.(*Group).RenderGroup()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n"},{"name":"role.gno","body":"package groups\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission Permission = \"role:edit\"\n)\n"},{"name":"z_0_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/r/demo/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// user not found\n"},{"name":"z_0_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// payment must not be less than 20000000\n"},{"name":"z_0_c_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n//\n// * [test_group](/r/demo/groups:test_group)\n"},{"name":"z_1_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser0\", \"my profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tusers.Register(caller, \"gnouser1\", \"my other profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\tusers.Invite(test2.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(caller, \"gnouser2\", \"my other profile 2\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\tusers.Invite(test3.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test3)\n\tusers.Register(caller, \"gnouser3\", \"my other profile 3\")\n\n\tstd.TestSetOrigCaller(caller)\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\tgroups.AddMember(gid, test3.String(), 32, \"i am from UAE\")\n\tprintln(groups.Render(\"test_group\"))\n}\n\n// Output:\n// 1\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: gnouser0\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n//\n// \t\t\t[0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001],\n"},{"name":"z_1_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.AddMember(2, \"g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy\", 55, \"metadata3\")\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (2) does not exists\n"},{"name":"z_1_c_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// add member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tgroups.AddMember(gid, test2.String(), 42, \"metadata3\")\n}\n\n// Error:\n// user not found\n"},{"name":"z_2_a_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nconst admin = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc main() {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(\"\", \"gnouser0\", \"my profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\tusers.Invite(test1.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test1)\n\tusers.Register(caller, \"gnouser1\", \"my other profile 1\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\tusers.Invite(test2.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test2)\n\tusers.Register(caller, \"gnouser2\", \"my other profile 2\")\n\n\tstd.TestSetOrigCaller(admin)\n\tusers.GrantInvites(caller.String() + \":1\")\n\t// switch back to caller\n\tstd.TestSetOrigCaller(caller)\n\t// invite another addr\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\tusers.Invite(test3.String())\n\t// switch to test1\n\tstd.TestSetOrigCaller(test3)\n\tusers.Register(caller, \"gnouser3\", \"my other profile 3\")\n\n\tstd.TestSetOrigCaller(caller)\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\tgroups.AddMember(gid, test2.String(), 42, \"metadata3\")\n\n\tgroups.DeleteMember(gid, 0)\n\tprintln(groups.RenderGroup(gid))\n}\n\n// Output:\n// 1\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: gnouser0\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n"},{"name":"z_2_b_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteMember(2, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (2) does not exists\n"},{"name":"z_2_d_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// delete member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tgroups.DeleteMember(gid, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// unauthorized to delete member\n"},{"name":"z_2_e_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteGroup(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n"},{"name":"z_2_f_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteGroup(20)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (20) does not exists\n"},{"name":"z_2_g_filetest.gno","body":"// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"\", \"gnouser\", \"my profile\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// delete group via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\tstd.TestSetOrigCaller(test2)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 9000000}}, nil)\n\n\tgroups.DeleteGroup(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// unauthorized to delete group\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"keystore","path":"gno.land/r/demo/keystore","files":[{"name":"keystore.gno","body":"package keystore\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar data avl.Tree\n\nconst (\n\tBaseURL = \"/r/demo/keystore\"\n\tStatusOK = \"ok\"\n\tStatusNoUser = \"user not found\"\n\tStatusNotFound = \"key not found\"\n\tStatusNoWriteAccess = \"no write access\"\n\tStatusCouldNotExecute = \"could not execute\"\n\tStatusNoDatabases = \"no databases\"\n)\n\nfunc init() {\n\tdata = avl.Tree{} // user -\u003e avl.Tree\n}\n\n// KeyStore stores the owner-specific avl.Tree\ntype KeyStore struct {\n\tOwner std.Address\n\tData avl.Tree\n}\n\n// Set will set a value to a key\n// requires write-access (original caller must be caller)\nfunc Set(k, v string) string {\n\torigOwner := std.GetOrigCaller()\n\treturn set(origOwner.String(), k, v)\n}\n\n// set (private) will set a key to value\n// requires write-access (original caller must be caller)\nfunc set(owner, k, v string) string {\n\torigOwner := std.GetOrigCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData: avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\tkeystore.Data.Set(k, v)\n\treturn StatusOK\n}\n\n// Remove removes a key\n// requires write-access (original owner must be caller)\nfunc Remove(k string) string {\n\torigOwner := std.GetOrigCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// remove (private) removes a key\n// requires write-access (original owner must be caller)\nfunc remove(owner, k string) string {\n\torigOwner := std.GetOrigCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData: avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\t_, removed := keystore.Data.Remove(k)\n\tif !removed {\n\t\treturn StatusCouldNotExecute\n\t}\n\treturn StatusOK\n}\n\n// Get returns a value for a key\n// read-only\nfunc Get(k string) string {\n\torigOwner := std.GetOrigCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// get (private) returns a value for a key\n// read-only\nfunc get(owner, k string) string {\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\tval, found := keystore.Data.Get(k)\n\tif !found {\n\t\treturn StatusNotFound\n\t}\n\treturn val.(string)\n}\n\n// Size returns size of database\n// read-only\nfunc Size() string {\n\torigOwner := std.GetOrigCaller()\n\treturn size(origOwner.String())\n}\n\nfunc size(owner string) string {\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\treturn ufmt.Sprintf(\"%d\", keystore.Data.Size())\n}\n\n// Render provides read-only url access to the functions of the keystore\n// \"\" -\u003e show all keystores listed by owner\n// \"owner\" -\u003e show all keys for that owner's keystore\n// \"owner:size\" -\u003e returns size of owner's keystore\n// \"owner:get:key\" -\u003e show value for that key in owner's keystore\nfunc Render(p string) string {\n\tvar response string\n\targs := strings.Split(p, \":\")\n\tnumArgs := len(args)\n\tif p == \"\" {\n\t\tnumArgs = 0\n\t}\n\tswitch numArgs {\n\tcase 0:\n\t\tif data.Size() == 0 {\n\t\t\treturn StatusNoDatabases\n\t\t}\n\t\tdata.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tks := value.(*KeyStore)\n\t\t\tresponse += ufmt.Sprintf(\"- [%s](%s:%s) (%d keys)\\n\", ks.Owner, BaseURL, ks.Owner, ks.Data.Size())\n\t\t\treturn false\n\t\t})\n\tcase 1:\n\t\towner := args[0]\n\t\tkeystoreInterface, exists := data.Get(owner)\n\t\tif !exists {\n\t\t\treturn StatusNoUser\n\t\t}\n\t\tks := keystoreInterface.(*KeyStore)\n\t\ti := 0\n\t\tresponse += ufmt.Sprintf(\"# %s database\\n\\n\", ks.Owner)\n\t\tks.Data.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tresponse += ufmt.Sprintf(\"- %d [%s](%s:%s:get:%s)\\n\", i, key, BaseURL, ks.Owner, key)\n\t\t\ti++\n\t\t\treturn false\n\t\t})\n\tcase 2:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tif cmd == \"size\" {\n\t\t\treturn size(owner)\n\t\t}\n\tcase 3:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tkey := args[2]\n\t\tif cmd == \"get\" {\n\t\t\treturn get(owner, key)\n\t\t}\n\t}\n\n\treturn response\n}\n"},{"name":"keystore_test.gno","body":"package keystore\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestRender(t *testing.T) {\n\tconst (\n\t\tauthor1 std.Address = testutils.TestAddress(\"author1\")\n\t\tauthor2 std.Address = testutils.TestAddress(\"author2\")\n\t)\n\n\ttt := []struct {\n\t\tcaller std.Address\n\t\towner std.Address\n\t\tps []string\n\t\texp string\n\t}{\n\t\t// can set database if the owner is the caller\n\t\t{author1, author1, []string{\"set\", \"hello\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hello\", \"world\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\t\t// only owner can remove\n\t\t{author1, author1, []string{\"remove\", \"hi\"}, StatusOK},\n\t\t{author1, author1, []string{\"get\", \"hi\"}, StatusNotFound},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t// add back\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\n\t\t// different owner has different database\n\t\t{author2, author2, []string{\"set\", \"hello\", \"universe\"}, StatusOK},\n\t\t// either author can get the other info\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// either author can get the other info\n\t\t{author2, author1, []string{\"get\", \"hello\"}, \"world\"},\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// anyone can view the databases\n\t\t{author1, author2, []string{}, `- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/keystore:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6) (2 keys)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00) (1 keys)`},\n\t\t// anyone can view the keys in a database\n\t\t{author1, author2, []string{\"\"}, `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00 database\n\n- 0 [hello](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00:get:hello)`},\n\t}\n\tfor _, tc := range tt {\n\t\tp := \"\"\n\t\tif len(tc.ps) \u003e 0 {\n\t\t\tp = tc.owner.String()\n\t\t\tfor i, psv := range tc.ps {\n\t\t\t\tp += \":\" + psv\n\t\t\t}\n\t\t}\n\t\tp = strings.TrimSuffix(p, \":\")\n\t\tt.Run(p, func(t *testing.T) {\n\t\t\tstd.TestSetOrigCaller(tc.caller)\n\t\t\tvar act string\n\t\t\tif len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"set\" {\n\t\t\t\tact = strings.TrimSpace(Set(tc.ps[1], tc.ps[2]))\n\t\t\t} else if len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"remove\" {\n\t\t\t\tact = strings.TrimSpace(Remove(tc.ps[1]))\n\t\t\t} else {\n\t\t\t\tact = strings.TrimSpace(Render(p))\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tc.exp, act, ufmt.Sprintf(\"%v -\u003e '%s'\", tc.ps, p))\n\t\t})\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"markdown","path":"gno.land/r/demo/markdown_test","files":[{"name":"markdown.gno","body":"package markdown\n\n// this package can be used to test markdown rendering engines.\n\nfunc Render(path string) string {\n\toutput := `_imported from https://github.com/markedjs/marked/blob/master/docs/demo/quickref.md_\n\nMarkdown Quick Reference\n========================\n\nThis guide is a very brief overview, with examples, of the syntax that [Markdown] supports. It is itself written in Markdown and you can copy the samples over to the left-hand pane for experimentation. It's shown as *text* and not *rendered HTML*.\n\n[Markdown]: http://daringfireball.net/projects/markdown/\n\n\nSimple Text Formatting\n======================\n\nFirst thing is first. You can use *stars* or _underscores_ for italics. **Double stars** and __double underscores__ for bold. ***Three together*** for ___both___.\n\nParagraphs are pretty easy too. Just have a blank line between chunks of text.\n\n\u003e This chunk of text is in a block quote. Its multiple lines will all be\n\u003e indented a bit from the rest of the text.\n\u003e\n\u003e \u003e Multiple levels of block quotes also work.\n\nSometimes you want to include code, such as when you are explaining how ` + \"`\u003ch1\u003e`\" + ` HTML tags work, or maybe you are a programmer and you are discussing ` + \"`someMethod()`\" + `.\n\nIf you want to include code and have new\nlines preserved, indent the line with a tab\nor at least four spaces:\n\n Extra spaces work here too.\n This is also called preformatted text and it is useful for showing examples.\n The text will stay as text, so any *markdown* or \u003cu\u003eHTML\u003c/u\u003e you add will\n not show up formatted. This way you can show markdown examples in a\n markdown document.\n\n\u003e You can also use preformatted text with your blockquotes\n\u003e as long as you add at least five spaces.\n\n\nHeadings\n========\n\nThere are a couple of ways to make headings. Using three or more equals signs on a line under a heading makes it into an \"h1\" style. Three or more hyphens under a line makes it \"h2\" (slightly smaller). You can also use multiple pound symbols (` + \"`#`\" + `) before and after a heading. Pounds after the title are ignored. Here are some examples:\n\nThis is H1\n==========\n\nThis is H2\n----------\n\n# This is H1\n## This is H2\n### This is H3 with some extra pounds ###\n#### You get the idea ####\n##### I don't need extra pounds at the end\n###### H6 is the max\n\n\nLinks\n=====\n\nLet's link to a few sites. First, let's use the bare URL, like \u003chttps://www.github.com\u003e. Great for text, but ugly for HTML.\nNext is an inline link to [Google](https://www.google.com). A little nicer.\nThis is a reference-style link to [Wikipedia] [1].\nLastly, here's a pretty link to [Yahoo]. The reference-style and pretty links both automatically use the links defined below, but they could be defined *anywhere* in the markdown and are removed from the HTML. The names are also case insensitive, so you can use [YaHoO] and have it link properly.\n\n[1]: https://www.wikipedia.org\n[Yahoo]: https://www.yahoo.com\n\nTitle attributes may be added to links by adding text after a link.\nThis is the [inline link](https://www.bing.com \"Bing\") with a \"Bing\" title.\nYou can also go to [W3C] [2] and maybe visit a [friend].\n\n[2]: https://w3c.org (The W3C puts out specs for web-based things)\n[Friend]: https://facebook.com \"Facebook!\"\n\nEmail addresses in plain text are not linked: test@example.com.\nEmail addresses wrapped in angle brackets are linked: \u003ctest@example.com\u003e.\nThey are also obfuscated so that email harvesting spam robots hopefully won't get them.\n\n\nLists\n=====\n\n* This is a bulleted list\n* Great for shopping lists\n- You can also use hyphens\n+ Or plus symbols\n\nThe above is an \"unordered\" list. Now, on for a bit of order.\n\n1. Numbered lists are also easy\n2. Just start with a number\n3738762. However, the actual number doesn't matter when converted to HTML.\n1. This will still show up as 4.\n\nYou might want a few advanced lists:\n\n- This top-level list is wrapped in paragraph tags\n- This generates an extra space between each top-level item.\n\n- You do it by adding a blank line\n\n- This nested list also has blank lines between the list items.\n\n- How to create nested lists\n 1. Start your regular list\n 2. Indent nested lists with two spaces\n 3. Further nesting means you should indent with two more spaces\n * This line is indented with four spaces.\n\n- List items can be quite lengthy. You can keep typing and either continue\nthem on the next line with no indentation.\n\n- Alternately, if that looks ugly, you can also\n indent the next line a bit for a prettier look.\n\n- You can put large blocks of text in your list by just indenting with two spaces.\n\n This is formatted the same as code, but you can inspect the HTML\n and find that it's just wrapped in a ` + \"`\u003cp\u003e`\" + ` tag and *won't* be shown\n as preformatted text.\n\n You can keep adding more and more paragraphs to a single\n list item by adding the traditional blank line and then keep\n on indenting the paragraphs with two spaces.\n\n You really only need to indent the first line,\nbut that looks ugly.\n\n- Lists support blockquotes\n\n \u003e Just like this example here. By the way, you can\n \u003e nest lists inside blockquotes!\n \u003e - Fantastic!\n\n- Lists support preformatted text\n\n You just need to indent an additional four spaces.\n\n\nEven More\n=========\n\nHorizontal Rule\n---------------\n\nIf you need a horizontal rule you just need to put at least three hyphens, asterisks, or underscores on a line by themselves. You can also even put spaces between the characters.\n\n---\n****************************\n_ _ _ _ _ _ _\n\nThose three all produced horizontal lines. Keep in mind that three hyphens under any text turns that text into a heading, so add a blank like if you use hyphens.\n\nImages\n------\n\nImages work exactly like links, but they have exclamation points in front. They work with references and titles too.\n\n![Google Logo](https://www.google.com/images/errors/logo_sm.gif) and ![Happy].\n\n[Happy]: https://wpclipart.com/smiley/happy/simple_colors/smiley_face_simple_green_small.png (\"Smiley face\")\n\n\nInline HTML\n-----------\n\nIf markdown is too limiting, you can just insert your own \u003cstrike\u003ecrazy\u003c/strike\u003e HTML. Span-level HTML \u003cu\u003ecan *still* use markdown\u003c/u\u003e. Block level elements must be separated from text by a blank line and must not have any spaces before the opening and closing HTML.\n\n\u003cdiv style='font-family: \"Comic Sans MS\", \"Comic Sans\", cursive;'\u003e\nIt is a pity, but markdown does **not** work in here for most markdown parsers.\n[Marked] handles it pretty well.\n\u003c/div\u003e`\n\treturn output\n}\n"},{"name":"markdown_test.gno","body":"package markdown\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\tif !strings.Contains(output, \"\\nMarkdown Quick Reference\\n\") {\n\t\tt.Errorf(\"invalid output\")\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"eval","path":"gno.land/r/demo/math_eval","files":[{"name":"math_eval.gno","body":"// eval realm is capable of evaluating 32-bit integer\n// expressions as they would appear in Go. For example:\n// /r/demo/math_eval:(4+12)/2-1+11*15\npackage eval\n\nimport (\n\tevalint32 \"gno.land/p/demo/math_eval/int32\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Render(p string) string {\n\tif len(p) == 0 {\n\t\treturn `\nevaluates 32-bit integer expressions. for example:\n\t\t\n[(4+12)/2-1+11*15](/r/demo/math_eval:(4+12)/2-1+11*15)\n\n`\n\t}\n\texpr, err := evalint32.Parse(p)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\tres, err := evalint32.Eval(expr, nil)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn ufmt.Sprintf(\"%s = %d\", p, res)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"memeland","path":"gno.land/r/demo/memeland","files":[{"name":"memeland.gno","body":"package memeland\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/memeland\"\n)\n\nvar m *memeland.Memeland\n\nfunc init() {\n\tm = memeland.NewMemeland()\n\tm.TransferOwnership(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n}\n\nfunc PostMeme(data string, timestamp int64) string {\n\treturn m.PostMeme(data, timestamp)\n}\n\nfunc Upvote(id string) string {\n\treturn m.Upvote(id)\n}\n\nfunc GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\treturn m.GetPostsInRange(startTimestamp, endTimestamp, page, pageSize, sortBy)\n}\n\nfunc RemovePost(id string) string {\n\treturn m.RemovePost(id)\n}\n\nfunc GetOwner() std.Address {\n\treturn m.Owner()\n}\n\nfunc TransferOwnership(newOwner std.Address) {\n\tif err := m.TransferOwnership(newOwner); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tnumOfMemes := int(m.MemeCounter)\n\tif numOfMemes == 0 {\n\t\treturn \"No memes posted yet! :/\"\n\t}\n\n\t// Default render is get Posts since year 2000 to now\n\treturn m.GetPostsInRange(0, time.Now().Unix(), 1, 10, \"DATE_CREATED\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"microblog","path":"gno.land/r/demo/microblog","files":[{"name":"README.md","body":"# microblog realm\n\n## Getting started:\n\n(One-time) Add the microblog package:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/p/demo/microblog\" --pkgdir \"examples/gno.land/p/demo/microblog\" \\\n --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\n(One-time) Add the microblog realm:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/r/demo/microblog\" --pkgdir \"examples/gno.land/r/demo/microblog\" \\\n --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\nAdd a microblog post:\n\n```\ngnokey maketx call --pkgpath \"gno.land/r/demo/microblog\" --func \"NewPost\" --args \"hello, world\" \\\n --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```"},{"name":"microblog.gno","body":"// Microblog is a website with shortform posts from users.\n// The API is simple - \"AddPost\" takes markdown and\n// adds it to the users site.\n// The microblog location is determined by the user address\n// /r/demo/microblog:\u003cYOUR-ADDRESS\u003e\npackage microblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/microblog\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\ttitle = \"gno-based microblog\"\n\tprefix = \"/r/demo/microblog:\"\n\tm *microblog.Microblog\n)\n\nfunc init() {\n\tm = microblog.NewMicroblog(title, prefix)\n}\n\nfunc renderHome() string {\n\toutput := ufmt.Sprintf(\"# %s\\n\\n\", m.Title)\n\toutput += \"# pages\\n\\n\"\n\n\tfor _, page := range m.GetPages() {\n\t\tif u := users.GetUserByAddress(page.Author); u != nil {\n\t\t\toutput += ufmt.Sprintf(\"- [%s (%s)](%s%s)\\n\", u.Name, page.Author.String(), m.Prefix, page.Author.String())\n\t\t} else {\n\t\t\toutput += ufmt.Sprintf(\"- [%s](%s%s)\\n\", page.Author.String(), m.Prefix, page.Author.String())\n\t\t}\n\t}\n\n\treturn output\n}\n\nfunc renderUser(user string) string {\n\tsilo, found := m.Pages.Get(user)\n\tif !found {\n\t\treturn \"404\" // StatusNotFound\n\t}\n\n\treturn PageToString((silo.(*microblog.Page)))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\n\tisHome := path == \"\"\n\tisUser := len(parts) == 1\n\n\tswitch {\n\tcase isHome:\n\t\treturn renderHome()\n\n\tcase isUser:\n\t\treturn renderUser(parts[0])\n\t}\n\n\treturn \"404\" // StatusNotFound\n}\n\nfunc PageToString(p *microblog.Page) string {\n\to := \"\"\n\tif u := users.GetUserByAddress(p.Author); u != nil {\n\t\to += ufmt.Sprintf(\"# [%s](/r/demo/users:%s)\\n\\n\", u, u)\n\t\to += ufmt.Sprintf(\"%s\\n\\n\", u.Profile)\n\t}\n\to += ufmt.Sprintf(\"## [%s](/r/demo/microblog:%s)\\n\\n\", p.Author, p.Author)\n\n\to += ufmt.Sprintf(\"joined %s, last updated %s\\n\\n\", p.CreatedAt.Format(\"2006-02-01\"), p.LastPosted.Format(\"2006-02-01\"))\n\to += \"## feed\\n\\n\"\n\tfor _, u := range p.GetPosts() {\n\t\to += u.String() + \"\\n\\n\"\n\t}\n\treturn o\n}\n\n// NewPost takes a single argument (post markdown) and\n// adds a post to the address of the caller.\nfunc NewPost(text string) string {\n\tif err := m.NewPost(text); err != nil {\n\t\treturn \"unable to add new post\"\n\t}\n\treturn \"added new post\"\n}\n\nfunc Register(name, profile string) string {\n\tcaller := std.GetOrigCaller() // main\n\tusers.Register(caller, name, profile)\n\treturn \"OK\"\n}\n"},{"name":"microblog_test.gno","body":"package microblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestMicroblog(t *testing.T) {\n\tconst (\n\t\tauthor1 std.Address = testutils.TestAddress(\"author1\")\n\t\tauthor2 std.Address = testutils.TestAddress(\"author2\")\n\t)\n\n\tstd.TestSetOrigCaller(author1)\n\n\turequire.Equal(t, \"404\", Render(\"/wrongpath\"), \"rendering not giving 404\")\n\turequire.NotEqual(t, \"404\", Render(\"\"), \"rendering / should not give 404\")\n\turequire.NoError(t, m.NewPost(\"goodbyte, web2\"), \"could not create post\")\n\n\t_, err := m.GetPage(author1.String())\n\turequire.NoError(t, err, \"silo should exist\")\n\n\t_, err = m.GetPage(\"no such author\")\n\turequire.Error(t, err, \"silo should not exist\")\n\n\tstd.TestSetOrigCaller(author2)\n\n\turequire.NoError(t, m.NewPost(\"hello, web3\"), \"could not create post\")\n\turequire.NoError(t, m.NewPost(\"hello again, web3\"), \"could not create post\")\n\turequire.NoError(t, m.NewPost(\"hi again,\\n web4?\"), \"could not create post\")\n\n\tprintln(\"--- MICROBLOG ---\\n\\n\")\n\n\texpected := `# gno-based microblog\n\n# pages\n\n- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n`\n\turequire.Equal(t, expected, Render(\"\"), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e goodbyte, web2\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author1.String())), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e hi again,\n\u003e\n\u003e web4?\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello again, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author2.String())), \"incorrect rendering\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"nft","path":"gno.land/r/demo/nft","files":[{"name":"README.md","body":"NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n- [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n- [gno.land/r/demo/nft/nft.go](https://gno.land/r/demo/nft/nft.go)\n- [zrealm_nft3.go test](https://github.com/gnolang/gno/blob/master/tests/files2/zrealm_nft3.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:gnolang/1)).\n"},{"name":"nft.gno","body":"package nft\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc721\"\n)\n\ntype token struct {\n\tgrc721.IGRC721 // implements the GRC721 interface\n\n\ttokenCounter int\n\ttokens avl.Tree // grc721.TokenID -\u003e *NFToken{}\n\toperators avl.Tree // owner std.Address -\u003e operator std.Address\n}\n\ntype NFToken struct {\n\towner std.Address\n\tapproved std.Address\n\ttokenID grc721.TokenID\n\tdata string\n}\n\nvar gToken = \u0026token{}\n\nfunc GetToken() *token { return gToken }\n\nfunc (grc *token) nextTokenID() grc721.TokenID {\n\tgrc.tokenCounter++\n\ts := strconv.Itoa(grc.tokenCounter)\n\treturn grc721.TokenID(s)\n}\n\nfunc (grc *token) getToken(tid grc721.TokenID) (*NFToken, bool) {\n\ttoken, ok := grc.tokens.Get(string(tid))\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn token.(*NFToken), true\n}\n\nfunc (grc *token) Mint(to std.Address, data string) grc721.TokenID {\n\ttid := grc.nextTokenID()\n\tgrc.tokens.Set(string(tid), \u0026NFToken{\n\t\towner: to,\n\t\ttokenID: tid,\n\t\tdata: data,\n\t})\n\treturn tid\n}\n\nfunc (grc *token) BalanceOf(owner std.Address) (count int64) {\n\tpanic(\"not yet implemented\")\n}\n\nfunc (grc *token) OwnerOf(tid grc721.TokenID) std.Address {\n\ttoken, ok := grc.getToken(tid)\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\treturn token.owner\n}\n\n// XXX not fully implemented yet.\nfunc (grc *token) SafeTransferFrom(from, to std.Address, tid grc721.TokenID) {\n\tgrc.TransferFrom(from, to, tid)\n\t// When transfer is complete, this function checks if `_to` is a smart\n\t// contract (code size \u003e 0). If so, it calls `onERC721Received` on\n\t// `_to` and throws if the return value is not\n\t// `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`.\n\t// XXX ensure \"to\" is a realm with onERC721Received() signature.\n}\n\nfunc (grc *token) TransferFrom(from, to std.Address, tid grc721.TokenID) {\n\tcaller := std.GetCallerAt(2)\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\t// Throws unless `msg.sender` is the current owner, an authorized\n\t// operator, or the approved address for this NFT.\n\tif caller != token.owner \u0026\u0026 caller != token.approved {\n\t\toperator, ok := grc.operators.Get(token.owner.String())\n\t\tif !ok || caller != operator.(std.Address) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t}\n\t// Throws if `_from` is not the current owner.\n\tif from != token.owner {\n\t\tpanic(\"from is not the current owner\")\n\t}\n\t// Throws if `_to` is the zero address.\n\tif to == \"\" {\n\t\tpanic(\"to cannot be empty\")\n\t}\n\t// Good.\n\ttoken.owner = to\n}\n\nfunc (grc *token) Approve(approved std.Address, tid grc721.TokenID) {\n\tcaller := std.GetCallerAt(2)\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\t// Throws unless `msg.sender` is the current owner,\n\t// or an authorized operator.\n\tif caller != token.owner {\n\t\toperator, ok := grc.operators.Get(token.owner.String())\n\t\tif !ok || caller != operator.(std.Address) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t}\n\t// Good.\n\ttoken.approved = approved\n}\n\n// XXX make it work for set of operators.\nfunc (grc *token) SetApprovalForAll(operator std.Address, approved bool) {\n\tcaller := std.GetCallerAt(2)\n\tgrc.operators.Set(caller.String(), operator)\n}\n\nfunc (grc *token) GetApproved(tid grc721.TokenID) std.Address {\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\treturn token.approved\n}\n\n// XXX make it work for set of operators\nfunc (grc *token) IsApprovedForAll(owner, operator std.Address) bool {\n\toperator2, ok := grc.operators.Get(owner.String())\n\tif !ok {\n\t\treturn false\n\t}\n\treturn operator == operator2.(std.Address)\n}\n"},{"name":"z_0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\taddr1 := testutils.TestAddress(\"addr1\")\n\t// addr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(addr1, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n}\n\n// Output:\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n\n// Realm:\n// switchrealm[\"gno.land/r/demo/nft\"]\n// switchrealm[\"gno.land/r/demo/nft\"]\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:11]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/grc/grc721.TokenID\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"NFT#1\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:10]={\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/nft.NFToken\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"564a9e78be869bd258fc3c9ad56f5a75ed68818f\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:11\"\n// }\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:9]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/nft.NFToken\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b53ffc464e1b5655d19b9d5277f3491717c24aca\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:8]={\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b1d928b3716b147c92730e8d234162bec2f0f2fc\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\"\n// }\n// }\n// }\n// u[67c479d3d51d4056b2f4111d5352912a00be311e:5]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b229b824842ec3e7f2341e33d0fa0ca77af2f480\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\",\n// \"ModTime\": \"7\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:4\",\n// \"RefCount\": \"1\"\n// }\n// }\n// u[67c479d3d51d4056b2f4111d5352912a00be311e:4]={\n// \"Fields\": [\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"1e0b9dddb406b4f50500a022266a4cb8a4ea38c6\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"05ab6746ea84b55ca133806af215d99a1c4b045e\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:6\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:4\",\n// \"ModTime\": \"7\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:3\",\n// \"RefCount\": \"1\"\n// }\n// }\n// switchrealm[\"gno.land/r/demo/nft\"]\n// switchrealm[\"gno.land/r/demo/nft_test\"]\n"},{"name":"z_1_filetest.gno","body":"// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(addr1, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Error:\n// unauthorized\n"},{"name":"z_2_filetest.gno","body":"// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.GetCallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\t// addr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.TransferFrom(caller, addr1, tid)\n}\n\n// Output:\n// g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n"},{"name":"z_3_filetest.gno","body":"// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.GetCallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.Approve(caller, tid) // approve self.\n\tgrc721.TransferFrom(caller, addr1, tid)\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Output:\n// g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n"},{"name":"z_4_filetest.gno","body":"// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.GetCallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.Approve(caller, tid) // approve self.\n\tgrc721.TransferFrom(caller, addr1, tid)\n\tgrc721.Approve(\"\", tid) // approve addr1.\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Error:\n// unauthorized\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"profile","path":"gno.land/r/demo/profile","files":[{"name":"profile.gno","body":"package profile\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tfields = avl.NewTree()\n\trouter = mux.NewRouter()\n)\n\n// Standard fields\nconst (\n\tDisplayName = \"DisplayName\"\n\tHomepage = \"Homepage\"\n\tBio = \"Bio\"\n\tAge = \"Age\"\n\tLocation = \"Location\"\n\tAvatar = \"Avatar\"\n\tGravatarEmail = \"GravatarEmail\"\n\tAvailableForHiring = \"AvailableForHiring\"\n\tInvalidField = \"InvalidField\"\n)\n\n// Events\nconst (\n\tProfileFieldCreated = \"ProfileFieldCreated\"\n\tProfileFieldUpdated = \"ProfileFieldUpdated\"\n)\n\n// Field types used when emitting event\nconst FieldType = \"FieldType\"\n\nconst (\n\tBoolField = \"BoolField\"\n\tStringField = \"StringField\"\n\tIntField = \"IntField\"\n)\n\nfunc init() {\n\trouter.HandleFunc(\"\", homeHandler)\n\trouter.HandleFunc(\"u/{addr}\", profileHandler)\n\trouter.HandleFunc(\"f/{addr}/{field}\", fieldHandler)\n}\n\n// List of supported string fields\nvar stringFields = map[string]bool{\n\tDisplayName: true,\n\tHomepage: true,\n\tBio: true,\n\tLocation: true,\n\tAvatar: true,\n\tGravatarEmail: true,\n}\n\n// List of support int fields\nvar intFields = map[string]bool{\n\tAge: true,\n}\n\n// List of support bool fields\nvar boolFields = map[string]bool{\n\tAvailableForHiring: true,\n}\n\n// Setters\n\nfunc SetStringField(field, value string) bool {\n\taddr := std.PrevRealm().Addr()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, StringField, field, value)\n\n\treturn updated\n}\n\nfunc SetIntField(field string, value int) bool {\n\taddr := std.PrevRealm().Addr()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, IntField, field, string(value))\n\n\treturn updated\n}\n\nfunc SetBoolField(field string, value bool) bool {\n\taddr := std.PrevRealm().Addr()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, BoolField, field, ufmt.Sprintf(\"%t\", value))\n\n\treturn updated\n}\n\n// Getters\n\nfunc GetStringField(addr std.Address, field, def string) string {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn def\n}\n\nfunc GetBoolField(addr std.Address, field string, def bool) bool {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(bool)\n\t}\n\n\treturn def\n}\n\nfunc GetIntField(addr std.Address, field string, def int) int {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(int)\n\t}\n\n\treturn def\n}\n"},{"name":"profile_test.gno","body":"package profile\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\n// Global addresses for test users\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n\tdave = testutils.TestAddress(\"dave\")\n\teve = testutils.TestAddress(\"eve\")\n\tfrank = testutils.TestAddress(\"frank\")\n\tuser1 = testutils.TestAddress(\"user1\")\n\tuser2 = testutils.TestAddress(\"user2\")\n)\n\nfunc TestStringFields(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(alice))\n\n\t// Get before setting\n\tname := GetStringField(alice, DisplayName, \"anon\")\n\tuassert.Equal(t, \"anon\", name)\n\n\t// Set new key\n\tupdated := SetStringField(DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, false)\n\tupdated = SetStringField(Homepage, \"https://example.com\")\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetStringField(DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tname = GetStringField(alice, DisplayName, \"anon\")\n\thomepage := GetStringField(alice, Homepage, \"\")\n\tbio := GetStringField(alice, Bio, \"42\")\n\n\tuassert.Equal(t, \"Alice foo\", name)\n\tuassert.Equal(t, \"https://example.com\", homepage)\n\tuassert.Equal(t, \"42\", bio)\n}\n\nfunc TestIntFields(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(bob))\n\n\t// Get before setting\n\tage := GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 25, age)\n\n\t// Set new key\n\tupdated := SetIntField(Age, 30)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetIntField(Age, 30)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tage = GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 30, age)\n}\n\nfunc TestBoolFields(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(charlie))\n\n\t// Get before setting\n\thiring := GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, false, hiring)\n\n\t// Set\n\tupdated := SetBoolField(AvailableForHiring, true)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetBoolField(AvailableForHiring, true)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\thiring = GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, true, hiring)\n}\n\nfunc TestMultipleProfiles(t *testing.T) {\n\t// Set profile for user1\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\tupdated := SetStringField(DisplayName, \"User One\")\n\tuassert.Equal(t, updated, false)\n\n\t// Set profile for user2\n\tstd.TestSetRealm(std.NewUserRealm(user2))\n\tupdated = SetStringField(DisplayName, \"User Two\")\n\tuassert.Equal(t, updated, false)\n\n\t// Get profiles\n\tstd.TestSetRealm(std.NewUserRealm(user1)) // Switch back to user1\n\tname1 := GetStringField(user1, DisplayName, \"anon\")\n\tstd.TestSetRealm(std.NewUserRealm(user2)) // Switch back to user2\n\tname2 := GetStringField(user2, DisplayName, \"anon\")\n\n\tuassert.Equal(t, \"User One\", name1)\n\tuassert.Equal(t, \"User Two\", name2)\n}\n\nfunc TestArbitraryStringField(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary string field\n\tupdated := SetStringField(\"MyEmail\", \"my@email.com\")\n\tuassert.Equal(t, updated, false)\n\n\tval := GetStringField(user1, \"MyEmail\", \"\")\n\tuassert.Equal(t, val, \"my@email.com\")\n}\n\nfunc TestArbitraryIntField(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetIntField(\"MyIncome\", 100_000)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetIntField(user1, \"MyIncome\", 0)\n\tuassert.Equal(t, val, 100_000)\n}\n\nfunc TestArbitraryBoolField(t *testing.T) {\n\tstd.TestSetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetBoolField(\"IsWinner\", true)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetBoolField(user1, \"IsWinner\", false)\n\tuassert.Equal(t, val, true)\n}\n"},{"name":"render.gno","body":"package profile\n\nimport (\n\t\"bytes\"\n\t\"net/url\"\n\t\"std\"\n\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tBaseURL = \"/r/demo/profile\"\n\tSetStringFieldURL = BaseURL + \"$help\u0026func=SetStringField\u0026field=%s\"\n\tSetIntFieldURL = BaseURL + \"$help\u0026func=SetIntField\u0026field=%s\"\n\tSetBoolFieldURL = BaseURL + \"$help\u0026func=SetBoolField\u0026field=%s\"\n\tViewAllFieldsURL = BaseURL + \":u/%s\"\n\tViewFieldURL = BaseURL + \":f/%s/%s\"\n)\n\nfunc homeHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"## Setters\\n\")\n\tfor field := range stringFields {\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range intFields {\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s Field](%s)\\n\", field, link))\n\t}\n\n\tb.WriteString(\"\\n---\\n\\n\")\n\n\tres.Write(b.String())\n}\n\nfunc profileHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Profile %s\\n\", addr))\n\n\taddress := std.Address(addr)\n\n\tfor field := range stringFields {\n\t\tvalue := GetStringField(address, field, \"n/a\")\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range intFields {\n\t\tvalue := GetIntField(address, field, 0)\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %d [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tvalue := GetBoolField(address, field, false)\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %t [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tres.Write(b.String())\n}\n\nfunc fieldHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\tfield := req.GetVar(\"field\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Field %s for %s\\n\", field, addr))\n\n\taddress := std.Address(addr)\n\tvalue := \"n/a\"\n\tvar editLink string\n\n\tif _, ok := stringFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%s\", GetStringField(address, field, \"n/a\"))\n\t\teditLink = ufmt.Sprintf(SetStringFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, url.QueryEscape(value))\n\t} else if _, ok := intFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%d\", GetIntField(address, field, 0))\n\t\teditLink = ufmt.Sprintf(SetIntFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t} else if _, ok := boolFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%t\", GetBoolField(address, field, false))\n\t\teditLink = ufmt.Sprintf(SetBoolFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t}\n\n\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, editLink))\n\n\tres.Write(b.String())\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"releases_example","path":"gno.land/r/demo/releases_example","files":[{"name":"dummy.gno","body":"package releases_example\n\nfunc init() {\n\t// dummy example data\n\tchangelog.NewRelease(\n\t\t\"v1\",\n\t\t\"r/demo/examples_example_v1\",\n\t\t\"initial release\",\n\t)\n\tchangelog.NewRelease(\n\t\t\"v2\",\n\t\t\"r/demo/examples_example_v2\",\n\t\t\"various improvements\",\n\t)\n}\n"},{"name":"example.gno","body":"// this package demonstrates a way to manage contract releases.\npackage releases_example\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/releases\"\n)\n\nvar (\n\tchangelog = releases.NewChangelog(\"example_app\")\n\tadmin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // @administrator\n)\n\nfunc init() {\n\t// FIXME: admin = std.GetCreator()\n}\n\nfunc NewRelease(name, url, notes string) {\n\tcaller := std.GetOrigCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tchangelog.NewRelease(name, url, notes)\n}\n\nfunc UpdateAdmin(address std.Address) {\n\tcaller := std.GetOrigCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tadmin = address\n}\n\nfunc Render(path string) string {\n\treturn changelog.Render(path)\n}\n"},{"name":"releases0_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/p/demo/releases\"\n)\n\nfunc main() {\n\tprintln(\"-----------\")\n\tchangelog := releases.NewChangelog(\"example\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v1\", \"r/blahblah\", \"* initial version\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v2\", \"r/blahblah2\", \"* various improvements\\n* new shiny logo\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(changelog.Latest().Render())\n}\n\n// Output:\n// -----------\n// # example\n//\n// -----------\n// # example\n//\n// ## [example v1 (latest)](r/blahblah)\n//\n// * initial version\n//\n// -----------\n// # example\n//\n// ## [example v2 (latest)](r/blahblah2)\n//\n// * various improvements\n// * new shiny logo\n//\n// ## [example v1](r/blahblah)\n//\n// * initial version\n//\n// -----------\n// ## [example v2 (latest)](r/blahblah2)\n//\n// * various improvements\n// * new shiny logo\n"},{"name":"releases1_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/r/demo/releases_example\"\n)\n\nfunc main() {\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"v1\"))\n\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"v42\"))\n}\n\n// Output:\n// -----------\n// # example_app\n//\n// ## [example_app v2 (latest)](r/demo/examples_example_v2)\n//\n// various improvements\n//\n// ## [example_app v1](r/demo/examples_example_v1)\n//\n// initial release\n//\n// -----------\n// ## [example_app v1](r/demo/examples_example_v1)\n//\n// initial release\n//\n// -----------\n// no such release\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tamagotchi","path":"gno.land/r/demo/tamagotchi","files":[{"name":"realm.gno","body":"package tamagotchi\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar t *tamagotchi.Tamagotchi\n\nfunc init() {\n\tReset(\"gnome#0\")\n}\n\nfunc Reset(optionalName string) string {\n\tname := optionalName\n\tif name == \"\" {\n\t\theight := std.GetHeight()\n\t\tname = ufmt.Sprintf(\"gnome#%d\", height)\n\t}\n\n\tt = tamagotchi.New(name)\n\n\treturn ufmt.Sprintf(\"A new tamagotchi is born. Their name is %s %s.\", t.Name(), t.Face())\n}\n\nfunc Feed() string {\n\tt.Feed()\n\treturn t.Markdown()\n}\n\nfunc Play() string {\n\tt.Play()\n\treturn t.Markdown()\n}\n\nfunc Heal() string {\n\tt.Heal()\n\treturn t.Markdown()\n}\n\nfunc Render(path string) string {\n\ttama := t.Markdown()\n\tlinks := `Actions:\n* [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n* [Play](/r/demo/tamagotchi$help\u0026func=Play)\n* [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n* [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n`\n\n\treturn tama + \"\\n\\n\" + links\n}\n"},{"name":"z0_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/r/demo/tamagotchi\"\n)\n\nfunc main() {\n\ttamagotchi.Reset(\"tamagnotchi\")\n\tprintln(tamagotchi.Render(\"\"))\n}\n\n// Output:\n// # tamagnotchi 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// Actions:\n// * [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n// * [Play](/r/demo/tamagotchi$help\u0026func=Play)\n// * [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n// * [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"crossrealm","path":"gno.land/r/demo/tests/crossrealm","files":[{"name":"crossrealm.gno","body":"package crossrealm\n\nimport (\n\t\"gno.land/p/demo/tests/p_crossrealm\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype LocalStruct struct {\n\tA int\n}\n\nfunc (ls *LocalStruct) String() string {\n\treturn ufmt.Sprintf(\"LocalStruct{%d}\", ls.A)\n}\n\n// local is saved locally in this realm\nvar local *LocalStruct\n\nfunc init() {\n\tlocal = \u0026LocalStruct{A: 123}\n}\n\n// Make1 returns a local object wrapped by a p struct\nfunc Make1() *p_crossrealm.Container {\n\treturn \u0026p_crossrealm.Container{\n\t\tA: 1,\n\t\tB: local,\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tests_foo","path":"gno.land/r/demo/tests_foo","files":[{"name":"foo.gno","body":"package tests_foo\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\n// for testing gno.land/r/demo/tests/interfaces.go\n\ntype FooStringer struct {\n\tFieldA string\n}\n\nfunc (fs *FooStringer) String() string {\n\treturn \"\u0026FooStringer{\" + fs.FieldA + \"}\"\n}\n\nfunc AddFooStringer(fa string) {\n\ttests.AddStringer(\u0026FooStringer{fa})\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"todolistrealm","path":"gno.land/r/demo/todolist","files":[{"name":"todolist.gno","body":"package todolistrealm\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// State variables\nvar (\n\ttodolistTree *avl.Tree\n\ttlid seqid.ID\n)\n\n// Constructor\nfunc init() {\n\ttodolistTree = avl.NewTree()\n}\n\nfunc NewTodoList(title string) (int, string) {\n\t// Create new Todolist\n\ttl := todolist.NewTodoList(title)\n\t// Update AVL tree with new state\n\ttlid.Next()\n\ttodolistTree.Set(strconv.Itoa(int(tlid)), tl)\n\treturn int(tlid), \"created successfully\"\n}\n\nfunc AddTask(todolistID int, title string) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// get the number of tasks in the todolist\n\tid := tl.(*todolist.TodoList).Tasks.Size()\n\n\t// create the task\n\ttask := todolist.NewTask(title)\n\n\t// Cast raw data from tree into Todolist struct\n\ttl.(*todolist.TodoList).AddTask(id, task)\n\n\treturn \"task added successfully\"\n}\n\nfunc ToggleTaskStatus(todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\ttask, found := tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif !found {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttodolist.ToggleTaskStatus(task.(*todolist.Task))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTask(todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\t_, ok = tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif !ok {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttl.(*todolist.TodoList).RemoveTask(strconv.Itoa(taskID))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTodoList(todolistID int) string {\n\t// Get Todolist from AVL tree\n\t_, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Remove the todolist\n\ttodolistTree.Remove(strconv.Itoa(todolistID))\n\n\treturn \"Todolist removed successfully\"\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderHomepage()\n\t}\n\n\treturn \"unknown page\"\n}\n\nfunc renderHomepage() string {\n\t// Define empty buffer\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"# Welcome to ToDolist\\n\\n\")\n\n\t// If no todolists have been created\n\tif todolistTree.Size() == 0 {\n\t\tb.WriteString(\"### No todolists available currently!\")\n\t\treturn b.String()\n\t}\n\n\t// Iterate through AVL tree\n\ttodolistTree.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t// cast raw data from tree into Todolist struct\n\t\ttl := value.(*todolist.TodoList)\n\n\t\t// Add Todolist name\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"## Todolist #%s: %s\\n\",\n\t\t\t\tkey, // Todolist ID\n\t\t\t\ttl.GetTodolistTitle(),\n\t\t\t),\n\t\t)\n\n\t\t// Add Todolist owner\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"#### Todolist owner : %s\\n\",\n\t\t\t\ttl.GetTodolistOwner(),\n\t\t\t),\n\t\t)\n\n\t\t// List all todos that are currently Todolisted\n\t\tif todos := tl.GetTasks(); len(todos) \u003e 0 {\n\t\t\tb.WriteString(\n\t\t\t\tufmt.Sprintf(\"Currently Todo tasks: %d\\n\\n\", len(todos)),\n\t\t\t)\n\n\t\t\tfor index, todo := range todos {\n\t\t\t\tb.WriteString(\n\t\t\t\t\tufmt.Sprintf(\"#%d - %s \", index, todo.Title),\n\t\t\t\t)\n\t\t\t\t// displays a checked box if task is marked as done, an empty box if not\n\t\t\t\tif todo.Done {\n\t\t\t\t\tb.WriteString(\n\t\t\t\t\t\t\"☑\\n\\n\",\n\t\t\t\t\t)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tb.WriteString(\n\t\t\t\t\t\"☐\\n\\n\",\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tb.WriteString(\"No tasks in this list currently\\n\")\n\t\t}\n\n\t\tb.WriteString(\"\\n\")\n\t\treturn false\n\t})\n\n\treturn b.String()\n}\n"},{"name":"todolist_test.gno","body":"package todolistrealm\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tnode interface{}\n\ttdl *todolist.TodoList\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttlid, _ := NewTodoList(title)\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\n\t// get the todolist node from the tree\n\tnode, _ = todolistTree.Get(strconv.Itoa(tlid))\n\t// convert the node to a TodoList struct\n\ttdl = node.(*todolist.TodoList)\n\n\tuassert.Equal(t, title, tdl.Title, \"title does not match\")\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\tuassert.Equal(t, tdl.Owner.String(), std.GetOrigCaller().String(), \"owner does not match\")\n\tuassert.Equal(t, 0, len(tdl.GetTasks()), \"Expected no tasks in the todo list\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\tAddTask(1, \"Task 1\")\n\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 1, len(tasks), \"total task does not match\")\n\tuassert.Equal(t, \"Task 1\", tasks[0].Title, \"task title does not match\")\n\tuassert.False(t, tasks[0].Done, \"Expected task to be not done\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\tToggleTaskStatus(1, 0)\n\ttask := tdl.GetTasks()[0]\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not marked as done\")\n\n\tToggleTaskStatus(1, 0)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is marked as done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\tRemoveTask(1, 0)\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 0, len(tasks), \"Expected no tasks in the todo list\")\n}\n\nfunc TestRemoveTodoList(t *testing.T) {\n\tRemoveTodoList(1)\n\tuassert.Equal(t, 0, todolistTree.Size(), \"Expected no tasks in the todo list\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"types","path":"gno.land/r/demo/types","files":[{"name":"types.gno","body":"// package to test types behavior in various conditions (TXs, imports).\npackage types\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tgInt int = -42\n\tgUint uint = 42\n\tgString string = \"a string\"\n\tgStringSlice []string = []string{\"a\", \"string\", \"slice\"}\n\tgError error = errors.New(\"an error\")\n\tgIntSlice []int = []int{-42, 0, 42}\n\tgUintSlice []uint = []uint{0, 42, 84}\n\tgTree avl.Tree\n\t// gInterface = interface{}{-42, \"a string\", uint(42)}\n)\n\nfunc init() {\n\tgTree.Set(\"a\", \"content of A\")\n\tgTree.Set(\"b\", \"content of B\")\n}\n\nfunc Noop() {}\nfunc RetTimeNow() time.Time { return time.Now() }\nfunc RetString() string { return gString }\nfunc RetStringPointer() *string { return \u0026gString }\nfunc RetUint() uint { return gUint }\nfunc RetInt() int { return gInt }\nfunc RetUintPointer() *uint { return \u0026gUint }\nfunc RetIntPointer() *int { return \u0026gInt }\nfunc RetTree() avl.Tree { return gTree }\nfunc RetIntSlice() []int { return gIntSlice }\nfunc RetUintSlice() []uint { return gUintSlice }\nfunc RetStringSlice() []string { return gStringSlice }\nfunc RetError() error { return gError }\nfunc Panic() { panic(\"PANIC!\") }\n\n// TODO: floats\n// TODO: typed errors\n// TODO: ret interface\n// TODO: recover\n// TODO: take types as input\n\nfunc Render(path string) string {\n\treturn \"package to test data types.\"\n}\n"},{"name":"types_test.gno","body":"package types\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ui","path":"gno.land/r/demo/ui","files":[{"name":"ui.gno","body":"package ui\n\nimport \"gno.land/p/demo/ui\"\n\nfunc Render(path string) string {\n\t// TODO: build this realm as a demo one with one page per feature.\n\n\t// TODO: pagination\n\t// TODO: non-standard markdown\n\t// TODO: error, warn\n\t// TODO: header\n\t// TODO: HTML\n\t// TODO: toc\n\t// TODO: forms\n\t// TODO: comments\n\n\tdom := ui.DOM{\n\t\tPrefix: \"r/demo/ui:\",\n\t}\n\n\tdom.Title = \"UI Demo\"\n\n\tdom.Header.Append(ui.Breadcrumb{\n\t\tui.Link{Text: \"foo\", Path: \"foo\"},\n\t\tui.Link{Text: \"bar\", Path: \"foo/bar\"},\n\t})\n\n\tdom.Body.Append(\n\t\tui.Paragraph(\"Simple UI demonstration.\"),\n\t\tui.BulletList{\n\t\t\tui.Text(\"a text\"),\n\t\t\tui.Link{Text: \"a relative link\", Path: \"foobar\"},\n\t\t\tui.Text(\"another text\"),\n\t\t\t// ui.H1(\"a H1 text\"),\n\t\t\tui.Bold(\"a bold text\"),\n\t\t\tui.Italic(\"italic text\"),\n\t\t\tui.Text(\"raw markdown with **bold** text in the middle.\"),\n\t\t\tui.Code(\"some inline code\"),\n\t\t\tui.Link{Text: \"a remote link\", URL: \"https://gno.land\"},\n\t\t},\n\t)\n\n\tdom.Footer.Append(ui.Text(\"I'm the footer.\"))\n\tdom.Body.Append(ui.Text(\"another string.\"))\n\tdom.Body.Append(ui.Paragraph(\"a paragraph.\"), ui.HR{})\n\n\treturn dom.String()\n}\n"},{"name":"ui_test.gno","body":"package ui\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestRender(t *testing.T) {\n\tgot := Render(\"\")\n\texpected := \"# UI Demo\\n\\n[foo](r/demo/ui:foo) / [bar](r/demo/ui:foo/bar)\\n\\n\\nSimple UI demonstration.\\n\\n- a text\\n- [a relative link](r/demo/ui:foobar)\\n- another text\\n- **a bold text**\\n- _italic text_\\n- raw markdown with **bold** text in the middle.\\n- `some inline code`\\n- [a remote link](https://gno.land)\\n\\nanother string.\\n\\na paragraph.\\n\\n\\n---\\n\\n\\nI'm the footer.\\n\\n\"\n\tuassert.Equal(t, expected, got)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"userbook","path":"gno.land/r/demo/userbook","files":[{"name":"userbook.gno","body":"// This realm demonstrates a small userbook system working with gnoweb\npackage userbook\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Signup struct {\n\taccount string\n\theight int64\n}\n\n// signups - keep a slice of signed up addresses efficient pagination\nvar signups []Signup\n\n// tracker - keep track of who signed up\nvar (\n\ttracker *avl.Tree\n\trouter *mux.Router\n)\n\nconst (\n\tdefaultPageSize = 20\n\tpathArgument = \"number\"\n\tsubPath = \"page/{\" + pathArgument + \"}\"\n\tsignUpEvent = \"SignUp\"\n)\n\nfunc init() {\n\t// Set up tracker tree\n\ttracker = avl.NewTree()\n\n\t// Set up route handling\n\trouter = mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHelper)\n\trouter.HandleFunc(subPath, renderHelper)\n\n\t// Sign up the deployer\n\tSignUp()\n}\n\nfunc SignUp() string {\n\t// Get transaction caller\n\tcaller := std.PrevRealm().Addr().String()\n\theight := std.GetHeight()\n\n\t// Check if the user is already signed up\n\tif _, exists := tracker.Get(caller); exists {\n\t\tpanic(caller + \" is already signed up!\")\n\t}\n\n\t// Sign up the user\n\ttracker.Set(caller, struct{}{})\n\tsignup := Signup{\n\t\tcaller,\n\t\theight,\n\t}\n\n\tsignups = append(signups, signup)\n\tstd.Emit(signUpEvent, \"SignedUpAccount\", signup.account)\n\n\treturn ufmt.Sprintf(\"%s added to userbook up at block #%d!\", signup.account, signup.height)\n}\n\nfunc GetSignupsInRange(page, pageSize int) ([]Signup, int) {\n\tif page \u003c 1 {\n\t\tpanic(\"page number cannot be less than 1\")\n\t}\n\n\tif pageSize \u003c 1 || pageSize \u003e 50 {\n\t\tpanic(\"page size must be from 1 to 50\")\n\t}\n\n\t// Pagination\n\t// Calculate indexes\n\tstartIndex := (page - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\n\t// If page does not contain any users\n\tif startIndex \u003e= len(signups) {\n\t\treturn nil, -1\n\t}\n\n\t// If page contains fewer users than the page size\n\tif endIndex \u003e len(signups) {\n\t\tendIndex = len(signups)\n\t}\n\n\treturn signups[startIndex:endIndex], endIndex\n}\n\nfunc renderHelper(res *mux.ResponseWriter, req *mux.Request) {\n\ttotalSignups := len(signups)\n\tres.Write(\"# Welcome to UserBook!\\n\\n\")\n\n\t// Get URL parameter\n\tpage, err := strconv.Atoi(req.GetVar(\"number\"))\n\tif err != nil {\n\t\tpage = 1 // render first page on bad input\n\t}\n\n\t// Fetch paginated signups\n\tfetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize)\n\t// Handle empty page case\n\tif len(fetchedSignups) == 0 {\n\t\tres.Write(\"No users on this page!\\n\\n\")\n\t\tres.Write(\"---\\n\\n\")\n\t\tres.Write(\"[Back to Page #1](/r/demo/userbook:page/1)\\n\\n\")\n\t\treturn\n\t}\n\n\t// Write page title\n\tres.Write(ufmt.Sprintf(\"## UserBook - Page #%d:\\n\\n\", page))\n\n\t// Write signups\n\tpageStartIndex := defaultPageSize * (page - 1)\n\tfor i, signup := range fetchedSignups {\n\t\tout := ufmt.Sprintf(\"#### User #%d - %s - signed up at Block #%d\\n\", pageStartIndex+i, signup.account, signup.height)\n\t\tres.Write(out)\n\t}\n\n\tres.Write(\"---\\n\\n\")\n\n\t// Write UserBook info\n\tlatestSignupIndex := totalSignups - 1\n\tres.Write(ufmt.Sprintf(\"#### Total users: %d\\n\", totalSignups))\n\tres.Write(ufmt.Sprintf(\"#### Latest signup: User #%d at Block #%d\\n\", latestSignupIndex, signups[latestSignupIndex].height))\n\n\tres.Write(\"---\\n\\n\")\n\n\t// Write page number\n\tres.Write(ufmt.Sprintf(\"You're viewing page #%d\", page))\n\n\t// Write navigation buttons\n\tvar prevPage string\n\tvar nextPage string\n\t// If we are on any page that is not the first page\n\tif page \u003e 1 {\n\t\tprevPage = ufmt.Sprintf(\" - [Previous page](/r/demo/userbook:page/%d)\", page-1)\n\t}\n\n\t// If there are more pages after the current one\n\tif endIndex \u003c totalSignups {\n\t\tnextPage = ufmt.Sprintf(\" - [Next page](/r/demo/userbook:page/%d)\\n\\n\", page+1)\n\t}\n\n\tres.Write(prevPage)\n\tres.Write(nextPage)\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n"},{"name":"userbook_test.gno","body":"package userbook\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestRender(t *testing.T) {\n\t// Sign up 20 users + deployer\n\tfor i := 0; i \u003c 20; i++ {\n\t\taddrName := ufmt.Sprintf(\"test%d\", i)\n\t\tcaller := testutils.TestAddress(addrName)\n\t\tstd.TestSetOrigCaller(caller)\n\t\tSignUp()\n\t}\n\n\ttestCases := []struct {\n\t\tname string\n\t\tnextPage bool\n\t\tprevPage bool\n\t\tpath string\n\t\texpectedNumberOfUsers int\n\t}{\n\t\t{\n\t\t\tname: \"1st page render\",\n\t\t\tnextPage: true,\n\t\t\tprevPage: false,\n\t\t\tpath: \"page/1\",\n\t\t\texpectedNumberOfUsers: 20,\n\t\t},\n\t\t{\n\t\t\tname: \"2nd page render\",\n\t\t\tnextPage: false,\n\t\t\tprevPage: true,\n\t\t\tpath: \"page/2\",\n\t\t\texpectedNumberOfUsers: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"Invalid path render\",\n\t\t\tnextPage: true,\n\t\t\tprevPage: false,\n\t\t\tpath: \"page/invalidtext\",\n\t\t\texpectedNumberOfUsers: 20,\n\t\t},\n\t\t{\n\t\t\tname: \"Empty Page\",\n\t\t\tnextPage: false,\n\t\t\tprevPage: false,\n\t\t\tpath: \"page/1000\",\n\t\t\texpectedNumberOfUsers: 0,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tnumUsers := countUsers(got)\n\n\t\t\tif tc.prevPage \u0026\u0026 !strings.Contains(got, \"Previous page\") {\n\t\t\t\tt.Fatalf(\"expected to find Previous page, didn't find it\")\n\t\t\t}\n\t\t\tif tc.nextPage \u0026\u0026 !strings.Contains(got, \"Next page\") {\n\t\t\t\tt.Fatalf(\"expected to find Next page, didn't find it\")\n\t\t\t}\n\n\t\t\tif tc.expectedNumberOfUsers != numUsers {\n\t\t\t\tt.Fatalf(\"expected %d, got %d users\", tc.expectedNumberOfUsers, numUsers)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc countUsers(input string) int {\n\treturn strings.Count(input, \"#### User #\")\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"wugnot","path":"gno.land/r/demo/wugnot","files":[{"name":"wugnot.gno","body":"package wugnot\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n\tpusers \"gno.land/p/demo/users\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\tbanker *grc20.Banker = grc20.NewBanker(\"wrapped GNOT\", \"wugnot\", 0)\n\tToken = banker.Token()\n)\n\nconst (\n\tugnotMinDeposit uint64 = 1000\n\twugnotMinDeposit uint64 = 1\n)\n\nfunc Deposit() {\n\tcaller := std.PrevRealm().Addr()\n\tsent := std.GetOrigSend()\n\tamount := sent.AmountOf(\"ugnot\")\n\n\trequire(uint64(amount) \u003e= ugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d ugnot.\", amount, ugnotMinDeposit))\n\tcheckErr(banker.Mint(caller, uint64(amount)))\n}\n\nfunc Withdraw(amount uint64) {\n\trequire(amount \u003e= wugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d wugnot.\", amount, wugnotMinDeposit))\n\n\tcaller := std.PrevRealm().Addr()\n\tpkgaddr := std.CurrentRealm().Addr()\n\tcallerBal := Token.BalanceOf(caller)\n\trequire(amount \u003c= callerBal, ufmt.Sprintf(\"Insufficient balance: %d available, %d needed.\", callerBal, amount))\n\n\t// send swapped ugnots to qcaller\n\tstdBanker := std.GetBanker(std.BankerTypeRealmSend)\n\tsend := std.Coins{{\"ugnot\", int64(amount)}}\n\tstdBanker.SendCoins(pkgaddr, caller, send)\n\tcheckErr(banker.Burn(caller, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn banker.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := std.Address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\"\n\t}\n}\n\nfunc TotalSupply() uint64 { return Token.TotalSupply() }\n\nfunc BalanceOf(owner pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\treturn Token.BalanceOf(ownerAddr)\n}\n\nfunc Allowance(owner, spender pusers.AddressOrName) uint64 {\n\townerAddr := users.Resolve(owner)\n\tspenderAddr := users.Resolve(spender)\n\treturn Token.Allowance(ownerAddr, spenderAddr)\n}\n\nfunc Transfer(to pusers.AddressOrName, amount uint64) {\n\ttoAddr := users.Resolve(to)\n\tcheckErr(Token.Transfer(toAddr, amount))\n}\n\nfunc Approve(spender pusers.AddressOrName, amount uint64) {\n\tspenderAddr := users.Resolve(spender)\n\tcheckErr(Token.Approve(spenderAddr, amount))\n}\n\nfunc TransferFrom(from, to pusers.AddressOrName, amount uint64) {\n\tfromAddr := users.Resolve(from)\n\ttoAddr := users.Resolve(to)\n\tcheckErr(Token.TransferFrom(fromAddr, toAddr, amount))\n}\n\nfunc require(condition bool, msg string) {\n\tif !condition {\n\t\tpanic(msg)\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"z0_filetest.gno","body":"// PKGPATH: gno.land/r/demo/wugnot_test\npackage wugnot_test\n\nimport (\n\t\"fmt\"\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/wugnot\"\n\n\tpusers \"gno.land/p/demo/users\"\n)\n\nvar (\n\taddr1 = testutils.TestAddress(\"test1\")\n\taddrc = std.DerivePkgAddr(\"gno.land/r/demo/wugnot\")\n\taddrt = std.DerivePkgAddr(\"gno.land/r/demo/wugnot_test\")\n)\n\nfunc main() {\n\tstd.TestSetOrigPkgAddr(addrc)\n\tstd.TestIssueCoins(addrc, std.Coins{{\"ugnot\", 100000001}}) // TODO: remove this\n\n\t// issue ugnots\n\tstd.TestIssueCoins(addr1, std.Coins{{\"ugnot\", 100000001}})\n\n\t// print initial state\n\tprintBalances()\n\t// println(wugnot.Render(\"queues\"))\n\t// println(\"A -\", wugnot.Render(\"\"))\n\n\tstd.TestSetOrigCaller(addr1)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 123_400}}, nil)\n\twugnot.Deposit()\n\tprintBalances()\n\twugnot.Withdraw(4242)\n\tprintBalances()\n}\n\nfunc printBalances() {\n\tprintSingleBalance := func(name string, addr std.Address) {\n\t\twugnotBal := wugnot.BalanceOf(pusers.AddressOrName(addr))\n\t\tstd.TestSetOrigCaller(addr)\n\t\trobanker := std.GetBanker(std.BankerTypeReadonly)\n\t\tcoins := robanker.GetCoins(addr).AmountOf(\"ugnot\")\n\t\tfmt.Printf(\"| %-13s | addr=%s | wugnot=%-5d | ugnot=%-9d |\\n\",\n\t\t\tname, addr, wugnotBal, coins)\n\t}\n\tprintln(\"-----------\")\n\tprintSingleBalance(\"wugnot_test\", addrt)\n\tprintSingleBalance(\"wugnot\", addrc)\n\tprintSingleBalance(\"addr1\", addr1)\n\tprintln(\"-----------\")\n}\n\n// Output:\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=200000000 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=200000000 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=200004242 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=99995759 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"gnoblog","path":"gno.land/r/gnoland/blog","files":[{"name":"admin.gno","body":"package gnoblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tcommenterList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // FIXME: delete instead?\n}\n\nfunc NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor {\n\tcallback := func() error {\n\t\taddPost(std.PrevRealm().Addr(), slug, title, body, publicationDate, authors, tags)\n\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\tcaller := std.GetOrigCaller()\n\taddPost(caller, slug, title, body, publicationDate, authors, tags)\n}\n\nfunc addPost(caller std.Address, slug, title, body, publicationDate, authors, tags string) {\n\tvar tagList []string\n\tif tags != \"\" {\n\t\ttagList = strings.Split(tags, \",\")\n\t}\n\tvar authorList []string\n\tif authors != \"\" {\n\t\tauthorList = strings.Split(authors, \",\")\n\t}\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModRemovePost(slug string) {\n\tassertIsModerator()\n\n\tb.RemovePost(slug)\n}\n\nfunc ModAddCommenter(addr std.Address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), true)\n}\n\nfunc ModDelCommenter(addr std.Address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), false) // FIXME: delete instead?\n}\n\nfunc ModDelComment(slug string, index int) {\n\tassertIsModerator()\n\n\terr := b.GetPost(slug).DeleteComment(index)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc isCommenter(addr std.Address) bool {\n\t_, found := commenterList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.GetOrigCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertIsCommenter() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) || isCommenter(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n"},{"name":"gnoblog.gno","body":"package gnoblog\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/blog\"\n)\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Gnoland's Blog\",\n\tPrefix: \"/r/gnoland/blog:\",\n}\n\nfunc AddComment(postSlug, comment string) {\n\tassertIsCommenter()\n\tassertNotInPause()\n\n\tcaller := std.GetOrigCaller()\n\terr := b.GetPost(postSlug).AddComment(caller, comment)\n\tcheckErr(err)\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n\nfunc RenderLastPostsWidget(limit int) string {\n\treturn b.RenderLastPostsWidget(limit)\n}\n\nfunc PostExists(slug string) bool {\n\tif b.GetPost(slug) == nil {\n\t\treturn false\n\t}\n\treturn true\n}\n"},{"name":"gnoblog_test.gno","body":"package gnoblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tstd.TestSetOrigCaller(std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"))\n\n\tauthor := std.GetOrigCaller()\n\n\t// by default, no posts.\n\t{\n\t\tgot := Render(\"\")\n\t\texpected := `\n# Gnoland's Blog\n\nNo posts.\n`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// create two posts, list post.\n\t{\n\t\tModAddPost(\"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\t\tModAddPost(\"slug2\", \"title2\", \"body2\", \"2022-05-20T13:17:23Z\", \"moul\", \"tag1,tag3\")\n\t\tgot := Render(\"\")\n\t\texpected := `\n\t# Gnoland's Blog\n\n\u003cdiv class='columns-3'\u003e\u003cdiv\u003e\n\n### [title2](/r/gnoland/blog:p/slug2)\n 20 May 2022\n\u003c/div\u003e\u003cdiv\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n 20 May 2022\n\u003c/div\u003e\u003c/div\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// view post.\n\t{\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\n\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)\n\nWritten by moul on 20 May 2022\n\nPublished by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003c/details\u003e\n\u003c/main\u003e\n\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// list by tags.\n\t{\n\t\tgot := Render(\"t/invalid\")\n\t\texpected := \"# [Gnoland's Blog](/r/gnoland/blog:) / t / invalid\\n\\nNo posts.\"\n\t\tassertMDEquals(t, got, expected)\n\n\t\tgot = Render(\"t/tag2\")\n\t\texpected = `\n# [Gnoland's Blog](/r/gnoland/blog:) / t / tag2\n\n\u003cdiv\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n 20 May 2022\n\u003c/div\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// add comments.\n\t{\n\t\tAddComment(\"slug1\", \"comment1\")\n\t\tAddComment(\"slug2\", \"comment2\")\n\t\tAddComment(\"slug1\", \"comment3\")\n\t\tAddComment(\"slug2\", \"comment4\")\n\t\tAddComment(\"slug1\", \"comment5\")\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)\n\nWritten by moul on 20 May 2022\n\nPublished by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003ch5\u003ecomment4\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// edit post.\n\t{\n\t\toldTitle := \"title2\"\n\t\toldDate := \"2022-05-20T13:17:23Z\"\n\n\t\tModEditPost(\"slug2\", oldTitle, \"body2++\", oldDate, \"manfred\", \"tag1,tag4\")\n\t\tgot := Render(\"p/slug2\")\n\t\texpected := `\u003cmain class='gno-tmpl-page'\u003e\n\n# title2\n\nbody2++\n\n---\n\nTags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4)\n\nWritten by manfred on 20 May 2022\n\nPublished by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog\n\n---\n\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\n\n\u003ch5\u003ecomment4\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\n\t\thome := Render(\"\")\n\n\t\tif strings.Count(home, oldTitle) != 1 {\n\t\t\tt.Errorf(\"post not edited properly\")\n\t\t}\n\t\t// Edits work everything except title, slug, and publicationDate\n\t\t// Edits to the above will cause duplication on the blog home page\n\t}\n\n\t{ // Test remove functionality\n\t\ttitle := \"example title\"\n\t\tslug := \"testSlug1\"\n\t\tModAddPost(slug, title, \"body1\", \"2022-05-25T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\n\t\tgot := Render(\"\")\n\n\t\tif !strings.Contains(got, title) {\n\t\t\tt.Errorf(\"post was not added properly\")\n\t\t}\n\n\t\tpostRender := Render(\"p/\" + slug)\n\n\t\tif !strings.Contains(postRender, title) {\n\t\t\tt.Errorf(\"post not rendered properly\")\n\t\t}\n\n\t\tModRemovePost(slug)\n\t\tgot = Render(\"\")\n\n\t\tif strings.Contains(got, title) {\n\t\t\tt.Errorf(\"post was not removed\")\n\t\t}\n\n\t\tpostRender = Render(\"p/\" + slug)\n\n\t\tassertMDEquals(t, postRender, \"404\")\n\t}\n\n\t// TODO: pagination.\n\t// TODO: ?format=...\n\n\t// all 404s\n\t{\n\t\tnotFoundPaths := []string{\n\t\t\t\"p/slug3\",\n\t\t\t\"p\",\n\t\t\t\"p/\",\n\t\t\t\"x/x\",\n\t\t\t\"t\",\n\t\t\t\"t/\",\n\t\t\t\"/\",\n\t\t\t\"p/slug1/\",\n\t\t}\n\t\tfor _, notFoundPath := range notFoundPaths {\n\t\t\tgot := Render(notFoundPath)\n\t\t\texpected := \"404\"\n\t\t\tif got != expected {\n\t\t\t\tt.Errorf(\"path %q: expected %q, got %q.\", notFoundPath, expected, got)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc assertMDEquals(t *testing.T, got, expected string) {\n\tt.Helper()\n\texpected = strings.TrimSpace(expected)\n\tgot = strings.TrimSpace(got)\n\tif expected != got {\n\t\tt.Errorf(\"invalid render output.\\nexpected %q.\\ngot %q.\", expected, got)\n\t}\n}\n"},{"name":"util.gno","body":"package gnoblog\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"events","path":"gno.land/r/gnoland/events","files":[{"name":"administration.gno","body":"package events\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable/exts/authorizable\"\n)\n\nvar (\n\tsu = std.Address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\tauth = authorizable.NewAuthorizableWithAddress(su)\n)\n\n// GetOwner gets the owner of the events realm\nfunc GetOwner() std.Address {\n\treturn auth.Owner()\n}\n\n// AddModerator adds a moderator to the events realm\nfunc AddModerator(mod std.Address) {\n\tauth.AssertCallerIsOwner()\n\n\tif err := auth.AddToAuthList(mod); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"errors.gno","body":"package events\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n)\n\nvar (\n\tErrEmptyName = errors.New(\"event name cannot be empty\")\n\tErrNoSuchID = errors.New(\"event with specified ID does not exist\")\n\tErrMinWidgetSize = errors.New(\"you need to request at least 1 event to render\")\n\tErrMaxWidgetSize = errors.New(\"maximum number of events in widget is\" + strconv.Itoa(MaxWidgetSize))\n\tErrDescriptionTooLong = errors.New(\"event description is too long\")\n\tErrInvalidStartTime = errors.New(\"invalid start time format\")\n\tErrInvalidEndTime = errors.New(\"invalid end time format\")\n\tErrEndBeforeStart = errors.New(\"end time cannot be before start time\")\n\tErrStartEndTimezonemMismatch = errors.New(\"start and end timezones are not the same\")\n)\n"},{"name":"events.gno","body":"// Package events allows you to upload data about specific IRL/online events\n// It includes dynamic support for updating rendering events based on their\n// status, ie if they are upcoming, in progress, or in the past.\npackage events\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype (\n\tEvent struct {\n\t\tid string\n\t\tname string // name of event\n\t\tdescription string // short description of event\n\t\tlink string // link to auth corresponding web2 page, ie eventbrite/luma or conference page\n\t\tlocation string // location of the event\n\t\tstartTime time.Time // given in RFC3339\n\t\tendTime time.Time // end time of the event, given in RFC3339\n\t}\n\n\teventsSlice []*Event\n)\n\nvar (\n\tevents = make(eventsSlice, 0) // sorted\n\tidCounter seqid.ID\n)\n\nconst (\n\tmaxDescLength = 100\n\tEventAdded = \"EventAdded\"\n\tEventDeleted = \"EventDeleted\"\n\tEventEdited = \"EventEdited\"\n)\n\n// AddEvent adds auth new event\n// Start time \u0026 end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00\nfunc AddEvent(name, description, link, location, startTime, endTime string) (string, error) {\n\tauth.AssertOnAuthList()\n\n\tif strings.TrimSpace(name) == \"\" {\n\t\treturn \"\", ErrEmptyName\n\t}\n\n\tif len(description) \u003e maxDescLength {\n\t\treturn \"\", ufmt.Errorf(\"%s: provided length is %d, maximum is %d\", ErrDescriptionTooLong, len(description), maxDescLength)\n\t}\n\n\t// Parse times\n\tst, et, err := parseTimes(startTime, endTime)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid := idCounter.Next().String()\n\te := \u0026Event{\n\t\tid: id,\n\t\tname: name,\n\t\tdescription: description,\n\t\tlink: link,\n\t\tlocation: location,\n\t\tstartTime: st,\n\t\tendTime: et,\n\t}\n\n\tevents = append(events, e)\n\tsort.Sort(events)\n\n\tstd.Emit(EventAdded,\n\t\t\"id\", e.id,\n\t)\n\n\treturn id, nil\n}\n\n// DeleteEvent deletes an event with auth given ID\nfunc DeleteEvent(id string) {\n\tauth.AssertOnAuthList()\n\n\te, idx, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tevents = append(events[:idx], events[idx+1:]...)\n\n\tstd.Emit(EventDeleted,\n\t\t\"id\", e.id,\n\t)\n}\n\n// EditEvent edits an event with auth given ID\n// It only updates values corresponding to non-empty arguments sent with the call\n// Note: if you need to update the start time or end time, you need to provide both every time\nfunc EditEvent(id string, name, description, link, location, startTime, endTime string) {\n\tauth.AssertOnAuthList()\n\n\te, _, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Set only valid values\n\tif strings.TrimSpace(name) != \"\" {\n\t\te.name = name\n\t}\n\n\tif strings.TrimSpace(description) != \"\" {\n\t\te.description = description\n\t}\n\n\tif strings.TrimSpace(link) != \"\" {\n\t\te.link = link\n\t}\n\n\tif strings.TrimSpace(location) != \"\" {\n\t\te.location = location\n\t}\n\n\tif strings.TrimSpace(startTime) != \"\" || strings.TrimSpace(endTime) != \"\" {\n\t\tst, et, err := parseTimes(startTime, endTime)\n\t\tif err != nil {\n\t\t\tpanic(err) // need to also revert other state changes\n\t\t}\n\n\t\toldStartTime := e.startTime\n\t\te.startTime = st\n\t\te.endTime = et\n\n\t\t// If sort order was disrupted, sort again\n\t\tif oldStartTime != e.startTime {\n\t\t\tsort.Sort(events)\n\t\t}\n\t}\n\n\tstd.Emit(EventEdited,\n\t\t\"id\", e.id,\n\t)\n}\n\nfunc GetEventByID(id string) (*Event, int, error) {\n\tfor i, event := range events {\n\t\tif event.id == id {\n\t\t\treturn event, i, nil\n\t\t}\n\t}\n\n\treturn nil, -1, ErrNoSuchID\n}\n\n// Len returns the length of the slice\nfunc (m eventsSlice) Len() int {\n\treturn len(m)\n}\n\n// Less compares the startTime fields of two elements\n// In this case, events will be sorted by largest startTime first (upcoming \u003e past)\nfunc (m eventsSlice) Less(i, j int) bool {\n\treturn m[i].startTime.After(m[j].startTime)\n}\n\n// Swap swaps two elements in the slice\nfunc (m eventsSlice) Swap(i, j int) {\n\tm[i], m[j] = m[j], m[i]\n}\n\n// parseTimes parses the start and end time for an event and checks for possible errors\nfunc parseTimes(startTime, endTime string) (time.Time, time.Time, error) {\n\tst, err := time.Parse(time.RFC3339, startTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidStartTime, err.Error())\n\t}\n\n\tet, err := time.Parse(time.RFC3339, endTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidEndTime, err.Error())\n\t}\n\n\tif et.Before(st) {\n\t\treturn time.Time{}, time.Time{}, ErrEndBeforeStart\n\t}\n\n\t_, stOffset := st.Zone()\n\t_, etOffset := et.Zone()\n\tif stOffset != etOffset {\n\t\treturn time.Time{}, time.Time{}, ErrStartEndTimezonemMismatch\n\t}\n\n\treturn st, et, nil\n}\n"},{"name":"events_test.gno","body":"package events\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tsuRealm = std.NewUserRealm(su)\n\n\tnow = \"2009-02-13T23:31:30Z\" // time.Now() is hardcoded to this value in the gno test machine currently\n\tparsedTimeNow, _ = time.Parse(time.RFC3339, now)\n)\n\nfunc TestAddEvent(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tAddEvent(\"Event 1\", \"this event is upcoming\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"Event 1\") {\n\t\tt.Fatalf(\"Expected to find Event 1 in render\")\n\t}\n\n\te2Start := parsedTimeNow.Add(-time.Hour * 24 * 5)\n\te2End := e2Start.Add(time.Hour * 4)\n\n\tAddEvent(\"Event 2\", \"this event is in the past\", \"gno.land\", \"gnome land\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\n\tgot = renderHome(false)\n\n\tupcomingPos := strings.Index(got, \"## Upcoming events\")\n\tpastPos := strings.Index(got, \"## Past events\")\n\n\te1Pos := strings.Index(got, \"Event 1\")\n\te2Pos := strings.Index(got, \"Event 2\")\n\n\t// expected index ordering: upcoming \u003c e1 \u003c past \u003c e2\n\tif e1Pos \u003c upcomingPos || e1Pos \u003e pastPos {\n\t\tt.Fatalf(\"Expected to find Event 1 in Upcoming events\")\n\t}\n\n\tif e2Pos \u003c upcomingPos || e2Pos \u003c pastPos || e2Pos \u003c e1Pos {\n\t\tt.Fatalf(\"Expected to find Event 2 on auth different pos\")\n\t}\n\n\t// larger index =\u003e smaller startTime (future =\u003e past)\n\tif events[0].startTime.Unix() \u003c events[1].startTime.Unix() {\n\t\tt.Fatalf(\"expected ordering to be different\")\n\t}\n}\n\nfunc TestAddEventErrors(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\t_, err := AddEvent(\"\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorIs(t, err, ErrEmptyName)\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:30:31Z\")\n\tuassert.ErrorIs(t, err, ErrEndBeforeStart)\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31+06:00\", \"2009-02-13T23:33:31+02:00\")\n\tuassert.ErrorIs(t, err, ErrStartEndTimezonemMismatch)\n\n\ttooLongDesc := `Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean ma`\n\t_, err = AddEvent(\"sample name\", tooLongDesc, \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrDescriptionTooLong.Error())\n}\n\nfunc TestDeleteEvent(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tid, _ := AddEvent(\"ToDelete\", \"description\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Expected to find ToDelete event in render\")\n\t}\n\n\tDeleteEvent(id)\n\tgot = renderHome(false)\n\n\tif strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Did not expect to find ToDelete event in render\")\n\t}\n}\n\nfunc TestEditEvent(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\tloc := \"gnome land\"\n\n\tid, _ := AddEvent(\"ToDelete\", \"description\", \"gno.land\", loc, e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tnewName := \"New Name\"\n\tnewDesc := \"Normal description\"\n\tnewLink := \"new Link\"\n\tnewST := e1Start.Add(time.Hour)\n\tnewET := newST.Add(time.Hour)\n\n\tEditEvent(id, newName, newDesc, newLink, \"\", newST.Format(time.RFC3339), newET.Format(time.RFC3339))\n\tedited, _, _ := GetEventByID(id)\n\n\t// Check updated values\n\tuassert.Equal(t, edited.name, newName)\n\tuassert.Equal(t, edited.description, newDesc)\n\tuassert.Equal(t, edited.link, newLink)\n\tuassert.True(t, edited.startTime.Equal(newST))\n\tuassert.True(t, edited.endTime.Equal(newET))\n\n\t// Check if the old values are the same\n\tuassert.Equal(t, edited.location, loc)\n}\n\nfunc TestInvalidEdit(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\tuassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() {\n\t\tEditEvent(\"123123\", \"\", \"\", \"\", \"\", \"\", \"\")\n\t})\n}\n\nfunc TestParseTimes(t *testing.T) {\n\t// times not provided\n\t// end time before start time\n\t// timezone Missmatch\n\n\t_, _, err := parseTimes(\"\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, _, err = parseTimes(now, \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, _, err = parseTimes(\"2009-02-13T23:30:30Z\", \"2009-02-13T21:30:30Z\")\n\tuassert.ErrorContains(t, err, ErrEndBeforeStart.Error())\n\n\t_, _, err = parseTimes(\"2009-02-10T23:30:30+02:00\", \"2009-02-13T21:30:33+05:00\")\n\tuassert.ErrorContains(t, err, ErrStartEndTimezonemMismatch.Error())\n}\n\nfunc TestRenderEventWidget(t *testing.T) {\n\tstd.TestSetOrigCaller(su)\n\tstd.TestSetRealm(suRealm)\n\n\t// No events yet\n\tevents = nil\n\tout, err := RenderEventWidget(1)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, out, \"No events.\")\n\n\t// Too many events\n\tout, err = RenderEventWidget(MaxWidgetSize + 1)\n\tuassert.ErrorIs(t, err, ErrMaxWidgetSize)\n\n\t// Too little events\n\tout, err = RenderEventWidget(0)\n\tuassert.ErrorIs(t, err, ErrMinWidgetSize)\n\n\t// Ordering \u0026 if requested amt is larger than the num of events that exist\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\te2Start := parsedTimeNow.Add(time.Hour * 24 * 10) // event 2 is after event 1\n\te2End := e2Start.Add(time.Hour * 4)\n\n\t_, err = AddEvent(\"Event 1\", \"description\", \"gno.land\", \"loc\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\t_, err = AddEvent(\"Event 2\", \"description\", \"gno.land\", \"loc\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\tout, err = RenderEventWidget(MaxWidgetSize)\n\turequire.NoError(t, err)\n\n\tuniqueSequence := \"- [\" // sequence that is displayed once per each event as per the RenderEventWidget function\n\tuassert.Equal(t, 2, strings.Count(out, uniqueSequence))\n\n\tuassert.True(t, strings.Index(out, \"Event 1\") \u003e strings.Index(out, \"Event 2\"))\n}\n"},{"name":"rendering.gno","body":"package events\n\nimport (\n\t\"bytes\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tMaxWidgetSize = 5\n)\n\n// RenderEventWidget shows up to eventsToRender of the latest events to a caller\nfunc RenderEventWidget(eventsToRender int) (string, error) {\n\tnumOfEvents := len(events)\n\tif numOfEvents == 0 {\n\t\treturn \"No events.\", nil\n\t}\n\n\tif eventsToRender \u003e MaxWidgetSize {\n\t\treturn \"\", ErrMaxWidgetSize\n\t}\n\n\tif eventsToRender \u003c 1 {\n\t\treturn \"\", ErrMinWidgetSize\n\t}\n\n\tif eventsToRender \u003e numOfEvents {\n\t\teventsToRender = numOfEvents\n\t}\n\n\toutput := \"\"\n\n\tfor _, event := range events[:eventsToRender] {\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", event.name, event.link)\n\t}\n\n\treturn output, nil\n}\n\n// renderHome renders the home page of the events realm\nfunc renderHome(admin bool) string {\n\toutput := \"# gno.land events\\n\\n\"\n\n\tif len(events) == 0 {\n\t\toutput += \"No upcoming or past events.\"\n\t\treturn output\n\t}\n\n\toutput += \"Below is a list of all gno.land events, including in progress, upcoming, and past ones.\\n\\n\"\n\toutput += \"---\\n\\n\"\n\n\tvar (\n\t\tinProgress = \"\"\n\t\tupcoming = \"\"\n\t\tpast = \"\"\n\t\tnow = time.Now()\n\t)\n\n\tfor _, e := range events {\n\t\tif now.Before(e.startTime) {\n\t\t\tupcoming += e.Render(admin)\n\t\t} else if now.After(e.endTime) {\n\t\t\tpast += e.Render(admin)\n\t\t} else {\n\t\t\tinProgress += e.Render(admin)\n\t\t}\n\t}\n\n\tif upcoming != \"\" {\n\t\t// Add upcoming events\n\t\toutput += \"## Upcoming events\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += upcoming\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif inProgress != \"\" {\n\t\toutput += \"## Currently in progress\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += inProgress\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif past != \"\" {\n\t\t// Add past events\n\t\toutput += \"## Past events\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += past\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t}\n\n\treturn output\n}\n\n// Render returns the markdown representation of a single event instance\nfunc (e Event) Render(admin bool) string {\n\tvar buf bytes.Buffer\n\n\tbuf.WriteString(\"\u003cdiv\u003e\\n\\n\")\n\tbuf.WriteString(ufmt.Sprintf(\"### %s\\n\\n\", e.name))\n\tbuf.WriteString(ufmt.Sprintf(\"%s\\n\\n\", e.description))\n\tbuf.WriteString(ufmt.Sprintf(\"**Location:** %s\\n\\n\", e.location))\n\n\t_, offset := e.startTime.Zone() // offset is in seconds\n\thoursOffset := offset / (60 * 60)\n\tsign := \"\"\n\tif offset \u003e= 0 {\n\t\tsign = \"+\"\n\t}\n\n\tbuf.WriteString(ufmt.Sprintf(\"**Starts:** %s UTC%s%d\\n\\n\", e.startTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\tbuf.WriteString(ufmt.Sprintf(\"**Ends:** %s UTC%s%d\\n\\n\", e.endTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\n\tif admin {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[EDIT](/r/gnoland/events$help\u0026func=EditEvent\u0026id=%s)\\n\\n\", e.id))\n\t\tbuf.WriteString(ufmt.Sprintf(\"[DELETE](/r/gnoland/events$help\u0026func=DeleteEvent\u0026id=%s)\\n\\n\", e.id))\n\t}\n\n\tif e.link != \"\" {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[See more](%s)\\n\\n\", e.link))\n\t}\n\n\tbuf.WriteString(\"\u003c/div\u003e\")\n\n\treturn buf.String()\n}\n\n// Render is the main rendering entry point\nfunc Render(path string) string {\n\tif path == \"admin\" {\n\t\treturn renderHome(true)\n\t}\n\n\treturn renderHome(false)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"faucet","path":"gno.land/r/gnoland/faucet","files":[{"name":"admin.gno","body":"package faucet\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nfunc AdminSetInPause(inPause bool) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgInPause = inPause\n\treturn \"\"\n}\n\nfunc AdminSetMessage(message string) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgMessage = message\n\treturn \"\"\n}\n\nfunc AdminSetTransferLimit(amount int64) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgLimit = std.NewCoin(\"ugnot\", amount)\n\treturn \"\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgAdminAddr = addr\n\treturn \"\"\n}\n\nfunc AdminAddController(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tsize := gControllers.Size()\n\n\tif size \u003e= gControllersMaxSize {\n\t\treturn \"can not add more controllers than allowed\"\n\t}\n\n\tif gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" exists, no need to add.\"\n\t}\n\n\tgControllers.Set(addr.String(), addr)\n\n\treturn \"\"\n}\n\nfunc AdminRemoveController(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif !gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\t_, ok := gControllers.Remove(addr.String())\n\n\t// it not should happen.\n\t// we will check anyway to prevent issues in the underline implementation.\n\n\tif !ok {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\treturn \"\"\n}\n\nfunc assertIsAdmin() error {\n\tcaller := std.GetOrigCaller()\n\tif caller != gAdminAddr {\n\t\treturn errors.New(\"restricted for admin\")\n\t}\n\treturn nil\n}\n"},{"name":"faucet.gno","body":"package faucet\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\t// configurable by admin.\n\tgAdminAddr std.Address = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tgControllers = avl.NewTree()\n\tgControllersMaxSize = 10 // limit it to 10\n\tgInPause = false\n\tgMessage = \"# Community Faucet.\\n\\n\"\n\n\t// internal vars, for stats.\n\tgTotalTransferred std.Coins\n\tgTotalTransfers = uint(0)\n\n\t// per request limit, 350 gnot\n\tgLimit std.Coin = std.NewCoin(\"ugnot\", 350000000)\n)\n\nfunc Transfer(to std.Address, send int64) string {\n\tif err := assertIsController(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif gInPause {\n\t\treturn errors.New(\"faucet in pause\").Error()\n\t}\n\n\t// limit the per request\n\tif send \u003e gLimit.Amount {\n\t\treturn errors.New(\"Per request limit \" + gLimit.String() + \" exceed\").Error()\n\t}\n\tsendCoins := std.Coins{std.NewCoin(\"ugnot\", send)}\n\n\tgTotalTransferred = gTotalTransferred.Add(sendCoins)\n\tgTotalTransfers++\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tpkgaddr := std.CurrentRealm().Addr()\n\tbanker.SendCoins(pkgaddr, to, sendCoins)\n\treturn \"\"\n}\n\nfunc GetPerTransferLimit() int64 {\n\treturn gLimit.Amount\n}\n\nfunc Render(_ string) string {\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\tbalance := banker.GetCoins(std.CurrentRealm().Addr())\n\n\toutput := gMessage\n\tif gInPause {\n\t\toutput += \"Status: inactive.\\n\"\n\t} else {\n\t\toutput += \"Status: active.\\n\"\n\t}\n\toutput += ufmt.Sprintf(\"Balance: %s.\\n\", balance.String())\n\toutput += ufmt.Sprintf(\"Total transfers: %s (in %d times).\\n\\n\", gTotalTransferred.String(), gTotalTransfers)\n\n\toutput += \"Package address: \" + std.CurrentRealm().Addr().String() + \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Admin: %s\\n\\n \", gAdminAddr.String())\n\toutput += ufmt.Sprintf(\"Controllers:\\n\\n \")\n\n\tfor i := 0; i \u003c gControllers.Size(); i++ {\n\t\t_, v := gControllers.GetByIndex(i)\n\t\toutput += ufmt.Sprintf(\"%s \", v.(std.Address))\n\t}\n\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Per request limit: %s\\n\\n\", gLimit.String())\n\n\treturn output\n}\n\nfunc assertIsController() error {\n\tcaller := std.GetOrigCaller()\n\n\tok := gControllers.Has(caller.String())\n\tif !ok {\n\t\treturn errors.New(caller.String() + \" is not on the controller list\")\n\t}\n\treturn nil\n}\n"},{"name":"faucet_test.gno","body":"package faucet\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tfaucetaddr = std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\tcontrolleraddr3 = testutils.TestAddress(\"controller3\")\n\t\tcontrolleraddr4 = testutils.TestAddress(\"controller4\")\n\t\tcontrolleraddr5 = testutils.TestAddress(\"controller5\")\n\t\tcontrolleraddr6 = testutils.TestAddress(\"controller6\")\n\t\tcontrolleraddr7 = testutils.TestAddress(\"controller7\")\n\t\tcontrolleraddr8 = testutils.TestAddress(\"controller8\")\n\t\tcontrolleraddr9 = testutils.TestAddress(\"controller9\")\n\t\tcontrolleraddr10 = testutils.TestAddress(\"controller10\")\n\t\tcontrolleraddr11 = testutils.TestAddress(\"controller11\")\n\n\t\ttest1addr = testutils.TestAddress(\"test1\")\n\t)\n\t// deposit 1000gnot to faucet contract\n\tstd.TestIssueCoins(faucetaddr, std.Coins{{\"ugnot\", 1000000000}})\n\tassertBalance(t, faucetaddr, 1200000000)\n\n\t// by default, balance is empty, and as a user I cannot call Transfer, or Admin commands.\n\n\tassertBalance(t, test1addr, 0)\n\tstd.TestSetOrigCaller(test1addr)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\tassertErr(t, faucet.AdminAddController(controlleraddr1))\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\t// as an admin, add the controller to contract and deposit more 2000gnot to contract\n\tstd.TestSetOrigCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr1))\n\tassertBalance(t, faucetaddr, 1200000000)\n\n\t// now, send some tokens as controller.\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertNoErr(t, faucet.Transfer(test1addr, 1000000))\n\tassertBalance(t, test1addr, 1000000)\n\tassertNoErr(t, faucet.Transfer(test1addr, 1000000))\n\tassertBalance(t, test1addr, 2000000)\n\tassertBalance(t, faucetaddr, 1198000000)\n\n\t// remove controller\n\t// as an admin, remove controller\n\tstd.TestSetOrigCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminRemoveController(controlleraddr1))\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\t// duplicate controller\n\tstd.TestSetOrigCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr1))\n\tassertErr(t, faucet.AdminAddController(controlleraddr1))\n\t// add more than more than allowed controllers\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr2))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr3))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr4))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr5))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr6))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr7))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr8))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr9))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr10))\n\tassertErr(t, faucet.AdminAddController(controlleraddr11))\n\n\t// send more than per transfer limit\n\tstd.TestSetOrigCaller(adminaddr)\n\tfaucet.AdminSetTransferLimit(300000000)\n\tstd.TestSetOrigCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 301000000))\n\n\t// block transefer from the address not on the controllers list.\n\tstd.TestSetOrigCaller(controlleraddr11)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n}\n\nfunc assertErr(t *testing.T, err string) {\n\tt.Helper()\n\n\tif err == \"\" {\n\t\tt.Logf(\"info: got err: %v\", err)\n\t\tt.Errorf(\"expected an error, got nil.\")\n\t}\n}\n\nfunc assertNoErr(t *testing.T, err string) {\n\tt.Helper()\n\tif err != \"\" {\n\t\tt.Errorf(\"got err: %v.\", err)\n\t}\n}\n\nfunc assertBalance(t *testing.T, addr std.Address, expectedBal int64) {\n\tt.Helper()\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(addr)\n\tgot := coins.AmountOf(\"ugnot\")\n\n\tif expectedBal != got {\n\t\tt.Errorf(\"invalid balance: expected %d, got %d.\", expectedBal, got)\n\t}\n}\n"},{"name":"z0_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with empty path and no controllers\nfunc main() {\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n"},{"name":"z1_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with a path and no controllers\nfunc main() {\n\tprintln(faucet.Render(\"path\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n"},{"name":"z2_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with empty path and 2 controllers\nfunc main() {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t)\n\tstd.TestSetOrigCaller(adminaddr)\n\terr := faucet.AdminAddController(controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n"},{"name":"z3_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints coints to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\tstd.TestIssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with 2 controllers and 2 transfers\nfunc main() {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\ttestaddr1 = testutils.TestAddress(\"test1\")\n\t\ttestaddr2 = testutils.TestAddress(\"test2\")\n\t)\n\tstd.TestSetOrigCaller(adminaddr)\n\terr := faucet.AdminAddController(controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tstd.TestSetOrigCaller(controlleraddr1)\n\terr = faucet.Transfer(testaddr1, 1000000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tstd.TestSetOrigCaller(controlleraddr2)\n\terr = faucet.Transfer(testaddr1, 2000000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 197000000ugnot.\n// Total transfers: 3000000ugnot (in 2 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ghverify","path":"gno.land/r/gnoland/ghverify","files":[{"name":"README.md","body":"# ghverify\n\nThis realm is intended to enable off chain gno address to github handle verification.\nThe steps are as follows:\n- A user calls `RequestVerification` and provides a github handle. This creates a new static oracle feed.\n- An off-chain agent controlled by the owner of this realm requests current feeds using the `GnorkleEntrypoint` function and provides a message of `\"request\"`\n- The agent receives the task information that includes the github handle and the gno address. It performs the verification step by checking whether this github user has the address in a github repository it controls.\n- The agent publishes the result of the verification by calling `GnorkleEntrypoint` with a message structured like: `\"ingest,\u003ctask id\u003e,\u003cverification status\u003e\"`. The verification status is `OK` if verification succeeded and any other value if it failed.\n- The oracle feed's ingester processes the verification and the handle to address mapping is written to the avl trees that exist as ghverify realm variables."},{"name":"contract.gno","body":"package ghverify\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\nconst (\n\t// The agent should send this value if it has verified the github handle.\n\tverifiedResult = \"OK\"\n)\n\nvar (\n\townerAddress = std.GetOrigCaller()\n\toracle *gnorkle.Instance\n\tpostHandler postGnorkleMessageHandler\n\n\thandleToAddressMap = avl.NewTree()\n\taddressToHandleMap = avl.NewTree()\n)\n\nfunc init() {\n\toracle = gnorkle.NewInstance()\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\ntype postGnorkleMessageHandler struct{}\n\n// Handle does post processing after a message is ingested by the oracle feed. It extracts the value to realm\n// storage and removes the feed from the oracle.\nfunc (h postGnorkleMessageHandler) Handle(i *gnorkle.Instance, funcType message.FuncType, feed gnorkle.Feed) error {\n\tif funcType != message.FuncTypeIngest {\n\t\treturn nil\n\t}\n\n\tresult, _, consumable := feed.Value()\n\tif !consumable {\n\t\treturn nil\n\t}\n\n\t// The value is consumable, meaning the ingestion occurred, so we can remove the feed from the oracle\n\t// after saving it to realm storage.\n\tdefer oracle.RemoveFeed(feed.ID())\n\n\t// Couldn't verify; nothing to do.\n\tif result.String != verifiedResult {\n\t\treturn nil\n\t}\n\n\tfeedTasks := feed.Tasks()\n\tif len(feedTasks) != 1 {\n\t\treturn errors.New(\"expected feed to have exactly one task\")\n\t}\n\n\ttask, ok := feedTasks[0].(*verificationTask)\n\tif !ok {\n\t\treturn errors.New(\"expected ghverify task\")\n\t}\n\n\thandleToAddressMap.Set(task.githubHandle, task.gnoAddress)\n\taddressToHandleMap.Set(task.gnoAddress, task.githubHandle)\n\treturn nil\n}\n\n// RequestVerification creates a new static feed with a single task that will\n// instruct an agent to verify the github handle / gno address pair.\nfunc RequestVerification(githubHandle string) {\n\tgnoAddress := string(std.GetOrigCaller())\n\tif err := oracle.AddFeeds(\n\t\tstatic.NewSingleValueFeed(\n\t\t\tgnoAddress,\n\t\t\t\"string\",\n\t\t\t\u0026verificationTask{\n\t\t\t\tgnoAddress: gnoAddress,\n\t\t\t\tgithubHandle: githubHandle,\n\t\t\t},\n\t\t),\n\t); err != nil {\n\t\tpanic(err)\n\t}\n\tstd.Emit(\n\t\t\"verification_requested\",\n\t\t\"from\", gnoAddress,\n\t\t\"handle\", githubHandle,\n\t)\n}\n\n// GnorkleEntrypoint is the entrypoint to the gnorkle oracle handler.\nfunc GnorkleEntrypoint(message string) string {\n\tresult, err := oracle.HandleMessage(message, postHandler)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn result\n}\n\n// SetOwner transfers ownership of the contract to the given address.\nfunc SetOwner(owner std.Address) {\n\tif ownerAddress != std.GetOrigCaller() {\n\t\tpanic(\"only the owner can set a new owner\")\n\t}\n\n\townerAddress = owner\n\n\t// In the context of this contract, the owner is the only one that can\n\t// add new feeds to the oracle.\n\toracle.ClearWhitelist(\"\")\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\n// GetHandleByAddress returns the github handle associated with the given gno address.\nfunc GetHandleByAddress(address string) string {\n\tif value, ok := addressToHandleMap.Get(address); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// GetAddressByHandle returns the gno address associated with the given github handle.\nfunc GetAddressByHandle(handle string) string {\n\tif value, ok := handleToAddressMap.Get(handle); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// Render returns a json object string will all verified handle -\u003e address mappings.\nfunc Render(_ string) string {\n\tresult := \"{\"\n\tvar appendComma bool\n\thandleToAddressMap.Iterate(\"\", \"\", func(handle string, address interface{}) bool {\n\t\tif appendComma {\n\t\t\tresult += \",\"\n\t\t}\n\n\t\tresult += `\"` + handle + `\": \"` + address.(string) + `\"`\n\t\tappendComma = true\n\n\t\treturn false\n\t})\n\n\treturn result + \"}\"\n}\n"},{"name":"contract_test.gno","body":"package ghverify\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestVerificationLifecycle(t *testing.T) {\n\tdefaultAddress := std.GetOrigCaller()\n\tuser1Address := std.Address(testutils.TestAddress(\"user 1\"))\n\tuser2Address := std.Address(testutils.TestAddress(\"user 2\"))\n\n\t// Verify request returns no feeds.\n\tresult := GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\tstd.TestSetOrigCaller(user1Address)\n\tRequestVerification(\"deelawn\")\n\n\t// A subsequent request from the same address should panic because there is\n\t// already a feed with an ID of this user's address.\n\tvar errMsg string\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tRequestVerification(\"deelawn\")\n\t}()\n\tif errMsg != \"feed already exists\" {\n\t\tt.Fatalf(\"expected feed already exists, got %s\", errMsg)\n\t}\n\n\t// Verify the request returns no feeds for this non-whitelisted user.\n\tresult = GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\tstd.TestSetOrigCaller(user2Address)\n\tRequestVerification(\"omarsy\")\n\n\t// Set the caller back to the whitelisted user and verify that the feed data\n\t// returned matches what should have been created by the `RequestVerification`\n\t// invocation.\n\tstd.TestSetOrigCaller(defaultAddress)\n\tresult = GnorkleEntrypoint(\"request\")\n\texpResult := `[{\"id\":\"` + string(user1Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user1Address) + `\",\"github_handle\":\"deelawn\"}]},` +\n\t\t`{\"id\":\"` + string(user2Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user2Address) + `\",\"github_handle\":\"omarsy\"}]}]`\n\tif result != expResult {\n\t\tt.Fatalf(\"expected request result %s, got %s\", expResult, result)\n\t}\n\n\t// Try to trigger feed ingestion from the non-authorized user.\n\tstd.TestSetOrigCaller(user1Address)\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tGnorkleEntrypoint(\"ingest,\" + string(user1Address) + \",OK\")\n\t}()\n\tif errMsg != \"caller not whitelisted\" {\n\t\tt.Fatalf(\"expected caller not whitelisted, got %s\", errMsg)\n\t}\n\n\t// Set the caller back to the whitelisted user and transfer contract ownership.\n\tstd.TestSetOrigCaller(defaultAddress)\n\tSetOwner(defaultAddress)\n\n\t// Now trigger the feed ingestion from the user and new owner and only whitelisted address.\n\tGnorkleEntrypoint(\"ingest,\" + string(user1Address) + \",OK\")\n\tGnorkleEntrypoint(\"ingest,\" + string(user2Address) + \",OK\")\n\n\t// Verify the ingestion autocommitted the value and triggered the post handler.\n\tdata := Render(\"\")\n\texpResult = `{\"deelawn\": \"` + string(user1Address) + `\",\"omarsy\": \"` + string(user2Address) + `\"}`\n\tif data != expResult {\n\t\tt.Fatalf(\"expected render data %s, got %s\", expResult, data)\n\t}\n\n\t// Finally make sure the feed was cleaned up after the data was committed.\n\tresult = GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Check that the accessor functions are working as expected.\n\tif handle := GetHandleByAddress(string(user1Address)); handle != \"deelawn\" {\n\t\tt.Fatalf(\"expected deelawn, got %s\", handle)\n\t}\n\tif address := GetAddressByHandle(\"deelawn\"); address != string(user1Address) {\n\t\tt.Fatalf(\"expected %s, got %s\", string(user1Address), address)\n\t}\n}\n"},{"name":"task.gno","body":"package ghverify\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n)\n\ntype verificationTask struct {\n\tgnoAddress string\n\tgithubHandle string\n}\n\n// MarshalJSON marshals the task contents to JSON.\nfunc (t *verificationTask) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write(\n\t\t[]byte(`{\"gno_address\":\"` + t.gnoAddress + `\",\"github_handle\":\"` + t.githubHandle + `\"}`),\n\t)\n\n\tw.Flush()\n\treturn buf.Bytes(), nil\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"home","path":"gno.land/r/gnoland/home","files":[{"name":"home.gno","body":"package home\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/ui\"\n\tblog \"gno.land/r/gnoland/blog\"\n\tevents \"gno.land/r/gnoland/events\"\n)\n\n// XXX: p/demo/ui API is crappy, we need to make it more idiomatic\n// XXX: use an updatable block system to update content from a DAO\n// XXX: var blocks avl.Tree\n\nvar (\n\toverride string\n\tadmin = ownable.NewWithAddress(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\") // @manfred by default\n)\n\nfunc Render(_ string) string {\n\tif override != \"\" {\n\t\treturn override\n\t}\n\n\tdom := ui.DOM{Prefix: \"r/gnoland/home:\"}\n\tdom.Title = \"Welcome to gno.land\"\n\tdom.Classes = []string{\"gno-tmpl-section\"}\n\n\t// body\n\tdom.Body.Append(introSection()...)\n\n\tdom.Body.Append(ui.Jumbotron(discoverLinks()))\n\n\tdom.Body.Append(\n\t\tui.Columns{3, []ui.Element{\n\t\t\tlastBlogposts(4),\n\t\t\tupcomingEvents(),\n\t\t\tlastContributions(4),\n\t\t}},\n\t)\n\n\tdom.Body.Append(ui.HR{})\n\tdom.Body.Append(playgroundSection()...)\n\tdom.Body.Append(ui.HR{})\n\tdom.Body.Append(packageStaffPicks()...)\n\tdom.Body.Append(ui.HR{})\n\tdom.Body.Append(worxDAO()...)\n\tdom.Body.Append(ui.HR{})\n\t// footer\n\tdom.Footer.Append(\n\t\tui.Columns{2, []ui.Element{\n\t\t\tsocialLinks(),\n\t\t\tquoteOfTheBlock(),\n\t\t}},\n\t)\n\n\t// Testnet disclaimer\n\tdom.Footer.Append(\n\t\tui.HR{},\n\t\tui.Bold(\"This is a testnet.\"),\n\t\tui.Text(\"Package names are not guaranteed to be available for production.\"),\n\t)\n\n\treturn dom.String()\n}\n\nfunc lastBlogposts(limit int) ui.Element {\n\tposts := blog.RenderLastPostsWidget(limit)\n\treturn ui.Element{\n\t\tui.H3(\"[Latest Blogposts](/r/gnoland/blog)\"),\n\t\tui.Text(posts),\n\t}\n}\n\nfunc lastContributions(limit int) ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"Latest Contributions\"),\n\t\t// TODO: import r/gh to\n\t\tui.Link{Text: \"View latest contributions\", URL: \"https://github.com/gnolang/gno/pulls\"},\n\t}\n}\n\nfunc upcomingEvents() ui.Element {\n\tout, _ := events.RenderEventWidget(events.MaxWidgetSize)\n\treturn ui.Element{\n\t\tui.H3(\"[Latest Events](/r/gnoland/events)\"),\n\t\tui.Text(out),\n\t}\n}\n\nfunc introSection() ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts.\"),\n\t\tui.Paragraph(\"With transparent and timeless code, gno.land is the next generation of smart contract platforms, serving as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse.\"),\n\t\tui.Paragraph(\"Intuitive and easy to use, gno.land lowers the barrier to web3 and makes censorship-resistant platforms accessible to everyone. If you want to help lay the foundations of a fairer and freer world, join us today.\"),\n\t}\n}\n\nfunc worxDAO() ui.Element {\n\t// WorxDAO\n\t// XXX(manfred): please, let me finish a v0, then we can iterate\n\t// highest level == highest responsibility\n\t// teams are responsible for components they don't owne\n\t// flag : realm maintainers VS facilitators\n\t// teams\n\t// committee of trustees to create the directory\n\t// each directory is a name, has a parent and have groups\n\t// homepage team - blocks aggregating events\n\t// XXX: TODO\n\t/*`\n\t# Directory\n\n\t* gno.land (owned by group)\n\t *\n\t* gnovm\n\t * gnolang (language)\n\t * gnovm\n\t - current challenges / concerns / issues\n\t* tm2\n\t * amino\n\t *\n\n\t## Contributors\n\t``*/\n\treturn ui.Element{\n\t\tui.H3(\"Contributions (WorxDAO \u0026 GoR)\"),\n\t\t// TODO: GoR dashboard + WorxDAO topics\n\t\tui.Text(`coming soon`),\n\t}\n}\n\nfunc quoteOfTheBlock() ui.Element {\n\tquotes := []string{\n\t\t\"Gno is for Truth.\",\n\t\t\"Gno is for Social Coordination.\",\n\t\t\"Gno is _not only_ for DeFi.\",\n\t\t\"Now, you Gno.\",\n\t\t\"Come for the Go, Stay for the Gno.\",\n\t}\n\theight := std.GetHeight()\n\tidx := int(height) % len(quotes)\n\tqotb := quotes[idx]\n\n\treturn ui.Element{\n\t\tui.H3(ufmt.Sprintf(\"Quote of the ~Day~ Block#%d\", height)),\n\t\tui.Quote(qotb),\n\t}\n}\n\nfunc socialLinks() ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"Socials\"),\n\t\tui.BulletList{\n\t\t\t// XXX: improve UI to support a nice GO api for such links\n\t\t\tui.Text(\"Check out our [community projects](https://github.com/gnolang/awesome-gno)\"),\n\t\t\tui.Text(\"![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)\"),\n\t\t\tui.Text(\"![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)\"),\n\t\t\tui.Text(\"![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)\"),\n\t\t\tui.Text(\"![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)\"),\n\t\t},\n\t}\n}\n\nfunc playgroundSection() ui.Element {\n\treturn ui.Element{\n\t\tui.H3(\"[Gno Playground](https://play.gno.land)\"),\n\t\tui.Paragraph(`Gno Playground is a web application designed for building, running, testing, and interacting\nwith your Gno code, enhancing your understanding of the Gno language. With Gno Playground, you can share your code,\nexecute tests, deploy your realms and packages to gno.land, and explore a multitude of other features.`),\n\t\tui.Paragraph(\"Experience the convenience of code sharing and rapid experimentation with [Gno Playground](https://play.gno.land).\"),\n\t}\n}\n\nfunc packageStaffPicks() ui.Element {\n\t// XXX: make it modifiable from a DAO\n\treturn ui.Element{\n\t\tui.H3(\"Explore New Packages and Realms\"),\n\t\tui.Columns{\n\t\t\t3,\n\t\t\t[]ui.Element{\n\t\t\t\t{\n\t\t\t\t\tui.H4(\"[r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/blog\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/dao\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/faucet\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/home\"},\n\t\t\t\t\t\tui.Link{URL: \"r/gnoland/pages\"},\n\t\t\t\t\t},\n\t\t\t\t\tui.H4(\"[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"r/sys/names\"},\n\t\t\t\t\t\tui.Link{URL: \"r/sys/rewards\"},\n\t\t\t\t\t\tui.Link{URL: \"/r/sys/validators/v2\"},\n\t\t\t\t\t},\n\t\t\t\t}, {\n\t\t\t\t\tui.H4(\"[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"r/demo/boards\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/users\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/banktest\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/foo20\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/foo721\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/microblog\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/nft\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/types\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/art/gnoface\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/art/millipede\"},\n\t\t\t\t\t\tui.Link{URL: \"r/demo/groups\"},\n\t\t\t\t\t\tui.Text(\"...\"),\n\t\t\t\t\t},\n\t\t\t\t}, {\n\t\t\t\t\tui.H4(\"[p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)\"),\n\t\t\t\t\tui.BulletList{\n\t\t\t\t\t\tui.Link{URL: \"p/demo/avl\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/blog\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/ui\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/ufmt\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/merkle\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/bf\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/flow\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/gnode\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/grc/grc20\"},\n\t\t\t\t\t\tui.Link{URL: \"p/demo/grc/grc721\"},\n\t\t\t\t\t\tui.Text(\"...\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc discoverLinks() ui.Element {\n\treturn ui.Element{\n\t\tui.Text(`\u003cdiv class=\"columns-3\"\u003e\n\u003cdiv class=\"column\"\u003e\n\n### Learn about gno.land\n\n- [About](/about)\n- [GitHub](https://github.com/gnolang)\n- [Blog](/blog)\n- [Events](/events)\n- Tokenomics (soon)\n- [Partners, Fund, Grants](/partners)\n- [Explore the Ecosystem](/ecosystem)\n- [Careers](https://jobs.ashbyhq.com/allinbits)\n\n\u003c/div\u003e\u003c!-- end column--\u003e\n\n\u003cdiv class=\"column\"\u003e\n\n### Build with Gno\n\n- [Write Gno in the browser](https://play.gno.land)\n- [Read about the Gno Language](/gnolang)\n- [Visit the official documentation](https://docs.gno.land)\n- [Gno by Example](https://gno-by-example.com/)\n- [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev)\n- [Get testnet GNOTs](https://faucet.gno.land)\n\n\u003c/div\u003e\u003c!-- end column--\u003e\n\u003cdiv class=\"column\"\u003e\n\n### Explore the universe\n\n- [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n- [Gnoscan](https://gnoscan.io)\n- [Portal Loop](https://docs.gno.land/concepts/portal-loop)\n- [Testnet 4](https://test4.gno.land/)\n- Testnet Faucet Hub (soon)\n\n\u003c/div\u003e\u003c!-- end column--\u003e\n\u003c/div\u003e\u003c!-- end columns-3--\u003e`),\n\t}\n}\n\nfunc AdminSetOverride(content string) {\n\tadmin.AssertCallerIsOwner()\n\toverride = content\n}\n\nfunc AdminTransferOwnership(newAdmin std.Address) {\n\tadmin.AssertCallerIsOwner()\n\tadmin.TransferOwnership(newAdmin)\n}\n"},{"name":"home_filetest.gno","body":"package main\n\nimport \"gno.land/r/gnoland/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// \u003cmain class='gno-tmpl-section'\u003e\n//\n// # Welcome to gno.land\n//\n// ### We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts.\n//\n//\n// With transparent and timeless code, gno.land is the next generation of smart contract platforms, serving as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse.\n//\n//\n// Intuitive and easy to use, gno.land lowers the barrier to web3 and makes censorship-resistant platforms accessible to everyone. If you want to help lay the foundations of a fairer and freer world, join us today.\n//\n// \u003cdiv class=\"jumbotron\"\u003e\n//\n// \u003cdiv class=\"columns-3\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Learn about gno.land\n//\n// - [About](/about)\n// - [GitHub](https://github.com/gnolang)\n// - [Blog](/blog)\n// - [Events](/events)\n// - Tokenomics (soon)\n// - [Partners, Fund, Grants](/partners)\n// - [Explore the Ecosystem](/ecosystem)\n// - [Careers](https://jobs.ashbyhq.com/allinbits)\n//\n// \u003c/div\u003e\u003c!-- end column--\u003e\n//\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Build with Gno\n//\n// - [Write Gno in the browser](https://play.gno.land)\n// - [Read about the Gno Language](/gnolang)\n// - [Visit the official documentation](https://docs.gno.land)\n// - [Gno by Example](https://gno-by-example.com/)\n// - [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev)\n// - [Get testnet GNOTs](https://faucet.gno.land)\n//\n// \u003c/div\u003e\u003c!-- end column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Explore the universe\n//\n// - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n// - [Gnoscan](https://gnoscan.io)\n// - [Portal Loop](https://docs.gno.land/concepts/portal-loop)\n// - [Testnet 4](https://test4.gno.land/)\n// - Testnet Faucet Hub (soon)\n//\n// \u003c/div\u003e\u003c!-- end column--\u003e\n// \u003c/div\u003e\u003c!-- end columns-3--\u003e\n// \u003c/div\u003e\u003c!-- /jumbotron --\u003e\n//\n// \u003cdiv class=\"columns-3\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### [Latest Blogposts](/r/gnoland/blog)\n//\n// No posts.\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### [Latest Events](/r/gnoland/events)\n//\n// No events.\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Latest Contributions\n//\n// [View latest contributions](https://github.com/gnolang/gno/pulls)\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003c/div\u003e\u003c!-- /columns-3 --\u003e\n//\n//\n// ---\n//\n// ### [Gno Playground](https://play.gno.land)\n//\n//\n// Gno Playground is a web application designed for building, running, testing, and interacting\n// with your Gno code, enhancing your understanding of the Gno language. With Gno Playground, you can share your code,\n// execute tests, deploy your realms and packages to gno.land, and explore a multitude of other features.\n//\n//\n// Experience the convenience of code sharing and rapid experimentation with [Gno Playground](https://play.gno.land).\n//\n//\n// ---\n//\n// ### Explore New Packages and Realms\n//\n// \u003cdiv class=\"columns-3\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// #### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)\n//\n// - [r/gnoland/blog](r/gnoland/blog)\n// - [r/gnoland/dao](r/gnoland/dao)\n// - [r/gnoland/faucet](r/gnoland/faucet)\n// - [r/gnoland/home](r/gnoland/home)\n// - [r/gnoland/pages](r/gnoland/pages)\n//\n// #### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)\n//\n// - [r/sys/names](r/sys/names)\n// - [r/sys/rewards](r/sys/rewards)\n// - [/r/sys/validators/v2](/r/sys/validators/v2)\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// #### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)\n//\n// - [r/demo/boards](r/demo/boards)\n// - [r/demo/users](r/demo/users)\n// - [r/demo/banktest](r/demo/banktest)\n// - [r/demo/foo20](r/demo/foo20)\n// - [r/demo/foo721](r/demo/foo721)\n// - [r/demo/microblog](r/demo/microblog)\n// - [r/demo/nft](r/demo/nft)\n// - [r/demo/types](r/demo/types)\n// - [r/demo/art/gnoface](r/demo/art/gnoface)\n// - [r/demo/art/millipede](r/demo/art/millipede)\n// - [r/demo/groups](r/demo/groups)\n// - ...\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// #### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)\n//\n// - [p/demo/avl](p/demo/avl)\n// - [p/demo/blog](p/demo/blog)\n// - [p/demo/ui](p/demo/ui)\n// - [p/demo/ufmt](p/demo/ufmt)\n// - [p/demo/merkle](p/demo/merkle)\n// - [p/demo/bf](p/demo/bf)\n// - [p/demo/flow](p/demo/flow)\n// - [p/demo/gnode](p/demo/gnode)\n// - [p/demo/grc/grc20](p/demo/grc/grc20)\n// - [p/demo/grc/grc721](p/demo/grc/grc721)\n// - ...\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003c/div\u003e\u003c!-- /columns-3 --\u003e\n//\n//\n// ---\n//\n// ### Contributions (WorxDAO \u0026 GoR)\n//\n// coming soon\n//\n// ---\n//\n//\n// \u003cdiv class=\"columns-2\"\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Socials\n//\n// - Check out our [community projects](https://github.com/gnolang/awesome-gno)\n// - ![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)\n// - ![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)\n// - ![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)\n// - ![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003cdiv class=\"column\"\u003e\n//\n// ### Quote of the ~Day~ Block#123\n//\n// \u003e Now, you Gno.\n//\n// \u003c/div\u003e\u003c!-- /column--\u003e\n// \u003c/div\u003e\u003c!-- /columns-2 --\u003e\n//\n//\n// ---\n//\n// **This is a testnet.**\n// Package names are not guaranteed to be available for production.\n//\n// \u003c/main\u003e\n"},{"name":"overide_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/home\"\n)\n\nfunc main() {\n\tstd.TestSetOrigCaller(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\thome.AdminSetOverride(\"Hello World!\")\n\tprintln(home.Render(\"\"))\n\thome.AdminTransferOwnership(testutils.TestAddress(\"newAdmin\"))\n\tdefer func() {\n\t\tr := recover()\n\t\tprintln(\"r: \", r)\n\t}()\n\thome.AdminSetOverride(\"Not admin anymore\")\n}\n\n// Output:\n// Hello World!\n// r: ownable: caller is not owner\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"monit","path":"gno.land/r/gnoland/monit","files":[{"name":"monit.gno","body":"// Package monit links a monitoring system with the chain in both directions.\n//\n// The agent will periodically call Incr() and verify that the value is always\n// higher than the previously known one. The contract will store the last update\n// time and use it to detect whether or not the monitoring agent is functioning\n// correctly.\npackage monit\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/watchdog\"\n)\n\nvar (\n\tcounter int\n\tlastUpdate time.Time\n\tlastCaller std.Address\n\twd = watchdog.Watchdog{Duration: 5 * time.Minute}\n\towner = ownable.New() // TODO: replace with -\u003e ownable.NewWithAddress...\n\twatchdogDuration = 5 * time.Minute\n)\n\n// Incr increments the counter and informs the watchdog that we're alive.\n// This function can be called by anyone.\nfunc Incr() int {\n\tcounter++\n\tlastUpdate = time.Now()\n\tlastCaller = std.PrevRealm().Addr()\n\twd.Alive()\n\treturn counter\n}\n\n// Reset resets the realm state.\n// This function can only be called by the admin.\nfunc Reset() {\n\tif owner.CallerIsOwner() != nil { // TODO: replace with owner.AssertCallerIsOwner\n\t\tpanic(\"unauthorized\")\n\t}\n\tcounter = 0\n\tlastCaller = std.PrevRealm().Addr()\n\tlastUpdate = time.Now()\n\twd = watchdog.Watchdog{Duration: 5 * time.Minute}\n}\n\nfunc Render(_ string) string {\n\tstatus := wd.Status()\n\treturn ufmt.Sprintf(\n\t\t\"counter=%d\\nlast update=%s\\nlast caller=%s\\nstatus=%s\",\n\t\tcounter, lastUpdate, lastCaller, status,\n\t)\n}\n\n// TransferOwnership transfers ownership to a new owner. This is a proxy to\n// ownable.Ownable.TransferOwnership.\nfunc TransferOwnership(newOwner std.Address) { owner.TransferOwnership(newOwner) }\n"},{"name":"monit_test.gno","body":"package monit\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPackage(t *testing.T) {\n\t// initial state, watchdog is KO.\n\t{\n\t\texpected := `counter=0\nlast update=0001-01-01 00:00:00 +0000 UTC\nlast caller=\nstatus=KO`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t// call Incr(), watchdog is OK.\n\tIncr()\n\tIncr()\n\tIncr()\n\t{\n\t\texpected := `counter=3\nlast update=2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\nlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\nstatus=OK`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t/* XXX: improve tests once we've the missing std.TestSkipTime feature\n\t\t// wait 1h, watchdog is KO.\n\t\tuse std.TestSkipTime(time.Hour)\n\t\t{\n\t\t\texpected := `counter=3\n\tlast update=2009-02-13 22:31:30 +0000 UTC m=+1234564290.000000001\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=KO`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\n\t\t// call Incr(), watchdog is OK.\n\t\tIncr()\n\t\t{\n\t\t\texpected := `counter=4\n\tlast update=2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=OK`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\t*/\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"gnopages","path":"gno.land/r/gnoland/pages","files":[{"name":"admin.gno","body":"package gnopages\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // XXX: delete instead?\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\tcaller := std.GetOrigCaller()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.GetOrigCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n"},{"name":"page_about.gno","body":"package gnopages\n\nfunc init() {\n\tpath := \"about\"\n\ttitle := \"gno.land Is A Platform To Write Smart Contracts In Gno\"\n\t// XXX: description := \"On gno.land, developers write smart contracts and other blockchain apps using Gno without learning a language that’s exclusive to a single ecosystem.\"\n\tbody := `\ngno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language. On gno.land, smart contracts can be uploaded on-chain only by publishing their full source code,\nmaking it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code\nlibraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent,\nauditable code that anyone can inspect and reuse.\n\ngno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of\nsmart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive\nto a single ecosystem or limited by design. Go developers can easily port their existing web apps to gno.land or build\nnew ones from scratch, making web3 vastly more accessible.\n\nSecured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes\nfairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that\noften corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and\nalignment.\n\nOne of our inspirations for gno.land is the gospels, which built a system of moral code that lasted thousands of years.\nBy observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for\nfuture generations with censorship-resistant tools that improve their understanding of the world.\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:22Z\", nil, nil)\n}\n"},{"name":"page_contribute.gno","body":"package gnopages\n\nfunc init() {\n\tpath := \"contribute\"\n\ttitle := \"Contributor Ecosystem: Call for Contributions\"\n\tbody := `\n\ngno.land puts at the center of its identity the contributors that help to create and shape the project into what it is; incentivizing those who contribute the most and help advance its vision. Eventually, contributions will be incentivized directly on-chain; in the meantime, this page serves to illustrate our current off-chain initiatives.\n\ngno.land is still in full-steam development. For now, we're looking for the earliest of adopters; curious to explore a new way to build smart contracts and eager to make an impact. Joining gno.land's development now means you can help to shape the base of its development ecosystem, which will pave the way for the next generation of blockchain programming.\n\nAs an open-source project, we welcome all contributions. On this page you can find some pointers on where to get started; as well as some incentives for the most valuable and important contributions.\n\n## Where to get started\n\nIf you are interested in contributing to gno.land, you can jump on in on our [GitHub monorepo](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md) - where most development happens.\n\nA good place where to start are the issues tagged [\"good first issue\"](https://github.com/gnolang/gno/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). They should allow you to make some impact on the Gno repository while you're still exploring the details of how everything works.\n\n## Gno Bounties\n\nAdditionally, you can look out to help on specific issues labeled as bounties. All contributions will then concur to form your profile for Game of Realms.\n\nThe Gno bounty program is a good way to find interesting challenges in Gno, and get rewarded for helping us advance the project. We will maintain open and rewardable bounties in the gnolang/gno repository, and you can search all available bounties by using the [\"bounty\" label](https://github.com/gnolang/gno/labels/bounty).\n\nRecommendations on participating in the gno.land Bounty Program:\n\n- Identify the bounty you want to work on, and join in the discussion on the issue for anything that is unclear; or where you want to more clearly define the work to be done. At this stage, you can also start working on an initial implementation in your local enviornment.\n- Once you have spent time on the code related to the bounty, we recommend submitting a 'draft' PR as soon as possible.\n - The draft PR doesn't indicate that the bounty has been assigned to you, others are free to work on other draft PRs for the bounty.\n - Make sure to reference the bounty issue on the PR description you're writing.\n - After submitting the 'draft' PR, continue working until you are ready to mark the PR as \"ready for review\".\n - The core team will review the bounty PR submission after the work on the bounty has been completed, and determine if it qualifies for the bounty reward.\n- Ask for clarification early if an element on the requirements or implementation design is unclear.\n - Aside from publishing the PR early, keeping regular updates with the core team on the bounty issue is key to being on the right track.\n - As part of the requirements, you must adhere to the [contributing guidelines](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md); additionally, it is expected that any newly added code or functionality is properly documented, tested and covered, at least in 80% of added code.\n - You're welcome to propose additional features and work on an issue should you envision a plausible expansion or change in scope. The core team may assign a bounty to the additional work, or change the bounty with respect to the changed scope.\n\nYou may make your submission at any time; however we invite you to publish your draft PR very early in the development process. This will make your work public, so you can easily get help by the core team and other community members. Additionally, your work can be continued by other people should you get stuck or no longer be willing to work on the bounty. Likewise, you can continue the abandoned or stuck work that someone else worked on.\n\nDon't fear your work being \"stolen\": if a submission is the result of multiple people's efforts, we will look to split the bounty in a way that is fair and recognises each participant in creating the final outcome. Here are some examples of how that can happen:\n\n- If Alice does most of the work and abandons it; then Bob comes around and finishes the job, then Bob's PR will be merged. But the core team will propose a split like 70% for Alice and 30% for Bob (depending, of course, on the relative effort undertaken by both).\n- If Alice makes a PR that does only 50% of the work outlined in the requirements for the original issue, she will get 50%. Someone can still come up and finish the job; and claim the remaining part.\n\t- If you, for instance, cannot complete the entirety of the task or, as a non-developer, can only contribute a part of the specification/implementation, you may still be awarded a bounty for your input in the contribution.\n- If Alice makes a PR that aside from implementing what's required, also undertakes creating useful tools among the way, she may qualify for an \"outstanding contribution\"; and may be awarded up to 25% more of the original bounty's value. Or she may also ask if the team would be willing to offer a different bounty for the implementation of the tools.\n\nParticipants in the gno.land Bounty Program must meet the legal Terms and Conditions referenced [here](https://docs.google.com/document/d/e/2PACX-1vSUF-JwIXGscrNsc5QBD7Pa6i83mXUGogAEIf1wkeb_w42UgL3Lj6jFKMlNTdwEMUnhsLkjRlhe25K4/pub).\n\n### Bounty sizes\n\nEach bounty is associated with a size, to which corresponds the maximum compensation for the work involved on the bounty. A bounty size may under rare occasion be revisited to a bigger or smaller size; hence why it's important to talk about your proposed solution with the core team ahead of time.\n\nIn some cases, the work associated with a bounty may be outstanding. When that happens, the core team can decide to award up to 25% of the bounty's value to the recipient.\n\nThe value of the bounty, aside from the material completion of the task, considers the involved time in managing the created pull request and iterating on feedback.\n\n\nt-shirt size | expected compensation\n-------------|-----------------------\n[XS] | $ 500\n[S] | $ 1000\n[M] | $ 2000\n[L] | $ 4000\n[XL] | $ 8000\n_[XXL]_ \\* | $ 16000\n_[3XL]_ \\* | $ 32000\n\n[XS]: https://github.com/gnolang/gno/labels/bounty%2FXS\n[S]: https://github.com/gnolang/gno/labels/bounty%2FS\n[M]: https://github.com/gnolang/gno/labels/bounty%2FM\n[L]: https://github.com/gnolang/gno/labels/bounty%2FL\n[XL]: https://github.com/gnolang/gno/labels/bounty%2FXL\n[XXL]: https://github.com/gnolang/gno/labels/bounty%2FXXL\n[3XL]: https://github.com/gnolang/gno/labels/bounty%2F3XL\n\n\\*: XXL and 3XL bounties are exceptional. Almost no issues will have these sizes; most will be broken down into smaller bounties.\n\n## gno.land Grants\n\nThe gno.land grants program is to encourage and support the growth of the gno.land contributor community, and build out the usability of the platform and smart contract library. The program provides financial resources to contributors to explore the Gno tech stack, and build dApps, tooling, infrastructure, products, and smart contract libraries in gno.land.\n\nFor more details on gno.land grants, suggested topics, and how to apply, visit our grants [repository](https://github.com/gnolang/grants). \n\n## Join Game of Realms\n\nGame of Realms is the overarching contributor network of gnomes, currently running off-chain, and will eventually transition on-chain. At this stage, a Game of Realms contribution is comprised of high-impact contributions identified as ['notable contributions'](https://github.com/gnolang/game-of-realms/tree/main/contributors).\n\nThese contributions are not linked to immediate financial rewards, but are notable in nature, in the sense they are a challenge, make a significant addition to the project, and require persistence, with minimal feedback loops from the core team.\n\nThe selection of a notable contribution or the sum of contributions that equal 'notable' is based on the impact it has on the development of the project. For now, it is focused on code contributions, and will evolve over time. The Gno development teams will initially qualify and evaluate notable contributions, and vote off-chain on adding them to the 'notable contributions' folder on GitHub.\n\nYou can always contribute to the project, and all contributions will be noticed. Contributing now is a way to build your personal contributor profile in gno.land early on in the ecosystem, and signal your commitment to the project, the community, and its future.\n\nThere are a variety of ways to make your contributions count:\n\n- Core code contributions\n- Realm and pure package development\n- Validator tooling\n- Developer tooling\n- Tutorials and documentation\n\nTo start, we recommend you create a PR in the Game of Realms [repository](https://github.com/gnolang/game-of-realms) to create your profile page for all your contributions.`\n\n\t_ = b.NewPost(\"\", path, title, body, \"2024-09-05T00:00:00Z\", nil, nil)\n}\n"},{"name":"page_ecosystem.gno","body":"package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"ecosystem\"\n\t\ttitle = \"Discover gno.land Ecosystem Projects \u0026 Initiatives\"\n\t\t// XXX: description = \"Dive further into the gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building.\"\n\t\tbody = `\n### [Gno Playground](https://play.gno.land)\n\nGno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your\nunderstanding of the Gno language. You can share your code, run unit tests, deploy your realms and packages, and execute\nfunctions in your code using the repo.\n\nVisit the playground at [play.gno.land](https://play.gno.land)!\n\n### [Gno Studio Connect](https://gno.studio/connect)\n\nGno Studio Connect provides seamless access to realms, making it simple to explore, interact, and engage\nwith gno.land’s smart contracts through function calls. Connect focuses on function calls, enabling users to interact\nwith any realm’s exposed function(s) on gno.land.\n\nSee your realm interactions in [Gno Studio Connect](https://gno.studio/connect)\n\n### [Gnoscan](https://gnoscan.io)\n\nDeveloped by the Onbloc team, Gnoscan is gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find\ninformation that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts.\nGnoscan makes our on-chain data easy to read and intuitive to discover.\n\nExplore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)!\n\n### Adena\n\nAdena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to\ninteract easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a\nhigh-quality interface, support for NFTs and custom tokens, and seamless integration. Install Adena via the [official website](https://www.adena.app/)\n\n### Gnoswap\n\nGnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an\nautomated market maker (AMM) protocol written in Gno that allows for permissionless token exchanges on the platform.\n\n### Flippando\n\nFlippando is a simple on-chain memory game, ported from Solidity to Gno, which starts with an empty matrix to flip tiles\non to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player\nmust memorize their colors until the entire matrix is uncovered. The end result can be minted as an NFT, which can later\nbe assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles. Play the game at [Flippando](https://gno.flippando.xyz/flip)\n\n### Gno Native Kit\n\n[Gno Native Kit](https://github.com/gnolang/gnonative) is a framework that allows developers to build and port gno.land (d)apps written in the (d)app's native language.\n\n\n`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:23Z\", nil, nil)\n}\n"},{"name":"page_gnolang.gno","body":"package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"gnolang\"\n\t\ttitle = \"About the Gno, the Language for gno.land\"\n\t\t// TODO fix broken images\n\t\tbody = `\n\n[Gno](https://github.com/gnolang/gno) is an interpretation of the widely-used Go (Golang) programming language for blockchain created by Cosmos co-founder Jae Kwon in 2022 to mark a new era in smart contracting. Gno is ~99% identical to Go, so Go programmers can start coding in Gno right away, with a minimal learning curve. For example, Gno comes with blockchain-specific standard libraries, but any code that doesn’t use blockchain-specific logic can run in Go with minimal processing. Libraries that don’t make sense in the blockchain context, such as network or operating-system access, are not available in Gno. Otherwise, Gno loads and uses many standard libraries that power Go, so most of the parsing of the source code is the same.\n\nUnder the hood, the Gno code is parsed into an abstract syntax tree (AST) and the AST itself is used in the interpreter, rather than bytecode as in many virtual machines such as Java, Python, or Wasm. This makes even the GnoVM accessible to any Go programmer. The novel design of the intuitive GnoVM interpreter allows Gno to freeze and resume the program by persisting and loading the entire memory state. Gno is deterministic, auto-persisted, and auto-Merkle-ized, allowing (smart contract) programs to be succinct, as the programmer doesn’t have to serialize and deserialize objects to persist them into a database (unlike programming applications with the Cosmos SDK).\n\n## How Gno Differs from Go\n\n![Gno and Go differences](static/img/gno-language/go-and-gno.jpg)\n\nThe composable nature of Go/Gno allows for type-checked interactions between contracts, making gno.land safer and more powerful, as well as operationally cheaper and faster. Smart contracts on gno.land are light, simple, more focused, and easily interoperable—a network of interconnected contracts rather than siloed monoliths that limit interactions with other contracts.\n\n![Example of Gno code](static/img/gno-language/code-example.jpg)\n\n## Gno Inherits Go’s Built-in Security Features\n\nGo supports secure programming through exported/non-exported fields, enabling a “least-authority” design. It is easy to create objects and APIs that expose only what should be accessible to callers while hiding what should not be simply by the capitalization of letters, thus allowing a succinct representation of secure logic that can be called by multiple users.\n\nAnother major advantage of Go is that the language comes with an ecosystem of great tooling, like the compiler and third-party tools that statically analyze code. Gno inherits these advantages from Go directly to create a smart contract programming language that provides embedding, composability, type-check safety, and garbage collection, helping developers to write secure code relying on the compiler, parser, and interpreter to give warning alerts for common mistakes.\n\n## Gno vs Solidity\n\nThe most widely-adopted smart contract language today is Ethereum’s EVM-compatible Solidity. With bytecode built from the ground up and Turing complete, Solidity opened up a world of possibilities for decentralized applications (dApps) and there are currently more than 10 million contracts deployed on Ethereum. However, Solidity provides limited tooling and its EVM has a stack limit and computational inefficiencies.\n\nSolidity is designed for one purpose only (writing smart contracts) and is bound by the limitations of the EVM. In addition, developers have to learn several languages if they want to understand the whole stack or work across different ecosystems. Gno aspires to exceed Solidity on multiple fronts (and other smart contract languages like CosmWasm or Substrate) as every part of the stack is written in Gno. It’s easy for developers to understand the entire system just by studying a relatively small code base.\n\n## Gno Is Essential for the Wider Adoption of Web3\n\nGno makes imports as easy as they are in web2 with runtime-based imports for seamless dependency flow comprehension, and support for complex structs, beyond primitive types. Gno is ultimately cost-effective as dependencies are loaded once, enabling remote function calls as local, and providing automatic and independent per-realm state persistence.\n\nUsing Gno, developers can rapidly accelerate application development and adopt a modular structure by reusing and reassembling existing modules without building from scratch. They can embed one structure inside another in an intuitive way while preserving localism, and the language specification is simple, successfully balancing practicality and minimalism.\n\nThe Go language is so well designed that the Gno smart contract system will become the new gold standard for smart contract development and other blockchain applications. As a programming language that is universally adopted, secure, composable, and complete, Gno is essential for the broader adoption of web3 and its sustainable growth.`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:25Z\", nil, nil)\n}\n"},{"name":"page_license.gno","body":"package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"license\"\n\t\ttitle = \"Gno Network General Public License\"\n\t\tbody = `Copyright (C) 2024 NewTendermint, LLC\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of the GNO Network General Public License as published by\nNewTendermint, LLC, either version 4 of the License, or (at your option) any\nlater version published by NewTendermint, LLC.\n\nThis program is distributed in the hope that it will be useful, but is provided\nas-is and WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNO Network\nGeneral Public License for more details.\n\nYou should have received a copy of the GNO Network General Public License along\nwith this program. If not, see \u003chttps://gno.land/license\u003e.\n\nAttached below are the terms of the GNO Network General Public License, Version\n4 (a fork of the GNU Affero General Public License 3).\n\n## Additional Terms\n\n### Strong Attribution\n\nIf any of your user interfaces, such as websites and mobile applications, serve\nas the primary point of entry to a platform or blockchain that 1) offers users\nthe ability to upload their own smart contracts to the platform or blockchain,\nand 2) leverages any Covered Work (including the GNO virtual machine) to run\nthose smart contracts on the platform or blockchain (\"Applicable Work\"), then\nthe Applicable Work must prominently link to (1) gno.land or (2) any other URL\ndesignated by NewTendermint, LLC that has not been rejected by the governance of\nthe first chain known as gno.land, provided that the identity of the first chain\nis not ambiguous. In the event the identity of the first chain is ambiguous,\nthen NewTendermint, LLC's designation shall control. Such link must appear\nconspicuously in the header or footer of the Applicable Work, such that all\nusers may learn of gno.land or the URL designated by NewTendermint, LLC.\n\nThis additional attribution requirement shall remain in effect for (1) 7\nyears from the date of publication of the Applicable Work, or (2) 7 years from\nthe date of publication of the Covered Work (including republication of new\nversions), whichever is later, but no later than 12 years after the application\nof this strong attribution requirement to the publication of the Applicable\nWork. For purposes of this Strong Attribution requirement, Covered Work shall\nmean any work that is licensed under the GNO Network General Public License,\nVersion 4 or later, by NewTendermint, LLC.\n\n\n# GNO NETWORK GENERAL PUBLIC LICENSE\n\nVersion 4, 7 May 2024\n\nModified from the GNU AFFERO GENERAL PUBLIC LICENSE.\nGNU is not affiliated with GNO or NewTendermint, LLC.\nCopyright (C) 2022 NewTendermint, LLC.\n\n## Preamble\n\nThe GNO Network General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works. By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\nWhen we speak of free software, we are referring to freedom, not\nprice. Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nDevelopers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\nA secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate. Many developers of free software are heartened and\nencouraged by the resulting cooperation. However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\nThe GNO Network General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community. It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server. Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n## TERMS AND CONDITIONS\n\n### 0. Definitions.\n\n\"This License\" refers to version 4 of the GNO Network General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense. Each licensee is addressed as \"you\". \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy. The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy. Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies. Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License. If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n### 1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it. \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form. A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities. However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work. For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n### 2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met. This License explicitly affirms your unlimited\npermission to run the unmodified Program. The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work. This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force. You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright. Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below. Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n### 4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n### 5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n- a) The work must carry prominent notices stating that you modified\n it, and giving a relevant date.\n- b) The work must carry prominent notices stating that it is\n released under this License and any conditions added under section\n 7. This requirement modifies the requirement in section 4 to\n \"keep intact all notices\".\n- c) You must license the entire work, as a whole, under this\n License to anyone who comes into possession of a copy. This\n License will therefore apply, along with any applicable section 7\n additional terms, to the whole of the work, and all its parts,\n regardless of how they are packaged. This License gives no\n permission to license the work in any other way, but it does not\n invalidate such permission if you have separately received it.\n- d) If the work has interactive user interfaces, each must display\n Appropriate Legal Notices; however, if the Program has interactive\n interfaces that do not display Appropriate Legal Notices, your\n work need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit. Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n### 6. Conveying Non-Source Forms.\n\n You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n- a) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by the\n Corresponding Source fixed on a durable physical medium\n customarily used for software interchange.\n- b) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by a\n written offer, valid for at least three years and valid for as\n long as you offer spare parts or customer support for that product\n model, to give anyone who possesses the object code either (1) a\n copy of the Corresponding Source for all the software in the\n product that is covered by this License, on a durable physical\n medium customarily used for software interchange, for a price no\n more than your reasonable cost of physically performing this\n conveying of source, or (2) access to copy the\n Corresponding Source from a network server at no charge.\n- c) Convey individual copies of the object code with a copy of the\n written offer to provide the Corresponding Source. This\n alternative is allowed only occasionally and noncommercially, and\n only if you received the object code with such an offer, in accord\n with subsection 6b.\n- d) Convey the object code by offering access from a designated\n place (gratis or for a charge), and offer equivalent access to the\n Corresponding Source in the same way through the same place at no\n further charge. You need not require recipients to copy the\n Corresponding Source along with the object code. If the place to\n copy the object code is a network server, the Corresponding Source\n may be on a different server (operated by you or a third party)\n that supports equivalent copying facilities, provided you maintain\n clear directions next to the object code saying where to find the\n Corresponding Source. Regardless of what server hosts the\n Corresponding Source, you remain obligated to ensure that it is\n available for as long as needed to satisfy these requirements.\n- e) Convey the object code using peer-to-peer transmission, provided\n you inform other peers where the object code and Corresponding\n Source of the work are being offered to the general public at no\n charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling. In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage. For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product. A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source. The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information. But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed. Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n### 7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law. If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit. (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.) You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n- a) Disclaiming warranty or limiting liability differently from the\n terms of sections 15 and 16 of this License; or\n- b) Requiring preservation of specified reasonable legal notices or\n author attributions in that material or in the Appropriate Legal\n Notices displayed by works containing it; or\n- c) Prohibiting misrepresentation of the origin of that material, or\n requiring that modified versions of such material be marked in\n reasonable ways as different from the original version; or\n- d) Limiting the use for publicity purposes of names of licensors or\n authors of the material; or\n- e) Declining to grant rights under trademark law for use of some\n trade names, trademarks, or service marks; or\n- f) Requiring indemnification of licensors and authors of that\n material by anyone who conveys the material (or modified versions of\n it) with contractual assumptions of liability to the recipient, for\n any liability that these contractual assumptions directly impose on\n those licensors and authors; or\n- g) Requiring strong attribution such as notices on any user interfaces\n that run or convey any covered work, such as a prominent link to a URL\n on the header of a website, such that all users of the covered work may\n become aware of the notice, for a period no longer than 20 years.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10. If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term. If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n### 8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License. Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License. If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n### 9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program. Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance. However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work. These actions infringe copyright if you do\nnot accept this License. Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n### 10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License. You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations. If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License. For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n### 11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based. The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version. For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement). To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients. \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License. You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n### 12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot convey a\ncovered work so as to simultaneously satisfy your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all. For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n### 13. Remote Network Interaction; Use with the GNU General Public License.\n\nNotwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software. This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work. The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n### 14. Revised Versions of this License.\n\nNewTendermint LLC may publish revised and/or new versions of\nthe GNO Network General Public License from time to time. Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nProgram specifies that a certain numbered version of the GNO Network General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Gno Software\nFoundation. If the Program does not specify a version number of the\nGNO Network General Public License, you may choose any version ever published\nby NewTendermint LLC.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNO Network General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions. However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n### 15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n### 16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n### 17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\n## How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n \u003cone line to give the program's name and a brief idea of what it does.\u003e\n Copyright (C) \u003cyear\u003e \u003cname of author\u003e\n\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNO Network General Public License as published by\n NewTendermint LLC, either version 4 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNO Network General Public License for more details.\n\n You should have received a copy of the GNO Network General Public License\n along with this program. If not, see \u003chttps://gno.land/license\u003e.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source. For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code. There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2024-04-22T00:00:00Z\", nil, nil)\n}\n"},{"name":"page_partners.gno","body":"package gnopages\n\nfunc init() {\n\tpath := \"partners\"\n\ttitle := \"Partnerships\"\n\tbody := `### Fund and Grants Program\n\nAre you a builder, tinkerer, or researcher? If you’re looking to create awesome dApps, tooling, infrastructure, \nor smart contract libraries on gno.land, you can apply for a grant. The gno.land Ecosystem Fund and Grants program \nprovides financial contributions for individuals and teams to innovate on the platform.\n\nRead more about our Funds and Grants program [here](https://github.com/gnolang/ecosystem-fund-grants).\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:27Z\", nil, nil)\n}\n"},{"name":"page_start.gno","body":"package gnopages\n\nfunc init() {\n\tpath := \"start\"\n\ttitle := \"Getting Started with Gno\"\n\t// XXX: description := \"\"\n\n\t// TODO: codegen to use README files here\n\n\t/* TODO: port previous message: This is a demo of Gno smart contract programming. This document was\n\tconstructed by Gno onto a smart contract hosted on the data Realm\n\tname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n\t([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\t*/\n\tbody := `## Getting Started with Gno\n\n- [Install Gno Key](/r/demo/boards:testboard/5)\n- TODO: add more links\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:28Z\", nil, nil)\n}\n"},{"name":"page_testnets.gno","body":"package gnopages\n\nfunc init() {\n\tpath := \"testnets\"\n\ttitle := \"gno.land Testnet List\"\n\tbody := `\n- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet\n- [staging.gno.land](https://staging.gno.land) - wiped every commit to monorepo master\n- _[test4.gno.land](https://test4.gno.land) (latest)_\n\nFor a list of RPC endpoints, see the [reference documentation](https://docs.gno.land/reference/rpc-endpoints).\n\n## Local development\n\nSee the \"Getting started\" section in the [official documentation](https://docs.gno.land/getting-started/local-setup).\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:29Z\", nil, nil)\n}\n"},{"name":"page_tokenomics.gno","body":"package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"tokenomics\"\n\t\ttitle = \"gno.land Tokenomics\"\n\t\t// XXX: description = \"\"\"\n\t\tbody = `Lorem Ipsum`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:30Z\", nil, nil)\n}\n"},{"name":"pages.gno","body":"package gnopages\n\nimport (\n\t\"gno.land/p/demo/blog\"\n)\n\n// TODO: switch from p/blog to p/pages\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Gnoland's Pages\",\n\tPrefix: \"/r/gnoland/pages:\",\n\tNoBreadcrumb: true,\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n"},{"name":"pages_test.gno","body":"package gnopages\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestHome(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"\")\n\texpectedSubtrings := []string{\n\t\t\"/r/gnoland/pages:p/tokenomics\",\n\t\t\"/r/gnoland/pages:p/start\",\n\t\t\"/r/gnoland/pages:p/contribute\",\n\t\t\"/r/gnoland/pages:p/about\",\n\t\t\"/r/gnoland/pages:p/gnolang\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintln(got)\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n\nfunc TestAbout(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"p/about\")\n\texpectedSubtrings := []string{\n\t\t\"gno.land Is A Platform To Write Smart Contracts In Gno\",\n\t\t\"gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\\nprogramming language.\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintln(got)\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n"},{"name":"util.gno","body":"package gnopages\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"validators","path":"gno.land/r/sys/validators/v2","files":[{"name":"doc.gno","body":"// Package validators implements the on-chain validator set management through Proof of Contribution.\n// The Realm exposes only a public executor for govdao proposals, that can suggest validator set changes.\npackage validators\n"},{"name":"gnosdk.gno","body":"package validators\n\nimport (\n\t\"gno.land/p/sys/validators\"\n)\n\n// GetChanges returns the validator changes stored on the realm, since the given block number.\n// This function is intended to be called by gno.land through the GnoSDK\nfunc GetChanges(from int64) []validators.Validator {\n\tvalsetChanges := make([]validators.Validator, 0)\n\n\t// Gather the changes from the specified block\n\tchanges.Iterate(getBlockID(from), \"\", func(_ string, value interface{}) bool {\n\t\tchs := value.([]change)\n\n\t\tfor _, ch := range chs {\n\t\t\tvalsetChanges = append(valsetChanges, ch.validator)\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn valsetChanges\n}\n"},{"name":"init.gno","body":"package validators\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/nt/poa\"\n\t\"gno.land/p/sys/validators\"\n)\n\nfunc init() {\n\t// Prepare the initial validator set\n\tset := []validators.Validator{\n\t\t// core-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1qn3jwvdpva622j3fyudqy65zstnqx2wnqhrs3s\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpndqtjh5dcsnd0gcez3frs3w6rsttmlekj4cyywegyh0n8uprwvj5n8688\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g1gtu9czw9qavrtdnf936usvwjwyjz0x0jk243au\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq4y0ppxhxazdwxhnsxxzdmh9rxht888n4fl0mskwcpq7y2404dm2h0lamk\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-3\n\t\t{\n\t\t\tAddress: std.Address(\"g19emxxnzzfa0pkffvthrss5drgccjnwj8mdme4f\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq288fe7pd2yy3h2h8qjh0elu3pxuamf3wpa9qt9s6jja0r3k64ue4mh636\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-4\n\t\t{\n\t\t\tAddress: std.Address(\"g1hyxtsgjr5zt06jcx4z0xenn3u442ad2xgzu7lp\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpy4mst534500z7k6xk5u7c9ex8zs44rjjhmxaxtw9zzjv82qkfhkhx2rfs\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-5\n\t\t{\n\t\t\tAddress: std.Address(\"g1l072ma0vfhx7s4vpevfvuxd6wzkv5ztt7gh99w\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqtvz3g6nvu3d6wdz97w7jdw2sjc65us5u8gj8pm4mhasw7zxakjhjn9qkm\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// core-val-6\n\t\t{\n\t\t\tAddress: std.Address(\"g1uwqd3284kuzm56auwyc9d87jf3953tp9pnt506\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp8xm09ura7mwyntee78cl64hgzq0x75f05tv7fkxpqvc797j37hsr3vgjg\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// berty-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1ut590acnamvhkrh4qz6dz9zt9e3hyu499u0gvl\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq2gncppkfzmx7s22mn60mf0uxzzpl23yx97hwmwm8yc6lupepqqnlexfch\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// onbloc-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1arkzjfrte9l97v9q2qye07v0lw07039gaa3hfy\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpkvdy7n9744qay76fzekpu9l6g3mp4hzhqjmp6k2as72ghlzc546ju3a09\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// onbloc-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g1x0m33nyne064xdx7tvlfcjwd4xkajjar6h523z\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp6s70v4wurhg699w6f9emkwxdlm2eyf2uv64annj47npq85tjeucedmky9\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1mxguhd5zacar64txhfm0v7hhtph5wur5hx86vs\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqz6fwulsygvu9xypka3zqxhkxllm467e3adphmj6y44vn3yy8qq34vxnse\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g1t9ctfa468hn6czff8kazw08crazehcxaqa2uaa\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpsq650w975vqsf6ajj5x4wdzfnrh64kmw7sljqz7wts6k0p6l36d0huls3\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-3\n\t\t{\n\t\t\tAddress: std.Address(\"g1sll4rtvrepdyzcvg5ml0kjtl7fnwgcsxgg9s5q\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zplr4zg2smgha4n9huwcywm6pnkuny2x2j44kk4srxcf0rrmpql3035k8s2\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// devx-val-4\n\t\t{\n\t\t\tAddress: std.Address(\"g1aa5pp94eaextkump38766hpdra74xtfh805msv\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqe85el3ardhel5vruywsdjw0vj2zjyhqhsyhcnuh0dy8xhuj8mxjg5h7uw\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// tori-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1r2lwzu0y0na4686a0lz4f2zqxlffqkfm7lqqqp\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq2quztlp2pffjsun3jeqyesru8rx9yc6tfj9na3hnw9qgn4zlrpul5mhd0\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// aib-val-1\n\t\t{\n\t\t\tAddress: std.Address(\"g1ecdu2gwz9d46srrhpu7k60pnrquvle5z2a5nn0\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqnrer4hlsq7q22egx9ur357hg8ftsntyh4z2g7x69u2s4ay25vdw4tredd\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// aib-val-2\n\t\t{\n\t\t\tAddress: std.Address(\"g169wsuqlrscnvxtsu6wrc0zuwn39tmctw7q9f0q\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpv6k4a2r6x6gt7eqp70l5vxluk9zkdmlqvkxztnc8zp2llq73e6ukxvsf6\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t\t// aib-val-3\n\t\t{\n\t\t\tAddress: std.Address(\"g1hfwh3ufph3zczs5wu4qvpgtv79fzh30rgzdux8\"),\n\t\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqxx3qdzl9f6lee42vhtka5luujhxg22tesyww52af68f75zzp0snyhl8mw\",\n\t\t\tVotingPower: 1,\n\t\t},\n\t}\n\n\t// The default valset protocol is PoA\n\tvp = poa.NewPoA(poa.WithInitialSet(set))\n\n\t// No changes to apply initially\n\tchanges = avl.NewTree()\n}\n"},{"name":"poc.gno","body":"package validators\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nconst errNoChangesProposed = \"no set changes proposed\"\n\n// NewPropExecutor creates a new executor that wraps a changes closure\n// proposal. This wrapper is required to ensure the GovDAO Realm actually\n// executed the callback.\n//\n// Concept adapted from:\n// https://github.com/gnolang/gno/pull/1945\nfunc NewPropExecutor(changesFn func() []validators.Validator) dao.Executor {\n\tif changesFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\tfor _, change := range changesFn() {\n\t\t\tif change.VotingPower == 0 {\n\t\t\t\t// This change request is to remove the validator\n\t\t\t\tremoveValidator(change.Address)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// This change request is to add the validator\n\t\t\taddValidator(change)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\n\n// IsValidator returns a flag indicating if the given bech32 address\n// is part of the validator set\nfunc IsValidator(addr std.Address) bool {\n\treturn vp.IsValidator(addr)\n}\n\n// GetValidators returns the typed validator set\nfunc GetValidators() []validators.Validator {\n\treturn vp.GetValidators()\n}\n"},{"name":"validators.gno","body":"package validators\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar (\n\tvp validators.ValsetProtocol // p is the underlying validator set protocol\n\tchanges *avl.Tree // changes holds any valset changes; seqid(block number) -\u003e []change\n)\n\n// change represents a single valset change, tied to a specific block number\ntype change struct {\n\tblockNum int64 // the block number associated with the valset change\n\tvalidator validators.Validator // the validator update\n}\n\n// addValidator adds a new validator to the validator set.\n// If the validator is already present, the method errors out\nfunc addValidator(validator validators.Validator) {\n\tval, err := vp.AddValidator(validator.Address, validator.PubKey, validator.VotingPower)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator added, note the change\n\tch := change{\n\t\tblockNum: std.GetHeight(),\n\t\tvalidator: val,\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tstd.Emit(validators.ValidatorAddedEvent)\n}\n\n// removeValidator removes the given validator from the set.\n// If the validator is not present in the set, the method errors out\nfunc removeValidator(address std.Address) {\n\tval, err := vp.RemoveValidator(address)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator removed, note the change\n\tch := change{\n\t\tblockNum: std.GetHeight(),\n\t\tvalidator: validators.Validator{\n\t\t\tAddress: val.Address,\n\t\t\tPubKey: val.PubKey,\n\t\t\tVotingPower: 0, // nullified the voting power indicates removal\n\t\t},\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tstd.Emit(validators.ValidatorRemovedEvent)\n}\n\n// saveChange saves the valset change\nfunc saveChange(ch change) {\n\tid := getBlockID(ch.blockNum)\n\n\tsetRaw, exists := changes.Get(id)\n\tif !exists {\n\t\tchanges.Set(id, []change{ch})\n\n\t\treturn\n\t}\n\n\t// Save the change\n\tset := setRaw.([]change)\n\tset = append(set, ch)\n\n\tchanges.Set(id, set)\n}\n\n// getBlockID converts the block number to a sequential ID\nfunc getBlockID(blockNum int64) string {\n\treturn seqid.ID(uint64(blockNum)).String()\n}\n\nfunc Render(_ string) string {\n\tvar (\n\t\tsize = changes.Size()\n\t\tmaxDisplay = 10\n\t)\n\n\tif size == 0 {\n\t\treturn \"No valset changes to apply.\"\n\t}\n\n\toutput := \"Valset changes:\\n\"\n\tchanges.ReverseIterateByOffset(size-maxDisplay, maxDisplay, func(_ string, value interface{}) bool {\n\t\tchs := value.([]change)\n\n\t\tfor _, ch := range chs {\n\t\t\toutput += ufmt.Sprintf(\n\t\t\t\t\"- #%d: %s (%d)\\n\",\n\t\t\t\tch.blockNum,\n\t\t\t\tch.validator.Address.String(),\n\t\t\t\tch.validator.VotingPower,\n\t\t\t)\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn output\n}\n"},{"name":"validators_test.gno","body":"package validators\n\nimport (\n\t\"testing\"\n\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/sys/validators\"\n)\n\n// generateTestValidators generates a dummy validator set\nfunc generateTestValidators(count int) []validators.Validator {\n\tvals := make([]validators.Validator, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tval := validators.Validator{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"%d\", i)),\n\t\t\tPubKey: \"public-key\",\n\t\t\tVotingPower: 10,\n\t\t}\n\n\t\tvals = append(vals, val)\n\t}\n\n\treturn vals\n}\n\nfunc TestValidators_AddRemove(t *testing.T) {\n\t// Clear any changes\n\tchanges = avl.NewTree()\n\n\tvar (\n\t\tvals = generateTestValidators(100)\n\t\tinitialHeight = int64(123)\n\t)\n\n\t// Add in the validators\n\tfor _, val := range vals {\n\t\taddValidator(val)\n\n\t\t// Make sure the validator is added\n\t\tuassert.True(t, vp.IsValidator(val.Address))\n\n\t\tstd.TestSkipHeights(1)\n\t}\n\n\tfor i := initialHeight; i \u003c initialHeight+int64(len(vals)); i++ {\n\t\t// Make sure the changes are saved\n\t\tchs := GetChanges(i)\n\n\t\t// We use the funky index calculation to make sure\n\t\t// changes are properly handled for each block span\n\t\tuassert.Equal(t, initialHeight+int64(len(vals))-i, int64(len(chs)))\n\n\t\tfor index, val := range vals[i-initialHeight:] {\n\t\t\t// Make sure the changes are equal to the additions\n\t\t\tch := chs[index]\n\n\t\t\tuassert.Equal(t, val.Address, ch.Address)\n\t\t\tuassert.Equal(t, val.PubKey, ch.PubKey)\n\t\t\tuassert.Equal(t, val.VotingPower, ch.VotingPower)\n\t\t}\n\t}\n\n\t// Save the beginning height for the removal\n\tinitialRemoveHeight := std.GetHeight()\n\n\t// Clear any changes\n\tchanges = avl.NewTree()\n\n\t// Remove the validators\n\tfor _, val := range vals {\n\t\tremoveValidator(val.Address)\n\n\t\t// Make sure the validator is removed\n\t\tuassert.False(t, vp.IsValidator(val.Address))\n\n\t\tstd.TestSkipHeights(1)\n\t}\n\n\tfor i := initialRemoveHeight; i \u003c initialRemoveHeight+int64(len(vals)); i++ {\n\t\t// Make sure the changes are saved\n\t\tchs := GetChanges(i)\n\n\t\t// We use the funky index calculation to make sure\n\t\t// changes are properly handled for each block span\n\t\tuassert.Equal(t, initialRemoveHeight+int64(len(vals))-i, int64(len(chs)))\n\n\t\tfor index, val := range vals[i-initialRemoveHeight:] {\n\t\t\t// Make sure the changes are equal to the additions\n\t\t\tch := chs[index]\n\n\t\t\tuassert.Equal(t, val.Address, ch.Address)\n\t\t\tuassert.Equal(t, val.PubKey, ch.PubKey)\n\t\t\tuassert.Equal(t, uint64(0), ch.VotingPower)\n\t\t}\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"valopers","path":"gno.land/r/gnoland/valopers/v2","files":[{"name":"init.gno","body":"package valopers\n\nimport \"gno.land/p/demo/avl\"\n\nfunc init() {\n\tvalopers = avl.NewTree()\n}\n"},{"name":"valopers.gno","body":"// Package valopers is designed around the permissionless lifecycle of valoper profiles.\n// It also includes parts designed for govdao to propose valset changes based on registered valopers.\npackage valopers\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ufmt\"\n\tpVals \"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao/bridge\"\n\tvalidators \"gno.land/r/sys/validators/v2\"\n)\n\nconst (\n\terrValoperExists = \"valoper already exists\"\n\terrValoperMissing = \"valoper does not exist\"\n\terrInvalidAddressUpdate = \"valoper updated address exists\"\n\terrValoperNotCaller = \"valoper is not the caller\"\n)\n\n// valopers keeps track of all the active validator operators\nvar valopers *avl.Tree // Address -\u003e Valoper\n\n// Valoper represents a validator operator profile\ntype Valoper struct {\n\tName string // the display name of the valoper\n\tMoniker string // the moniker of the valoper\n\tDescription string // the description of the valoper\n\n\tAddress std.Address // The bech32 gno address of the validator\n\tPubKey string // the bech32 public key of the validator\n\tP2PAddresses []string // the publicly reachable P2P addresses of the validator\n\tActive bool // flag indicating if the valoper is active\n}\n\n// Register registers a new valoper\nfunc Register(v Valoper) {\n\t// Check if the valoper is already registered\n\tif isValoper(v.Address) {\n\t\tpanic(errValoperExists)\n\t}\n\n\t// TODO add address derivation from public key\n\t// (when the laws of gno make it possible)\n\n\t// Save the valoper to the set\n\tvalopers.Set(v.Address.String(), v)\n}\n\n// Update updates an existing valoper\nfunc Update(address std.Address, v Valoper) {\n\t// Check if the valoper is present\n\tif !isValoper(address) {\n\t\tpanic(errValoperMissing)\n\t}\n\n\t// Check that the valoper wouldn't be\n\t// overwriting an existing one\n\tisAddressUpdate := address != v.Address\n\tif isAddressUpdate \u0026\u0026 isValoper(v.Address) {\n\t\tpanic(errInvalidAddressUpdate)\n\t}\n\n\t// Remove the old valoper info\n\t// in case the address changed\n\tif address != v.Address {\n\t\tvalopers.Remove(address.String())\n\t}\n\n\t// Save the new valoper info\n\tvalopers.Set(v.Address.String(), v)\n}\n\n// GetByAddr fetches the valoper using the address, if present\nfunc GetByAddr(address std.Address) Valoper {\n\tvaloperRaw, exists := valopers.Get(address.String())\n\tif !exists {\n\t\tpanic(errValoperMissing)\n\t}\n\n\treturn valoperRaw.(Valoper)\n}\n\n// Render renders the current valoper set\nfunc Render(_ string) string {\n\tif valopers.Size() == 0 {\n\t\treturn \"No valopers to display.\"\n\t}\n\n\toutput := \"Valset changes to apply:\\n\"\n\tvalopers.Iterate(\"\", \"\", func(_ string, value interface{}) bool {\n\t\tvaloper := value.(Valoper)\n\n\t\toutput += valoper.Render()\n\n\t\treturn false\n\t})\n\n\treturn output\n}\n\n// Render renders a single valoper with their information\nfunc (v Valoper) Render() string {\n\toutput := ufmt.Sprintf(\"## %s (%s)\\n\", v.Name, v.Moniker)\n\toutput += ufmt.Sprintf(\"%s\\n\\n\", v.Description)\n\toutput += ufmt.Sprintf(\"- Address: %s\\n\", v.Address.String())\n\toutput += ufmt.Sprintf(\"- PubKey: %s\\n\", v.PubKey)\n\toutput += \"- P2P Addresses: [\\n\"\n\n\tif len(v.P2PAddresses) == 0 {\n\t\toutput += \"]\\n\"\n\n\t\treturn output\n\t}\n\n\tfor index, addr := range v.P2PAddresses {\n\t\toutput += addr\n\n\t\tif index == len(v.P2PAddresses)-1 {\n\t\t\toutput += \"]\\n\"\n\n\t\t\tcontinue\n\t\t}\n\n\t\toutput += \",\\n\"\n\t}\n\n\treturn output\n}\n\n// isValoper checks if the valoper exists\nfunc isValoper(address std.Address) bool {\n\t_, exists := valopers.Get(address.String())\n\n\treturn exists\n}\n\n// GovDAOProposal creates a proposal to the GovDAO\n// for adding the given valoper to the validator set.\n// This function is meant to serve as a helper\n// for generating the govdao proposal\nfunc GovDAOProposal(address std.Address) {\n\tvar (\n\t\tvaloper = GetByAddr(address)\n\t\tvotingPower = uint64(1)\n\t)\n\n\t// Make sure the valoper is the caller\n\tif std.GetOrigCaller() != address {\n\t\tpanic(errValoperNotCaller)\n\t}\n\n\t// Determine the voting power\n\tif !valoper.Active {\n\t\tvotingPower = uint64(0)\n\t}\n\n\tchangesFn := func() []pVals.Validator {\n\t\treturn []pVals.Validator{\n\t\t\t{\n\t\t\t\tAddress: valoper.Address,\n\t\t\t\tPubKey: valoper.PubKey,\n\t\t\t\tVotingPower: votingPower,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Create the executor\n\texecutor := validators.NewPropExecutor(changesFn)\n\n\t// Craft the proposal description\n\tdescription := ufmt.Sprintf(\n\t\t\"Add valoper %s (Address: %s; PubKey: %s) to the valset\",\n\t\tvaloper.Name,\n\t\tvaloper.Address.String(),\n\t\tvaloper.PubKey,\n\t)\n\n\tprop := dao.ProposalRequest{\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\t// Create the govdao proposal\n\tbridge.GovDAO().Propose(prop)\n}\n"},{"name":"valopers_test.gno","body":"package valopers\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestValopers_Register(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"already a valoper\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tv := Valoper{\n\t\t\tAddress: testutils.TestAddress(\"valoper\"),\n\t\t}\n\n\t\t// Add the valoper\n\t\tvalopers.Set(v.Address.String(), v)\n\n\t\tuassert.PanicsWithMessage(t, errValoperExists, func() {\n\t\t\tRegister(v)\n\t\t})\n\t})\n\n\tt.Run(\"successful registration\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tv := Valoper{\n\t\t\tAddress: testutils.TestAddress(\"valoper\"),\n\t\t\tName: \"new valoper\",\n\t\t\tMoniker: \"val-1\",\n\t\t\tPubKey: \"pub key\",\n\t\t}\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(v)\n\t\t})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(v.Address)\n\n\t\t\tuassert.Equal(t, v.Address, valoper.Address)\n\t\t\tuassert.Equal(t, v.Name, valoper.Name)\n\t\t\tuassert.Equal(t, v.Moniker, valoper.Moniker)\n\t\t\tuassert.Equal(t, v.PubKey, valoper.PubKey)\n\t\t})\n\t})\n}\n\nfunc TestValopers_Update(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"non-existing valoper\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tv := Valoper{}\n\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, errValoperMissing, func() {\n\t\t\tUpdate(v.Address, v)\n\t\t})\n\t})\n\n\tt.Run(\"overwrite valoper\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tone := Valoper{\n\t\t\tAddress: testutils.TestAddress(\"valoper 1\"),\n\t\t}\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(one)\n\t\t})\n\n\t\tinitialAddress := testutils.TestAddress(\"valoper 2\")\n\t\ttwo := Valoper{\n\t\t\tAddress: initialAddress,\n\t\t}\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(two)\n\t\t})\n\n\t\t// Update the valoper address\n\t\t// so it overlaps\n\t\ttwo = Valoper{\n\t\t\tAddress: one.Address,\n\t\t}\n\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, errInvalidAddressUpdate, func() {\n\t\t\tUpdate(initialAddress, two)\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tvar (\n\t\t\tname = \"new valoper\"\n\t\t\tv = Valoper{\n\t\t\t\tAddress: testutils.TestAddress(\"valoper\"),\n\t\t\t\tName: name,\n\t\t\t\tPubKey: \"pub key\",\n\t\t\t}\n\t\t)\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(v)\n\t\t})\n\n\t\t// Update the valoper name\n\t\tv.Name = \"new name\"\n\t\tv.Active = false\n\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdate(v.Address, v)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(v.Address)\n\n\t\t\tuassert.Equal(t, v.Name, valoper.Name)\n\t\t\tuassert.Equal(t, v.Active, valoper.Active)\n\t\t})\n\t})\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"config","path":"gno.land/r/leon/config","files":[{"name":"config.gno","body":"package config\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar (\n\tmain std.Address // leon's main address\n\tbackup std.Address // backup address\n\n\tErrInvalidAddr = errors.New(\"leon's config: invalid address\")\n\tErrUnauthorized = errors.New(\"leon's config: unauthorized\")\n)\n\nfunc init() {\n\tmain = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n}\n\nfunc Address() std.Address {\n\treturn main\n}\n\nfunc Backup() std.Address {\n\treturn backup\n}\n\nfunc SetAddress(a std.Address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tmain = a\n\treturn nil\n}\n\nfunc SetBackup(a std.Address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tbackup = a\n\treturn nil\n}\n\nfunc checkAuthorized() error {\n\tcaller := std.PrevRealm().Addr()\n\tisAuthorized := caller == main || caller == backup\n\n\tif !isAuthorized {\n\t\treturn ErrUnauthorized\n\t}\n\n\treturn nil\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"home","path":"gno.land/r/leon/home","files":[{"name":"home.gno","body":"package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/demo/art/gnoface\"\n\t\"gno.land/r/demo/art/millipede\"\n\t\"gno.land/r/leon/config\"\n)\n\nvar (\n\tpfp string // link to profile picture\n\tpfpCaption string // profile picture caption\n\tabtMe [2]string\n)\n\nfunc init() {\n\tpfp = \"https://i.imgflip.com/91vskx.jpg\"\n\tpfpCaption = \"[My favourite painting \u0026 pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)\"\n\tabtMe = [2]string{\n\t\t`### About me\nHi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast, \nlife-long learner, and sharer of knowledge.`,\n\t\t`### Contributions\nMy contributions to gno.land can mainly be found \n[here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn).\n\nTODO import r/gh\n`,\n\t}\n}\n\nfunc UpdatePFP(url, caption string) {\n\tif !isAuthorized(std.PrevRealm().Addr()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tpfp = url\n\tpfpCaption = caption\n}\n\nfunc UpdateAboutMe(col1, col2 string) {\n\tif !isAuthorized(std.PrevRealm().Addr()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tabtMe[0] = col1\n\tabtMe[1] = col2\n}\n\nfunc Render(path string) string {\n\tout := \"# Leon's Homepage\\n\\n\"\n\n\tout += renderAboutMe()\n\tout += renderBlogPosts()\n\tout += \"\\n\\n\"\n\tout += renderArt()\n\n\treturn out\n}\n\nfunc renderBlogPosts() string {\n\tout := \"\"\n\t//out += \"## Leon's Blog Posts\"\n\n\t// todo fetch blog posts authored by @leohhhn\n\t// and render them\n\treturn out\n}\n\nfunc renderAboutMe() string {\n\tout := \"\u003cdiv class='columns-3'\u003e\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += ufmt.Sprintf(\"![my profile pic](%s)\\n\\n%s\\n\\n\", pfp, pfpCaption)\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += abtMe[0] + \"\\n\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += abtMe[1] + \"\\n\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-3 --\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderArt() string {\n\tout := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tout += \"# Gno Art\\n\\n\"\n\n\tout += \"\u003cdiv class='columns-3'\u003e\"\n\n\tout += renderGnoFace()\n\tout += renderMillipede()\n\tout += \"Empty spot :/\"\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-3 --\u003e\\n\\n\"\n\n\tout += \"This art is dynamic; it will change with every new block.\\n\\n\"\n\tout += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\n\treturn out\n}\n\nfunc renderGnoFace() string {\n\tout := \"\u003cdiv\u003e\\n\\n\"\n\tout += gnoface.Render(strconv.Itoa(int(std.GetHeight())))\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderMillipede() string {\n\tout := \"\u003cdiv\u003e\\n\\n\"\n\tout += \"Millipede\\n\\n\"\n\tout += \"```\\n\" + millipede.Draw(int(std.GetHeight())%10+1) + \"```\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc isAuthorized(addr std.Address) bool {\n\treturn addr == config.Address() || addr == config.Backup()\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"config","path":"gno.land/r/manfred/config","files":[{"name":"config.gno","body":"package config\n\nimport \"std\"\n\nvar addr = std.Address(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\nfunc Addr() std.Address {\n\treturn addr\n}\n\nfunc UpdateAddr(newAddr std.Address) {\n\tAssertIsAdmin()\n\taddr = newAddr\n}\n\nfunc AssertIsAdmin() {\n\tif std.GetOrigCaller() != addr {\n\t\tpanic(\"restricted area\")\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"home","path":"gno.land/r/manfred/home","files":[{"name":"home.gno","body":"package home\n\nimport \"gno.land/r/manfred/config\"\n\nvar (\n\ttodos []string\n\tstatus string\n\tmemeImgURL string\n)\n\nfunc init() {\n\ttodos = append(todos, \"fill this todo list...\")\n\tstatus = \"Online\" // Initial status set to \"Online\"\n\tmemeImgURL = \"https://i.imgflip.com/7ze8dc.jpg\"\n}\n\nfunc Render(path string) string {\n\tcontent := \"# Manfred's (gn)home Dashboard\\n\\n\"\n\n\tcontent += \"## Meme\\n\"\n\tcontent += \"![](\" + memeImgURL + \")\\n\\n\"\n\n\tcontent += \"## Status\\n\"\n\tcontent += status + \"\\n\\n\"\n\n\tcontent += \"## Personal ToDo List\\n\"\n\tfor _, todo := range todos {\n\t\tcontent += \"- [ ] \" + todo + \"\\n\"\n\t}\n\tcontent += \"\\n\"\n\n\t// TODO: Implement a feature to list replies on r/boards on my posts\n\t// TODO: Maybe integrate a calendar feature for upcoming events?\n\n\treturn content\n}\n\nfunc AddNewTodo(todo string) {\n\tconfig.AssertIsAdmin()\n\ttodos = append(todos, todo)\n}\n\nfunc DeleteTodo(todoIndex int) {\n\tconfig.AssertIsAdmin()\n\tif todoIndex \u003e= 0 \u0026\u0026 todoIndex \u003c len(todos) {\n\t\t// Remove the todo from the list by merging slices from before and after the todo\n\t\ttodos = append(todos[:todoIndex], todos[todoIndex+1:]...)\n\t} else {\n\t\tpanic(\"Invalid todo index\")\n\t}\n}\n\nfunc UpdateStatus(newStatus string) {\n\tconfig.AssertIsAdmin()\n\tstatus = newStatus\n}\n"},{"name":"z1_filetest.gno","body":"package main\n\nimport \"gno.land/r/manfred/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// # Manfred's (gn)home Dashboard\n//\n// ## Meme\n// ![](https://i.imgflip.com/7ze8dc.jpg)\n//\n// ## Status\n// Online\n//\n// ## Personal ToDo List\n// - [ ] fill this todo list...\n"},{"name":"z2_filetest.gno","body":"package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/manfred/home\"\n)\n\nfunc main() {\n\tstd.TestSetOrigCaller(\"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\")\n\thome.AddNewTodo(\"aaa\")\n\thome.AddNewTodo(\"bbb\")\n\thome.AddNewTodo(\"ccc\")\n\thome.AddNewTodo(\"ddd\")\n\thome.AddNewTodo(\"eee\")\n\thome.UpdateStatus(\"Lorem Ipsum\")\n\thome.DeleteTodo(3)\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// # Manfred's (gn)home Dashboard\n//\n// ## Meme\n// ![](https://i.imgflip.com/7ze8dc.jpg)\n//\n// ## Status\n// Lorem Ipsum\n//\n// ## Personal ToDo List\n// - [ ] fill this todo list...\n// - [ ] aaa\n// - [ ] bbb\n// - [ ] ddd\n// - [ ] eee\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"present","path":"gno.land/r/manfred/present","files":[{"name":"admin.gno","body":"package present\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // XXX: delete instead?\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\tcaller := std.GetOrigCaller()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.GetOrigCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.GetOrigCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"present_miami23.gno","body":"package present\n\nfunc init() {\n\tpath := \"miami23\"\n\ttitle := \"Portal Loop Demo (Miami 2023)\"\n\tbody := `\nRendered by Gno.\n\n[Source (WIP)](https://github.com/gnolang/gno/pull/1176)\n\n## Portal Loop\n\n- DONE: Dynamic homepage, key pages, aliases, and redirects.\n- TODO: Deploy with history, complete worxdao v0.\n- Will replace the static gno.land site.\n- Enhances local development.\n\n[GitHub Issue](https://github.com/gnolang/gno/issues/1108)\n\n## Roadmap\n\n- Crafting the roadmap this week, open to collaboration.\n- Combining onchain (portal loop) and offchain (GitHub).\n- Next week: Unveiling the official v0 roadmap.\n\n## Teams, DAOs, Projects\n\n- Developing worxDAO contracts for directories of projects and teams.\n- GitHub teams and projects align with this structure.\n- CODEOWNER file updates coming.\n- Initial teams announced next week.\n\n## Tech Team Retreat Plan\n\n- Continue Portal Loop.\n- Consider dApp development.\n- Explore new topics [here](https://github.com/orgs/gnolang/projects/15/).\n- Engage in workshops.\n- Connect and have fun with colleagues.\n`\n\t_ = b.NewPost(adminAddr, path, title, body, \"2023-10-15T13:17:24Z\", []string{\"moul\"}, []string{\"demo\", \"portal-loop\", \"miami\"})\n}\n"},{"name":"present_miami23_filetest.gno","body":"package main\n\nimport (\n\t\"gno.land/r/manfred/present\"\n)\n\nfunc main() {\n\tprintln(present.Render(\"\"))\n\tprintln(\"------------------------------------\")\n\tprintln(present.Render(\"p/miami23\"))\n}\n"},{"name":"presentations.gno","body":"package present\n\nimport (\n\t\"gno.land/p/demo/blog\"\n)\n\n// TODO: switch from p/blog to p/present\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Manfred's Presentations\",\n\tPrefix: \"/r/manfred/present:\",\n\tNoBreadcrumb: true,\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"guestbook","path":"gno.land/r/morgan/guestbook","files":[{"name":"admin.gno","body":"package guestbook\n\nimport (\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nvar owner = ownable.New()\n\n// AdminDelete removes the guestbook message with the given ID.\n// The user will still be marked as having submitted a message, so they\n// won't be able to re-submit a new message.\nfunc AdminDelete(signatureID string) {\n\towner.AssertCallerIsOwner()\n\n\tid, err := seqid.FromString(signatureID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tidb := id.Binary()\n\tif !guestbook.Has(idb) {\n\t\tpanic(\"signature does not exist\")\n\t}\n\tguestbook.Remove(idb)\n}\n"},{"name":"guestbook.gno","body":"// Realm guestbook contains an implementation of a simple guestbook.\n// Come and sign yourself up!\npackage guestbook\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n)\n\n// Signature is a single entry in the guestbook.\ntype Signature struct {\n\tMessage string\n\tAuthor std.Address\n\tTime time.Time\n}\n\nconst (\n\tmaxMessageLength = 140\n\tmaxPerPage = 25\n)\n\nvar (\n\tsignatureID seqid.ID\n\tguestbook avl.Tree // id -\u003e Signature\n\thasSigned avl.Tree // address -\u003e struct{}\n)\n\nfunc init() {\n\tSign(\"You reached the end of the guestbook!\")\n}\n\nconst (\n\terrNotAUser = \"this guestbook can only be signed by users\"\n\terrAlreadySigned = \"you already signed the guestbook!\"\n\terrInvalidCharacterInMessage = \"invalid character in message\"\n)\n\n// Sign signs the guestbook, with the specified message.\nfunc Sign(message string) {\n\tprev := std.PrevRealm()\n\tswitch {\n\tcase !prev.IsUser():\n\t\tpanic(errNotAUser)\n\tcase hasSigned.Has(prev.Addr().String()):\n\t\tpanic(errAlreadySigned)\n\t}\n\tmessage = validateMessage(message)\n\n\tguestbook.Set(signatureID.Next().Binary(), Signature{\n\t\tMessage: message,\n\t\tAuthor: prev.Addr(),\n\t\t// NOTE: time.Now() will yield the \"block time\", which is deterministic.\n\t\tTime: time.Now(),\n\t})\n\thasSigned.Set(prev.Addr().String(), struct{}{})\n}\n\nfunc validateMessage(msg string) string {\n\tif len(msg) \u003e maxMessageLength {\n\t\tpanic(\"Keep it brief! (max \" + strconv.Itoa(maxMessageLength) + \" bytes!)\")\n\t}\n\tout := \"\"\n\tfor _, ch := range msg {\n\t\tswitch {\n\t\tcase unicode.IsLetter(ch),\n\t\t\tunicode.IsNumber(ch),\n\t\t\tunicode.IsSpace(ch),\n\t\t\tunicode.IsPunct(ch):\n\t\t\tout += string(ch)\n\t\tdefault:\n\t\t\tpanic(errInvalidCharacterInMessage)\n\t\t}\n\t}\n\treturn out\n}\n\nfunc Render(maxID string) string {\n\tvar bld strings.Builder\n\n\tbld.WriteString(\"# Guestbook 📝\\n\\n[Come sign the guestbook!](./guestbook$help\u0026func=Sign)\\n\\n---\\n\\n\")\n\n\tvar maxIDBinary string\n\tif maxID != \"\" {\n\t\tmid, err := seqid.FromString(maxID)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// AVL iteration is exclusive, so we need to decrease the ID value to get the \"true\" maximum.\n\t\tmid--\n\t\tmaxIDBinary = mid.Binary()\n\t}\n\n\tvar lastID seqid.ID\n\tvar printed int\n\tguestbook.ReverseIterate(\"\", maxIDBinary, func(key string, val interface{}) bool {\n\t\tsig := val.(Signature)\n\t\tmessage := strings.ReplaceAll(sig.Message, \"\\n\", \"\\n\u003e \")\n\t\tbld.WriteString(\"\u003e \" + message + \"\\n\u003e\\n\")\n\t\tidValue, ok := seqid.FromBinary(key)\n\t\tif !ok {\n\t\t\tpanic(\"invalid seqid id\")\n\t\t}\n\n\t\tbld.WriteString(\"\u003e _Written by \" + sig.Author.String() + \" at \" + sig.Time.Format(time.DateTime) + \"_ (#\" + idValue.String() + \")\\n\\n---\\n\\n\")\n\t\tlastID = idValue\n\n\t\tprinted++\n\t\t// stop after exceeding limit\n\t\treturn printed \u003e= maxPerPage\n\t})\n\n\tif printed == 0 {\n\t\tbld.WriteString(\"No messages!\")\n\t} else if printed \u003e= maxPerPage {\n\t\tbld.WriteString(\"\u003cp style='text-align:right'\u003e\u003ca href='./guestbook:\" + lastID.String() + \"'\u003eNext page\u003c/a\u003e\u003c/p\u003e\")\n\t}\n\n\treturn bld.String()\n}\n"},{"name":"guestbook_test.gno","body":"package guestbook\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\nfunc TestSign(t *testing.T) {\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user\"))\n\tSign(\"Hello!\")\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user2\"))\n\tSign(\"Hello2!\")\n\n\tres := Render(\"\")\n\tt.Log(res)\n\tif !strings.Contains(res, \"\u003e Hello!\\n\u003e\\n\u003e _Written by g1user \") {\n\t\tt.Error(\"does not contain first user's message\")\n\t}\n\tif !strings.Contains(res, \"\u003e Hello2!\\n\u003e\\n\u003e _Written by g1user2 \") {\n\t\tt.Error(\"does not contain second user's message\")\n\t}\n\tif guestbook.Size() != 2 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n}\n\nfunc TestSign_FromRealm(t *testing.T) {\n\tstd.TestSetRealm(std.NewCodeRealm(\"gno.land/r/demo/users\"))\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Fatal(\"not a string\", rec)\n\t\t} else if recString != errNotAUser {\n\t\t\tt.Fatal(\"invalid error\", recString)\n\t\t}\n\t}()\n\tSign(\"Hey!\")\n}\n\nfunc TestSign_Double(t *testing.T) {\n\t// Should not allow signing twice.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user\"))\n\tSign(\"Hello!\")\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"type assertion failed\", rec)\n\t\t} else if recString != errAlreadySigned {\n\t\t\tt.Error(\"invalid error message\", recString)\n\t\t}\n\t}()\n\n\tSign(\"Hello again!\")\n}\n\nfunc TestSign_InvalidMessage(t *testing.T) {\n\t// Should not allow control characters in message.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\tstd.TestSetRealm(std.NewUserRealm(\"g1user\"))\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"type assertion failed\", rec)\n\t\t} else if recString != errInvalidCharacterInMessage {\n\t\t\tt.Error(\"invalid error message\", recString)\n\t\t}\n\t}()\n\tSign(\"\\x00Hello!\")\n}\n\nfunc TestAdminDelete(t *testing.T) {\n\tconst (\n\t\tuserAddr std.Address = \"g1user\"\n\t\tadminAddr std.Address = \"g1admin\"\n\t)\n\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\towner = ownable.NewWithAddress(adminAddr)\n\tsignatureID = 0\n\n\tstd.TestSetRealm(std.NewUserRealm(userAddr))\n\n\tconst bad = \"Very Bad Message! Nyeh heh heh!\"\n\tSign(bad)\n\n\tif rnd := Render(\"\"); !strings.Contains(rnd, bad) {\n\t\tt.Fatal(\"render does not contain bad message\", rnd)\n\t}\n\n\tstd.TestSetRealm(std.NewUserRealm(adminAddr))\n\tAdminDelete(signatureID.String())\n\n\tif rnd := Render(\"\"); strings.Contains(rnd, bad) {\n\t\tt.Error(\"render contains bad message\", rnd)\n\t}\n\tif guestbook.Size() != 0 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n\tif hasSigned.Size() != 1 {\n\t\tt.Error(\"invalid hasSigned size\")\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"home","path":"gno.land/r/morgan/home","files":[{"name":"home.gno","body":"package home\n\nconst staticHome = `# morgan's (gn)home\n\n- [📝 sign my guestbook](/r/morgan/guestbook)\n`\n\nfunc Render(path string) string {\n\treturn staticHome\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"registry","path":"gno.land/r/stefann/registry","files":[{"name":"registry.gno","body":"package registry\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\nvar (\n\tmainAddr std.Address\n\tbackupAddr std.Address\n\towner *ownable.Ownable\n)\n\nfunc init() {\n\tmainAddr = \"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\"\n\tbackupAddr = \"g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8\"\n\n\towner = ownable.NewWithAddress(mainAddr)\n}\n\nfunc MainAddr() std.Address {\n\treturn mainAddr\n}\n\nfunc BackupAddr() std.Address {\n\treturn backupAddr\n}\n\nfunc SetMainAddr(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertCallerIsOwner()\n\n\tmainAddr = addr\n\treturn nil\n}\n\nfunc SetBackupAddr(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertCallerIsOwner()\n\n\tbackupAddr = addr\n\treturn nil\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"home","path":"gno.land/r/stefann/home","files":[{"name":"home.gno","body":"package home\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/stefann/registry\"\n)\n\ntype City struct {\n\tName string\n\tURL string\n}\n\ntype Sponsor struct {\n\tAddress std.Address\n\tAmount std.Coins\n}\n\ntype Profile struct {\n\tpfp string\n\taboutMe []string\n}\n\ntype Travel struct {\n\tcities []City\n\tcurrentCityIndex int\n\tjarLink string\n}\n\ntype Sponsorship struct {\n\tmaxSponsors int\n\tsponsors *avl.Tree\n\tDonationsCount int\n\tsponsorsCount int\n}\n\nvar (\n\tprofile Profile\n\ttravel Travel\n\tsponsorship Sponsorship\n\towner *ownable.Ownable\n)\n\nfunc init() {\n\towner = ownable.NewWithAddress(registry.MainAddr())\n\n\tprofile = Profile{\n\t\tpfp: \"https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg\",\n\t\taboutMe: []string{\n\t\t\t`### About Me`,\n\t\t\t`Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`,\n\n\t\t\t`### Contributions`,\n\t\t\t`I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`,\n\t\t},\n\t}\n\n\ttravel = Travel{\n\t\tcities: []City{\n\t\t\t{Name: \"Venice\", URL: \"https://i.ibb.co/1mcZ7b1/venice.jpg\"},\n\t\t\t{Name: \"Tokyo\", URL: \"https://i.ibb.co/wNDJv3H/tokyo.jpg\"},\n\t\t\t{Name: \"São Paulo\", URL: \"https://i.ibb.co/yWMq2Sn/sao-paulo.jpg\"},\n\t\t\t{Name: \"Toronto\", URL: \"https://i.ibb.co/pb95HJB/toronto.jpg\"},\n\t\t\t{Name: \"Bangkok\", URL: \"https://i.ibb.co/pQy3w2g/bangkok.jpg\"},\n\t\t\t{Name: \"New York\", URL: \"https://i.ibb.co/6JWLm0h/new-york.jpg\"},\n\t\t\t{Name: \"Paris\", URL: \"https://i.ibb.co/q9vf6Hs/paris.jpg\"},\n\t\t\t{Name: \"Kandersteg\", URL: \"https://i.ibb.co/60DzywD/kandersteg.jpg\"},\n\t\t\t{Name: \"Rothenburg\", URL: \"https://i.ibb.co/cr8d2rQ/rothenburg.jpg\"},\n\t\t\t{Name: \"Capetown\", URL: \"https://i.ibb.co/bPGn0v3/capetown.jpg\"},\n\t\t\t{Name: \"Sydney\", URL: \"https://i.ibb.co/TBNzqfy/sydney.jpg\"},\n\t\t\t{Name: \"Oeschinen Lake\", URL: \"https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg\"},\n\t\t\t{Name: \"Barra Grande\", URL: \"https://i.ibb.co/z4RXKc1/barra-grande.jpg\"},\n\t\t\t{Name: \"London\", URL: \"https://i.ibb.co/CPGtvgr/london.jpg\"},\n\t\t},\n\t\tcurrentCityIndex: 0,\n\t\tjarLink: \"https://TODO\", // This value should be injected through UpdateJarLink after deployment.\n\t}\n\n\tsponsorship = Sponsorship{\n\t\tmaxSponsors: 5,\n\t\tsponsors: avl.NewTree(),\n\t\tDonationsCount: 0,\n\t\tsponsorsCount: 0,\n\t}\n}\n\nfunc UpdateCities(newCities []City) {\n\towner.AssertCallerIsOwner()\n\ttravel.cities = newCities\n}\n\nfunc AddCities(newCities ...City) {\n\towner.AssertCallerIsOwner()\n\n\ttravel.cities = append(travel.cities, newCities...)\n}\n\nfunc UpdateJarLink(newLink string) {\n\towner.AssertCallerIsOwner()\n\ttravel.jarLink = newLink\n}\n\nfunc UpdatePFP(url string) {\n\towner.AssertCallerIsOwner()\n\tprofile.pfp = url\n}\n\nfunc UpdateAboutMe(aboutMeStr string) {\n\towner.AssertCallerIsOwner()\n\tprofile.aboutMe = strings.Split(aboutMeStr, \"|\")\n}\n\nfunc AddAboutMeRows(newRows ...string) {\n\towner.AssertCallerIsOwner()\n\n\tprofile.aboutMe = append(profile.aboutMe, newRows...)\n}\n\nfunc UpdateMaxSponsors(newMax int) {\n\towner.AssertCallerIsOwner()\n\tif newMax \u003c= 0 {\n\t\tpanic(\"maxSponsors must be greater than zero\")\n\t}\n\tsponsorship.maxSponsors = newMax\n}\n\nfunc Donate() {\n\taddress := std.GetOrigCaller()\n\tamount := std.GetOrigSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\texistingAmount, exists := sponsorship.sponsors.Get(address.String())\n\tif exists {\n\t\tupdatedAmount := existingAmount.(std.Coins).Add(amount)\n\t\tsponsorship.sponsors.Set(address.String(), updatedAmount)\n\t} else {\n\t\tsponsorship.sponsors.Set(address.String(), amount)\n\t\tsponsorship.sponsorsCount++\n\t}\n\n\ttravel.currentCityIndex++\n\tsponsorship.DonationsCount++\n\n\tbanker := std.GetBanker(std.BankerTypeRealmSend)\n\townerAddr := registry.MainAddr()\n\tbanker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr()))\n}\n\ntype SponsorSlice []Sponsor\n\nfunc (s SponsorSlice) Len() int {\n\treturn len(s)\n}\n\nfunc (s SponsorSlice) Less(i, j int) bool {\n\treturn s[i].Amount.AmountOf(\"ugnot\") \u003e s[j].Amount.AmountOf(\"ugnot\")\n}\n\nfunc (s SponsorSlice) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc GetTopSponsors() []Sponsor {\n\tvar sponsorSlice SponsorSlice\n\n\tsponsorship.sponsors.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\taddr := std.Address(key)\n\t\tamount := value.(std.Coins)\n\t\tsponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount})\n\t\treturn false\n\t})\n\n\tsort.Sort(sponsorSlice)\n\treturn sponsorSlice\n}\n\nfunc GetTotalDonations() int {\n\ttotal := 0\n\tsponsorship.sponsors.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttotal += int(value.(std.Coins).AmountOf(\"ugnot\"))\n\t\treturn false\n\t})\n\treturn total\n}\n\nfunc Render(path string) string {\n\tout := ufmt.Sprintf(\"# Exploring %s!\\n\\n\", travel.cities[travel.currentCityIndex].Name)\n\n\tout += renderAboutMe()\n\tout += \"\\n\\n\"\n\tout += renderTips()\n\n\treturn out\n}\n\nfunc renderAboutMe() string {\n\tout := \"\u003cdiv class='rows-3'\u003e\"\n\n\tout += \"\u003cdiv style='position: relative; text-align: center;'\u003e\\n\\n\"\n\n\tout += ufmt.Sprintf(\"\u003cdiv style='background-image: url(%s); background-size: cover; background-position: center; width: 100%%; height: 600px; position: relative; border-radius: 15px; overflow: hidden;'\u003e\\n\\n\", travel.cities[travel.currentCityIndex%len(travel.cities)].URL)\n\n\tout += ufmt.Sprintf(\"\u003cimg src='%s' alt='my profile pic' style='width: 250px; height: auto; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 50%%; border: 3px solid #1e1e1e; position: absolute; top: 75%%; left: 50%%; transform: translate(-50%%, -50%%);'\u003e\\n\\n\", profile.pfp)\n\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tfor _, rows := range profile.aboutMe {\n\t\tout += \"\u003cdiv\u003e\\n\\n\"\n\t\tout += rows + \"\\n\\n\"\n\t\tout += \"\u003c/div\u003e\\n\\n\"\n\t}\n\n\tout += \"\u003c/div\u003e\u003c!-- /rows-3 --\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderTips() string {\n\tout := `\u003cdiv class=\"jumbotron\" style=\"display: flex; flex-direction: column; justify-content: flex-start; align-items: center; padding-top: 40px; padding-bottom: 50px; text-align: center;\"\u003e` + \"\\n\\n\"\n\n\tout += `\u003cdiv class=\"rows-2\" style=\"max-width: 500px; width: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;\"\u003e` + \"\\n\"\n\n\tout += `\u003ch1 style=\"margin-bottom: 50px;\"\u003eHelp Me Travel The World\u003c/h1\u003e` + \"\\n\\n\"\n\n\tout += renderTipsJar() + \"\\n\"\n\n\tout += ufmt.Sprintf(`\u003cstrong style=\"font-size: 1.2em;\"\u003eI am currently in %s, \u003cbr\u003e tip the jar to send me somewhere else!\u003c/strong\u003e`, travel.cities[travel.currentCityIndex].Name)\n\n\tout += `\u003cbr\u003e\u003cspan style=\"font-size: 1.2em; font-style: italic; margin-top: 10px; display: inline-block;\"\u003eClick the jar, tip in GNOT coins, and watch my background change as I head to a new adventure!\u003c/span\u003e\u003c/p\u003e` + \"\\n\\n\"\n\n\tout += renderSponsors()\n\n\tout += `\u003c/div\u003e\u003c!-- /rows-2 --\u003e` + \"\\n\\n\"\n\n\tout += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\n\treturn out\n}\n\nfunc formatAddress(address string) string {\n\tif len(address) \u003c= 8 {\n\t\treturn address\n\t}\n\treturn address[:4] + \"...\" + address[len(address)-4:]\n}\n\nfunc renderSponsors() string {\n\tout := `\u003ch3 style=\"margin-top: 5px; margin-bottom: 20px\"\u003eSponsor Leaderboard\u003c/h3\u003e` + \"\\n\"\n\n\tif sponsorship.sponsorsCount == 0 {\n\t\treturn out + `\u003cp style=\"text-align: center;\"\u003eNo sponsors yet. Be the first to tip the jar!\u003c/p\u003e` + \"\\n\"\n\t}\n\n\ttopSponsors := GetTopSponsors()\n\tnumSponsors := len(topSponsors)\n\tif numSponsors \u003e sponsorship.maxSponsors {\n\t\tnumSponsors = sponsorship.maxSponsors\n\t}\n\n\tout += `\u003cul style=\"list-style-type: none; padding: 0; border: 1px solid #ddd; border-radius: 8px; width: 100%; max-width: 300px; margin: 0 auto;\"\u003e` + \"\\n\"\n\n\tfor i := 0; i \u003c numSponsors; i++ {\n\t\tsponsor := topSponsors[i]\n\t\tisLastItem := (i == numSponsors-1)\n\n\t\tpadding := \"10px 5px\"\n\t\tborder := \"border-bottom: 1px solid #ddd;\"\n\n\t\tif isLastItem {\n\t\t\tpadding = \"8px 5px\"\n\t\t\tborder = \"\"\n\t\t}\n\n\t\tout += ufmt.Sprintf(\n\t\t\t`\u003cli style=\"padding: %s; %s text-align: left;\"\u003e\n\t\t\t\t\u003cstrong style=\"padding-left: 5px;\"\u003e%d. %s\u003c/strong\u003e\n\t\t\t\t\u003cspan style=\"float: right; padding-right: 5px;\"\u003e%s\u003c/span\u003e\n\t\t\t\u003c/li\u003e`,\n\t\t\tpadding, border, i+1, formatAddress(sponsor.Address.String()), sponsor.Amount.String(),\n\t\t)\n\t}\n\n\treturn out\n}\n\nfunc renderTipsJar() string {\n\tout := ufmt.Sprintf(`\u003ca href=\"%s\" target=\"_blank\" style=\"display: block; text-decoration: none;\"\u003e`, travel.jarLink) + \"\\n\"\n\n\tout += `\u003cimg src=\"https://i.ibb.co/4TH9zbw/tips-jar.png\" alt=\"Tips Jar\" style=\"width: 300px; height: auto; display: block; margin: 0 auto;\"\u003e` + \"\\n\"\n\n\tout += `\u003c/a\u003e` + \"\\n\"\n\n\treturn out\n}\n"},{"name":"home_test.gno","body":"package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestUpdatePFP(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tprofile.pfp = \"\"\n\n\tUpdatePFP(\"https://example.com/pic.png\")\n\n\tif profile.pfp != \"https://example.com/pic.png\" {\n\t\tt.Fatalf(\"expected pfp to be https://example.com/pic.png, got %s\", profile.pfp)\n\t}\n}\n\nfunc TestUpdateAboutMe(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tprofile.aboutMe = []string{}\n\n\tUpdateAboutMe(\"This is my new bio.|I love coding!\")\n\n\texpected := []string{\"This is my new bio.\", \"I love coding!\"}\n\n\tif len(profile.aboutMe) != len(expected) {\n\t\tt.Fatalf(\"expected aboutMe to have length %d, got %d\", len(expected), len(profile.aboutMe))\n\t}\n\n\tfor i := range profile.aboutMe {\n\t\tif profile.aboutMe[i] != expected[i] {\n\t\t\tt.Fatalf(\"expected aboutMe[%d] to be %s, got %s\", i, expected[i], profile.aboutMe[i])\n\t\t}\n\t}\n}\n\nfunc TestUpdateCities(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\ttravel.cities = []City{}\n\n\tnewCities := []City{\n\t\t{Name: \"Berlin\", URL: \"https://example.com/berlin.jpg\"},\n\t\t{Name: \"Vienna\", URL: \"https://example.com/vienna.jpg\"},\n\t}\n\n\tUpdateCities(newCities)\n\n\tif len(travel.cities) != 2 {\n\t\tt.Fatalf(\"expected 2 cities, got %d\", len(travel.cities))\n\t}\n\n\tif travel.cities[0].Name != \"Berlin\" || travel.cities[1].Name != \"Vienna\" {\n\t\tt.Fatalf(\"expected cities to be updated to Berlin and Vienna, got %+v\", travel.cities)\n\t}\n}\n\nfunc TestUpdateJarLink(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\ttravel.jarLink = \"\"\n\n\tUpdateJarLink(\"https://example.com/jar\")\n\n\tif travel.jarLink != \"https://example.com/jar\" {\n\t\tt.Fatalf(\"expected jarLink to be https://example.com/jar, got %s\", travel.jarLink)\n\t}\n}\n\nfunc TestUpdateMaxSponsors(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tsponsorship.maxSponsors = 0\n\n\tUpdateMaxSponsors(10)\n\n\tif sponsorship.maxSponsors != 10 {\n\t\tt.Fatalf(\"expected maxSponsors to be 10, got %d\", sponsorship.maxSponsors)\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatalf(\"expected panic for setting maxSponsors to 0\")\n\t\t}\n\t}()\n\tUpdateMaxSponsors(0)\n}\n\nfunc TestAddCities(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\ttravel.cities = []City{}\n\n\tAddCities(City{Name: \"Berlin\", URL: \"https://example.com/berlin.jpg\"})\n\n\tif len(travel.cities) != 1 {\n\t\tt.Fatalf(\"expected 1 city, got %d\", len(travel.cities))\n\t}\n\tif travel.cities[0].Name != \"Berlin\" || travel.cities[0].URL != \"https://example.com/berlin.jpg\" {\n\t\tt.Fatalf(\"expected city to be Berlin, got %+v\", travel.cities[0])\n\t}\n\n\tAddCities(\n\t\tCity{Name: \"Paris\", URL: \"https://example.com/paris.jpg\"},\n\t\tCity{Name: \"Tokyo\", URL: \"https://example.com/tokyo.jpg\"},\n\t)\n\n\tif len(travel.cities) != 3 {\n\t\tt.Fatalf(\"expected 3 cities, got %d\", len(travel.cities))\n\t}\n\tif travel.cities[1].Name != \"Paris\" || travel.cities[2].Name != \"Tokyo\" {\n\t\tt.Fatalf(\"expected cities to be Paris and Tokyo, got %+v\", travel.cities[1:])\n\t}\n}\n\nfunc TestAddAboutMeRows(t *testing.T) {\n\tvar owner = std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\tstd.TestSetOrigCaller(owner)\n\n\tprofile.aboutMe = []string{}\n\n\tAddAboutMeRows(\"I love exploring new technologies!\")\n\n\tif len(profile.aboutMe) != 1 {\n\t\tt.Fatalf(\"expected 1 aboutMe row, got %d\", len(profile.aboutMe))\n\t}\n\tif profile.aboutMe[0] != \"I love exploring new technologies!\" {\n\t\tt.Fatalf(\"expected first aboutMe row to be 'I love exploring new technologies!', got %s\", profile.aboutMe[0])\n\t}\n\n\tAddAboutMeRows(\"Travel is my passion!\", \"Always learning.\")\n\n\tif len(profile.aboutMe) != 3 {\n\t\tt.Fatalf(\"expected 3 aboutMe rows, got %d\", len(profile.aboutMe))\n\t}\n\tif profile.aboutMe[1] != \"Travel is my passion!\" || profile.aboutMe[2] != \"Always learning.\" {\n\t\tt.Fatalf(\"expected aboutMe rows to be 'Travel is my passion!' and 'Always learning.', got %+v\", profile.aboutMe[1:])\n\t}\n}\n\nfunc TestDonate(t *testing.T) {\n\tvar user = testutils.TestAddress(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.DonationsCount = 0\n\tsponsorship.sponsorsCount = 0\n\ttravel.currentCityIndex = 0\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 500))\n\tstd.TestSetOrigSend(coinsSent, std.NewCoins())\n\tDonate()\n\n\texistingAmount, exists := sponsorship.sponsors.Get(string(user))\n\tif !exists {\n\t\tt.Fatalf(\"expected sponsor to be added, but it was not found\")\n\t}\n\n\tif existingAmount.(std.Coins).AmountOf(\"ugnot\") != 500 {\n\t\tt.Fatalf(\"expected donation amount to be 500ugnot, got %d\", existingAmount.(std.Coins).AmountOf(\"ugnot\"))\n\t}\n\n\tif sponsorship.DonationsCount != 1 {\n\t\tt.Fatalf(\"expected DonationsCount to be 1, got %d\", sponsorship.DonationsCount)\n\t}\n\n\tif sponsorship.sponsorsCount != 1 {\n\t\tt.Fatalf(\"expected sponsorsCount to be 1, got %d\", sponsorship.sponsorsCount)\n\t}\n\n\tif travel.currentCityIndex != 1 {\n\t\tt.Fatalf(\"expected currentCityIndex to be 1, got %d\", travel.currentCityIndex)\n\t}\n\n\tcoinsSent = std.NewCoins(std.NewCoin(\"ugnot\", 300))\n\tstd.TestSetOrigSend(coinsSent, std.NewCoins())\n\tDonate()\n\n\texistingAmount, exists = sponsorship.sponsors.Get(string(user))\n\tif !exists {\n\t\tt.Fatalf(\"expected sponsor to exist after second donation, but it was not found\")\n\t}\n\n\tif existingAmount.(std.Coins).AmountOf(\"ugnot\") != 800 {\n\t\tt.Fatalf(\"expected total donation amount to be 800ugnot, got %d\", existingAmount.(std.Coins).AmountOf(\"ugnot\"))\n\t}\n\n\tif sponsorship.DonationsCount != 2 {\n\t\tt.Fatalf(\"expected DonationsCount to be 2 after second donation, got %d\", sponsorship.DonationsCount)\n\t}\n\n\tif travel.currentCityIndex != 2 {\n\t\tt.Fatalf(\"expected currentCityIndex to be 2 after second donation, got %d\", travel.currentCityIndex)\n\t}\n}\n\nfunc TestGetTopSponsors(t *testing.T) {\n\tvar user = testutils.TestAddress(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.sponsorsCount = 0\n\n\tsponsorship.sponsors.Set(\"g1address1\", std.NewCoins(std.NewCoin(\"ugnot\", 300)))\n\tsponsorship.sponsors.Set(\"g1address2\", std.NewCoins(std.NewCoin(\"ugnot\", 500)))\n\tsponsorship.sponsors.Set(\"g1address3\", std.NewCoins(std.NewCoin(\"ugnot\", 200)))\n\tsponsorship.sponsorsCount = 3\n\n\ttopSponsors := GetTopSponsors()\n\n\tif len(topSponsors) != 3 {\n\t\tt.Fatalf(\"expected 3 sponsors, got %d\", len(topSponsors))\n\t}\n\n\tif topSponsors[0].Address.String() != \"g1address2\" || topSponsors[0].Amount.AmountOf(\"ugnot\") != 500 {\n\t\tt.Fatalf(\"expected top sponsor to be g1address2 with 500ugnot, got %s with %dugnot\", topSponsors[0].Address.String(), topSponsors[0].Amount.AmountOf(\"ugnot\"))\n\t}\n\n\tif topSponsors[1].Address.String() != \"g1address1\" || topSponsors[1].Amount.AmountOf(\"ugnot\") != 300 {\n\t\tt.Fatalf(\"expected second sponsor to be g1address1 with 300ugnot, got %s with %dugnot\", topSponsors[1].Address.String(), topSponsors[1].Amount.AmountOf(\"ugnot\"))\n\t}\n\n\tif topSponsors[2].Address.String() != \"g1address3\" || topSponsors[2].Amount.AmountOf(\"ugnot\") != 200 {\n\t\tt.Fatalf(\"expected third sponsor to be g1address3 with 200ugnot, got %s with %dugnot\", topSponsors[2].Address.String(), topSponsors[2].Amount.AmountOf(\"ugnot\"))\n\t}\n}\n\nfunc TestGetTotalDonations(t *testing.T) {\n\tvar user = testutils.TestAddress(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.sponsorsCount = 0\n\n\tsponsorship.sponsors.Set(\"g1address1\", std.NewCoins(std.NewCoin(\"ugnot\", 300)))\n\tsponsorship.sponsors.Set(\"g1address2\", std.NewCoins(std.NewCoin(\"ugnot\", 500)))\n\tsponsorship.sponsors.Set(\"g1address3\", std.NewCoins(std.NewCoin(\"ugnot\", 200)))\n\tsponsorship.sponsorsCount = 3\n\n\ttotalDonations := GetTotalDonations()\n\n\tif totalDonations != 1000 {\n\t\tt.Fatalf(\"expected total donations to be 1000ugnot, got %dugnot\", totalDonations)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\ttravel.currentCityIndex = 0\n\ttravel.cities = []City{\n\t\t{Name: \"Venice\", URL: \"https://example.com/venice.jpg\"},\n\t\t{Name: \"Paris\", URL: \"https://example.com/paris.jpg\"},\n\t}\n\n\toutput := Render(\"\")\n\n\texpectedCity := \"Venice\"\n\tif !strings.Contains(output, expectedCity) {\n\t\tt.Fatalf(\"expected output to contain city name '%s', got %s\", expectedCity, output)\n\t}\n\n\texpectedURL := \"https://example.com/venice.jpg\"\n\tif !strings.Contains(output, expectedURL) {\n\t\tt.Fatalf(\"expected output to contain city URL '%s', got %s\", expectedURL, output)\n\t}\n\n\ttravel.currentCityIndex = 1\n\toutput = Render(\"\")\n\n\texpectedCity = \"Paris\"\n\tif !strings.Contains(output, expectedCity) {\n\t\tt.Fatalf(\"expected output to contain city name '%s', got %s\", expectedCity, output)\n\t}\n\n\texpectedURL = \"https://example.com/paris.jpg\"\n\tif !strings.Contains(output, expectedURL) {\n\t\tt.Fatalf(\"expected output to contain city URL '%s', got %s\", expectedURL, output)\n\t}\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"rewards","path":"gno.land/r/sys/rewards","files":[{"name":"rewards.gno","body":"// This package will be used to manage proof-of-contributions on the exposed smart-contract side.\npackage rewards\n\n// TODO: write specs.\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} +{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"users","path":"gno.land/r/sys/users","files":[{"name":"verify.gno","body":"package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/r/demo/users\"\n)\n\nconst admin = \"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\" // @moul\n\ntype VerifyNameFunc func(enabled bool, address std.Address, name string) bool\n\nvar (\n\towner = ownable.NewWithAddress(admin) // Package owner\n\tcheckFunc = VerifyNameByUser // Checking namespace callback\n\tenabled = true // For now this package is disabled by default\n)\n\nfunc IsEnabled() bool { return enabled }\n\n// This method ensures that the given address has ownership of the given name.\nfunc IsAuthorizedAddressForName(address std.Address, name string) bool {\n\treturn checkFunc(enabled, address, name)\n}\n\n// VerifyNameByUser checks from the `users` package that the user has correctly\n// registered the given name.\n// This function considers as valid an `address` that matches the `name`.\nfunc VerifyNameByUser(enable bool, address std.Address, name string) bool {\n\tif !enable {\n\t\treturn true\n\t}\n\n\t// Allow user with their own address as name\n\tif address.String() == name {\n\t\treturn true\n\t}\n\n\tif user := users.GetUserByName(name); user != nil {\n\t\treturn user.Address == address\n\t}\n\n\treturn false\n}\n\n// Admin calls\n\n// Enable this package.\nfunc AdminEnable() {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tenabled = true\n}\n\n// Disable this package.\nfunc AdminDisable() {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tenabled = false\n}\n\n// AdminUpdateVerifyCall updates the method that verifies the namespace.\nfunc AdminUpdateVerifyCall(check VerifyNameFunc) {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tcheckFunc = check\n}\n\n// AdminTransferOwnership transfers the ownership to a new owner.\nfunc AdminTransferOwnership(newOwner std.Address) error {\n\tif err := owner.CallerIsOwner(); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn owner.TransferOwnership(newOwner)\n}\n"}]},"deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""}} From 60304df0d975f0e272a67437ca9556f9b96b3aad Mon Sep 17 00:00:00 2001 From: Blake <104744707+r3v4s@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:41:17 +0900 Subject: [PATCH 223/345] chore(p/grc721): Distinct Event Types for GRC721 Functions (#3102) # Description Current event(emit) code in p/grc721 really doesn't emits event. Therefore, modified code to emit the events. And similar to #2749, made event type for each function to be unique.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- .../gno.land/p/demo/grc/grc721/basic_nft.gno | 39 ++++++-- .../p/demo/grc/grc721/grc721_metadata.gno | 9 +- .../gno.land/p/demo/grc/grc721/igrc721.gno | 24 ++--- .../cmd/gnoland/testdata/grc721_emit.txtar | 95 +++++++++++++++++++ 4 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/grc721_emit.txtar diff --git a/examples/gno.land/p/demo/grc/grc721/basic_nft.gno b/examples/gno.land/p/demo/grc/grc721/basic_nft.gno index bec7338db42..0505aaa1c26 100644 --- a/examples/gno.land/p/demo/grc/grc721/basic_nft.gno +++ b/examples/gno.land/p/demo/grc/grc721/basic_nft.gno @@ -2,6 +2,7 @@ package grc721 import ( "std" + "strconv" "gno.land/p/demo/avl" "gno.land/p/demo/ufmt" @@ -120,8 +121,12 @@ func (s *basicNFT) Approve(to std.Address, tid TokenID) error { } s.tokenApprovals.Set(string(tid), to.String()) - event := ApprovalEvent{owner, to, tid} - emit(&event) + std.Emit( + ApprovalEvent, + "owner", string(owner), + "to", string(to), + "tokenId", string(tid), + ) return nil } @@ -219,8 +224,11 @@ func (s *basicNFT) Burn(tid TokenID) error { s.balances.Set(owner.String(), balance) s.owners.Remove(string(tid)) - event := TransferEvent{owner, zeroAddress, tid} - emit(&event) + std.Emit( + BurnEvent, + "from", string(owner), + "tokenId", string(tid), + ) s.afterTokenTransfer(owner, zeroAddress, tid, 1) @@ -238,8 +246,12 @@ func (s *basicNFT) setApprovalForAll(owner, operator std.Address, approved bool) key := owner.String() + ":" + operator.String() s.operatorApprovals.Set(key, approved) - event := ApprovalForAllEvent{owner, operator, approved} - emit(&event) + std.Emit( + ApprovalForAllEvent, + "owner", string(owner), + "to", string(operator), + "approved", strconv.FormatBool(approved), + ) return nil } @@ -291,8 +303,12 @@ func (s *basicNFT) transfer(from, to std.Address, tid TokenID) error { s.balances.Set(to.String(), toBalance) s.owners.Set(string(tid), to) - event := TransferEvent{from, to, tid} - emit(&event) + std.Emit( + TransferEvent, + "from", string(from), + "to", string(to), + "tokenId", string(tid), + ) s.afterTokenTransfer(from, to, tid, 1) @@ -324,8 +340,11 @@ func (s *basicNFT) mint(to std.Address, tid TokenID) error { s.balances.Set(to.String(), toBalance) s.owners.Set(string(tid), to) - event := TransferEvent{zeroAddress, to, tid} - emit(&event) + std.Emit( + MintEvent, + "to", string(to), + "tokenId", string(tid), + ) s.afterTokenTransfer(zeroAddress, to, tid, 1) diff --git a/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno b/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno index 360f73ed106..05fad41be18 100644 --- a/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno +++ b/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno @@ -85,9 +85,12 @@ func (s *metadataNFT) mint(to std.Address, tid TokenID) error { // Set owner of the token ID to the recipient address s.basicNFT.owners.Set(string(tid), to) - // Emit transfer event - event := TransferEvent{zeroAddress, to, tid} - emit(&event) + std.Emit( + TransferEvent, + "from", string(zeroAddress), + "to", string(to), + "tokenId", string(tid), + ) s.basicNFT.afterTokenTransfer(zeroAddress, to, tid, 1) diff --git a/examples/gno.land/p/demo/grc/grc721/igrc721.gno b/examples/gno.land/p/demo/grc/grc721/igrc721.gno index 387547a7e26..6c26c953d51 100644 --- a/examples/gno.land/p/demo/grc/grc721/igrc721.gno +++ b/examples/gno.land/p/demo/grc/grc721/igrc721.gno @@ -19,20 +19,10 @@ type ( TokenURI string ) -type TransferEvent struct { - From std.Address - To std.Address - TokenID TokenID -} - -type ApprovalEvent struct { - Owner std.Address - Approved std.Address - TokenID TokenID -} - -type ApprovalForAllEvent struct { - Owner std.Address - Operator std.Address - Approved bool -} +const ( + MintEvent = "Mint" + BurnEvent = "Burn" + TransferEvent = "Transfer" + ApprovalEvent = "Approval" + ApprovalForAllEvent = "ApprovalForAll" +) diff --git a/gno.land/cmd/gnoland/testdata/grc721_emit.txtar b/gno.land/cmd/gnoland/testdata/grc721_emit.txtar new file mode 100644 index 00000000000..9836e81a9be --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/grc721_emit.txtar @@ -0,0 +1,95 @@ +# Test for https://github.com/gnolang/gno/pull/3102 +loadpkg gno.land/p/demo/grc/grc721 +loadpkg gno.land/r/demo/users +loadpkg gno.land/r/foo721 $WORK/foo721 + +gnoland start + +# Mint +gnokey maketx call -pkgpath gno.land/r/foo721 -func Mint -args g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -args 1 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Mint\",\"attrs\":\[{\"key\":\"to\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno.land\/r\/foo721\",\"func\":\"mint\"}\]' + +# Approve +gnokey maketx call -pkgpath gno.land/r/foo721 -func Approve -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args 1 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Approval\",\"attrs\":\[{\"key\":\"owner\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno.land\/r\/foo721\",\"func\":\"Approve\"}\]' + +# SetApprovalForAll +gnokey maketx call -pkgpath gno.land/r/foo721 -func SetApprovalForAll -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args false -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"ApprovalForAll\",\"attrs\":\[{\"key\":\"owner\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"approved\",\"value\":\"false\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"setApprovalForAll\"}\]' + +# TransferFrom +gnokey maketx call -pkgpath gno.land/r/foo721 -func TransferFrom -args g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args 1 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Transfer\",\"attrs\":\[{\"key\":\"from\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"transfer\"}\]' + +# Burn +gnokey maketx call -pkgpath gno.land/r/foo721 -func Burn -args 1 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Burn\",\"attrs\":\[{\"key\":\"from\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"Burn\"}\]' + + +-- foo721/foo721.gno -- +package foo721 + +import ( + "std" + + "gno.land/p/demo/grc/grc721" + "gno.land/r/demo/users" + + pusers "gno.land/p/demo/users" +) + +var ( + admin std.Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + foo = grc721.NewBasicNFT("FooNFT", "FNFT") +) + +// Setters + +func Approve(user pusers.AddressOrName, tid grc721.TokenID) { + err := foo.Approve(users.Resolve(user), tid) + if err != nil { + panic(err) + } +} + +func SetApprovalForAll(user pusers.AddressOrName, approved bool) { + err := foo.SetApprovalForAll(users.Resolve(user), approved) + if err != nil { + panic(err) + } +} + +func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { + err := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid) + if err != nil { + panic(err) + } +} + +// Admin + +func Mint(to pusers.AddressOrName, tid grc721.TokenID) { + caller := std.PrevRealm().Addr() + assertIsAdmin(caller) + err := foo.Mint(users.Resolve(to), tid) + if err != nil { + panic(err) + } +} + +func Burn(tid grc721.TokenID) { + caller := std.PrevRealm().Addr() + assertIsAdmin(caller) + err := foo.Burn(tid) + if err != nil { + panic(err) + } +} + +// Util + +func assertIsAdmin(address std.Address) { + if address != admin { + panic("restricted access") + } +} From 36cdadb83e5bb11641e09c22a7ce2a9295e6c749 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:10:08 +0100 Subject: [PATCH 224/345] feat: add r/sys/params + params genesis support (#3003) - [x] add `r/sys/params` - [x] add `genesis/genesis_params.toml` - [x] port some existing configurations - [x] open issue: add LRU lazy caching with instant invalidation using a transient store (https://github.com/gnolang/gno/issues/3023) Depends on #2920 Depends on #3003 (cherry-picked) Blocking #2911 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- examples/gno.land/r/gov/dao/v2/dao.gno | 16 +- .../gno.land/r/gov/dao/v2/prop1_filetest.gno | 82 +++++++++ .../gno.land/r/gov/dao/v2/prop2_filetest.gno | 64 +++++++ .../gno.land/r/gov/dao/v2/prop3_filetest.gno | 67 +++++++- .../gno.land/r/gov/dao/v2/prop4_filetest.gno | 157 ++++++++++++++++++ examples/gno.land/r/sys/params/gno.mod | 6 + examples/gno.land/r/sys/params/params.gno | 54 ++++++ .../gno.land/r/sys/params/params_test.gno | 15 ++ .../cmd/gnoland/testdata/genesis_params.txtar | 14 ++ gno.land/genesis/genesis_params.toml | 29 ++++ gno.land/pkg/gnoland/app.go | 8 +- gno.land/pkg/gnoland/app_test.go | 28 ++++ gno.land/pkg/gnoland/genesis.go | 50 +++++- gno.land/pkg/gnoland/param.go | 121 ++++++++++++++ gno.land/pkg/gnoland/param_test.go | 41 +++++ gno.land/pkg/gnoland/types.go | 1 + .../pkg/integration/testing_integration.go | 1 + gno.land/pkg/integration/testing_node.go | 17 +- gno.land/pkg/sdk/vm/gas_test.go | 2 +- gno.land/pkg/sdk/vm/keeper.go | 12 +- gnovm/tests/file.go | 16 ++ gnovm/tests/files/std12_stdlibs.gno | 15 ++ tm2/pkg/sdk/params/handler_test.go | 45 +++-- tm2/pkg/sdk/params/keeper.go | 2 +- tm2/pkg/sdk/params/keeper_test.go | 10 +- 25 files changed, 833 insertions(+), 40 deletions(-) create mode 100644 examples/gno.land/r/gov/dao/v2/prop4_filetest.gno create mode 100644 examples/gno.land/r/sys/params/gno.mod create mode 100644 examples/gno.land/r/sys/params/params.gno create mode 100644 examples/gno.land/r/sys/params/params_test.gno create mode 100644 gno.land/cmd/gnoland/testdata/genesis_params.txtar create mode 100644 gno.land/genesis/genesis_params.toml create mode 100644 gno.land/pkg/gnoland/param.go create mode 100644 gno.land/pkg/gnoland/param_test.go create mode 100644 gnovm/tests/files/std12_stdlibs.gno diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index c37eda80bff..d99a161bcdf 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -16,15 +16,13 @@ var ( ) func init() { - var ( - // Example initial member set (just test addresses) - set = []membstore.Member{ - { - Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - VotingPower: 10, - }, - } - ) + // Example initial member set (just test addresses) + set := []membstore.Member{ + { + Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + VotingPower: 10, + }, + } // Set the member store members = membstore.NewMembStore(membstore.WithInitialMembers(set)) diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index 07d06bfe74d..e889dde4f48 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -126,3 +126,85 @@ func main() { // - #123: g12345678 (10) // - #123: g000000000 (10) // - #123: g000000000 (0) + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ValidatorAdded", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "addValidator" +// }, +// { +// "type": "ValidatorAdded", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "addValidator" +// }, +// { +// "type": "ValidatorRemoved", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "removeValidator" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 76e744a6fc8..bfd3f44f6dd 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -108,3 +108,67 @@ func main() { // ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao) // 13 Feb 2009 //
+ +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 0cd3bce6f83..546213431e4 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -5,6 +5,7 @@ import ( "gno.land/p/demo/dao" "gno.land/p/demo/membstore" + "gno.land/r/gov/dao/bridge" govdao "gno.land/r/gov/dao/v2" ) @@ -34,7 +35,7 @@ func init() { Executor: govdao.NewMemberPropExecutor(memberFn), } - govdao.Propose(prop) + bridge.GovDAO().Propose(prop) } func main() { @@ -118,3 +119,67 @@ func main() { // // -- // 4 + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno new file mode 100644 index 00000000000..c90e76727da --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -0,0 +1,157 @@ +package main + +import ( + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" + govdaov2 "gno.land/r/gov/dao/v2" + "gno.land/r/sys/params" +) + +func init() { + mExec := params.NewStringPropExecutor("prop1.string", "value1") + comment := "setting prop1.string param" + prop := dao.ProposalRequest{ + Description: comment, + Executor: mExec, + } + id := bridge.GovDAO().Propose(prop) + println("new prop", id) +} + +func main() { + println("--") + println(govdaov2.Render("")) + println("--") + println(govdaov2.Render("0")) + println("--") + bridge.GovDAO().VoteOnProposal(0, "YES") + println("--") + println(govdaov2.Render("0")) + println("--") + bridge.GovDAO().ExecuteProposal(0) + println("--") + println(govdaov2.Render("0")) +} + +// Output: +// new prop 0 +// -- +// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// setting prop1.string param +// +// Status: active +// +// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%) +// +// Threshold met: false +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// setting prop1.string param +// +// Status: accepted +// +// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// +// Threshold met: true +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// setting prop1.string param +// +// Status: execution successful +// +// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// +// Threshold met: true + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "set", +// "attrs": [ +// { +// "key": "k", +// "value": "prop1.string" +// } +// ], +// "pkg_path": "gno.land/r/sys/params", +// "func": "" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/sys/params/gno.mod b/examples/gno.land/r/sys/params/gno.mod new file mode 100644 index 00000000000..4b4c2bf790f --- /dev/null +++ b/examples/gno.land/r/sys/params/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/sys/params + +require ( + gno.land/p/demo/dao v0.0.0-latest + gno.land/r/gov/dao/bridge v0.0.0-latest +) diff --git a/examples/gno.land/r/sys/params/params.gno b/examples/gno.land/r/sys/params/params.gno new file mode 100644 index 00000000000..fa04c90de3f --- /dev/null +++ b/examples/gno.land/r/sys/params/params.gno @@ -0,0 +1,54 @@ +// Package params provides functions for creating parameter executors that +// interface with the Params Keeper. +// +// This package enables setting various parameter types (such as strings, +// integers, booleans, and byte slices) through the GovDAO proposal mechanism. +// Each function returns an executor that, when called, sets the specified +// parameter in the Params Keeper. +// +// The executors are designed to be used within governance proposals to modify +// parameters dynamically. The integration with the GovDAO allows for parameter +// changes to be proposed and executed in a controlled manner, ensuring that +// modifications are subject to governance processes. +// +// Example usage: +// +// executor := params.NewStringPropExecutor("exampleKey", "exampleValue") +// // This executor can be used in a governance proposal to set the parameter. +package params + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" +) + +func NewStringPropExecutor(key string, value string) dao.Executor { + return newPropExecutor(key, func() { std.SetParamString(key, value) }) +} + +func NewInt64PropExecutor(key string, value int64) dao.Executor { + return newPropExecutor(key, func() { std.SetParamInt64(key, value) }) +} + +func NewUint64PropExecutor(key string, value uint64) dao.Executor { + return newPropExecutor(key, func() { std.SetParamUint64(key, value) }) +} + +func NewBoolPropExecutor(key string, value bool) dao.Executor { + return newPropExecutor(key, func() { std.SetParamBool(key, value) }) +} + +func NewBytesPropExecutor(key string, value []byte) dao.Executor { + return newPropExecutor(key, func() { std.SetParamBytes(key, value) }) +} + +func newPropExecutor(key string, fn func()) dao.Executor { + callback := func() error { + fn() + std.Emit("set", "k", key) + return nil + } + return bridge.GovDAO().NewGovDAOExecutor(callback) +} diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno new file mode 100644 index 00000000000..eaa1ad039d3 --- /dev/null +++ b/examples/gno.land/r/sys/params/params_test.gno @@ -0,0 +1,15 @@ +package params + +import "testing" + +// Testing this package is limited because it only contains an `std.Set` method +// without a corresponding `std.Get` method. For comprehensive testing, refer to +// the tests located in the r/gov/dao/ directory, specifically in one of the +// propX_filetest.gno files. + +func TestNewStringPropExecutor(t *testing.T) { + executor := NewStringPropExecutor("foo", "bar") + if executor == nil { + t.Errorf("executor shouldn't be nil") + } +} diff --git a/gno.land/cmd/gnoland/testdata/genesis_params.txtar b/gno.land/cmd/gnoland/testdata/genesis_params.txtar new file mode 100644 index 00000000000..43ecd8ccacb --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/genesis_params.txtar @@ -0,0 +1,14 @@ +# test for https://github.com/gnolang/gno/pull/3003 + +gnoland start + +gnokey query params/vm/gno.land/r/sys/params.test.foo.string +stdout 'data: "bar"$' +gnokey query params/vm/gno.land/r/sys/params.test.foo.int64 +stdout 'data: "-1337"' +gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64 +stdout 'data: "42"' +gnokey query params/vm/gno.land/r/sys/params.test.foo.bool +stdout 'data: true' +# XXX: bytes + diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml new file mode 100644 index 00000000000..5f4d9c5015c --- /dev/null +++ b/gno.land/genesis/genesis_params.toml @@ -0,0 +1,29 @@ + +## gno.land +["gno.land/r/sys/params.sys"] + users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support. + # TODO: validators_pkgpath.string = "gno.land/r/sys/validators" + # TODO: rewards_pkgpath.string = "gno.land/r/sys/rewards" + # TODO: token_lock.bool = true + +## gnovm +["gno.land/r/sys/params.vm"] + # TODO: chain_domain.string = "gno.land" + # TODO: max_gas.int64 = 100_000_000 + # TODO: chain_tz.string = "UTC" + # TODO: default_storage_allowance.string = "" + +## tm2 +["gno.land/r/sys/params.tm2"] + +## misc +["gno.land/r/sys/params.misc"] + +## testing +# do not remove these lines. they are needed for a txtar integration test. +["gno.land/r/sys/params.test"] + foo.string = "bar" + foo.int64 = -1337 + foo.uint64 = 42 + foo.bool = true + #foo.bytes = todo diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index d29ae3fd181..e0c93f6194f 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -294,7 +294,7 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci return nil, fmt.Errorf("invalid AppState of type %T", appState) } - // Parse and set genesis state balances + // Apply genesis balances. for _, bal := range state.Balances { acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) cfg.acctKpr.SetAccount(ctx, acc) @@ -304,6 +304,12 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci } } + // Apply genesis params. + for _, param := range state.Params { + param.register(ctx, cfg.paramsKpr) + } + + // Replay genesis txs. txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) // Run genesis txs diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 5e4dcb2b449..999e04b2c4b 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -66,6 +66,13 @@ func TestNewAppWithOptions(t *testing.T) { }, }, }, + Params: []Param{ + {key: "foo", kind: "string", value: "hello"}, + {key: "foo", kind: "int64", value: int64(-42)}, + {key: "foo", kind: "uint64", value: uint64(1337)}, + {key: "foo", kind: "bool", value: true}, + {key: "foo", kind: "bytes", value: []byte{0x48, 0x69, 0x21}}, + }, }, }) require.True(t, resp.IsOK(), "InitChain response: %v", resp) @@ -87,6 +94,27 @@ func TestNewAppWithOptions(t *testing.T) { Tx: tx, }) require.True(t, dtxResp.IsOK(), "DeliverTx response: %v", dtxResp) + + cres := bapp.Commit() + require.NotNil(t, cres) + + tcs := []struct { + path string + expectedVal string + }{ + {"params/vm/foo.string", `"hello"`}, + {"params/vm/foo.int64", `"-42"`}, + {"params/vm/foo.uint64", `"1337"`}, + {"params/vm/foo.bool", `true`}, + {"params/vm/foo.bytes", `"SGkh"`}, // XXX: make this test more readable + } + for _, tc := range tcs { + qres := bapp.Query(abci.RequestQuery{ + Path: tc.path, + }) + require.True(t, qres.IsOK()) + assert.Equal(t, qres.Data, []byte(tc.expectedVal)) + } } func TestNewAppWithOptions_ErrNoDB(t *testing.T) { diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index 613fba51c37..ea692bcaf0d 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -13,12 +13,16 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/pelletier/go-toml" ) // LoadGenesisBalancesFile loads genesis balances from the provided file path. func LoadGenesisBalancesFile(path string) ([]Balance, error) { // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot - content := osm.MustReadFile(path) + content, err := osm.ReadFile(path) + if err != nil { + return nil, err + } lines := strings.Split(string(content), "\n") balances := make([]Balance, 0, len(lines)) @@ -58,12 +62,54 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { return balances, nil } +// LoadGenesisParamsFile loads genesis params from the provided file path. +func LoadGenesisParamsFile(path string) ([]Param, error) { + // each param is in the form: key.kind=value + content, err := osm.ReadFile(path) + if err != nil { + return nil, err + } + + m := map[string] /*category*/ map[string] /*key*/ map[string] /*kind*/ interface{} /*value*/ {} + err = toml.Unmarshal(content, &m) + if err != nil { + return nil, err + } + + params := make([]Param, 0) + for category, keys := range m { + for key, kinds := range keys { + for kind, val := range kinds { + param := Param{ + key: category + "." + key, + kind: kind, + } + switch kind { + case "uint64": // toml + param.value = uint64(val.(int64)) + default: + param.value = val + } + if err := param.Verify(); err != nil { + return nil, err + } + params = append(params, param) + } + } + } + + return params, nil +} + // LoadGenesisTxsFile loads genesis transactions from the provided file path. // XXX: Improve the way we generate and load this file func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]TxWithMetadata, error) { txs := make([]TxWithMetadata, 0) - txsBz := osm.MustReadFile(path) + txsBz, err := osm.ReadFile(path) + if err != nil { + return nil, err + } txsLines := strings.Split(string(txsBz), "\n") for _, txLine := range txsLines { if txLine == "" { diff --git a/gno.land/pkg/gnoland/param.go b/gno.land/pkg/gnoland/param.go new file mode 100644 index 00000000000..4c1e1190751 --- /dev/null +++ b/gno.land/pkg/gnoland/param.go @@ -0,0 +1,121 @@ +package gnoland + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/params" +) + +type Param struct { + key string + kind string + value interface{} +} + +func (p Param) Verify() error { + // XXX: validate + return nil +} + +const ( + ParamKindString = "string" + ParamKindInt64 = "int64" + ParamKindUint64 = "uint64" + ParamKindBool = "bool" + ParamKindBytes = "bytes" +) + +func (p *Param) Parse(entry string) error { + parts := strings.SplitN(strings.TrimSpace(entry), "=", 2) // .= + if len(parts) != 2 { + return fmt.Errorf("malformed entry: %q", entry) + } + + keyWithKind := parts[0] + rawValue := parts[1] + p.kind = keyWithKind[strings.LastIndex(keyWithKind, ".")+1:] + p.key = strings.TrimSuffix(keyWithKind, "."+p.kind) + switch p.kind { + case ParamKindString: + p.value = rawValue + case ParamKindInt64: + v, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + return err + } + p.value = v + case ParamKindBool: + v, err := strconv.ParseBool(rawValue) + if err != nil { + return err + } + p.value = v + case ParamKindUint64: + v, err := strconv.ParseUint(rawValue, 10, 64) + if err != nil { + return err + } + p.value = v + case ParamKindBytes: + v, err := hex.DecodeString(rawValue) + if err != nil { + return err + } + p.value = v + default: + return errors.New("unsupported param kind: " + p.kind + " (" + entry + ")") + } + + return p.Verify() +} + +func (p Param) String() string { + typedKey := p.key + "." + p.kind + switch p.kind { + case ParamKindString: + return fmt.Sprintf("%s=%s", typedKey, p.value) + case ParamKindInt64: + return fmt.Sprintf("%s=%d", typedKey, p.value) + case ParamKindUint64: + return fmt.Sprintf("%s=%d", typedKey, p.value) + case ParamKindBool: + if p.value.(bool) { + return fmt.Sprintf("%s=true", typedKey) + } + return fmt.Sprintf("%s=false", typedKey) + case ParamKindBytes: + return fmt.Sprintf("%s=%x", typedKey, p.value) + } + panic("invalid param kind:" + p.kind) +} + +func (p *Param) UnmarshalAmino(rep string) error { + return p.Parse(rep) +} + +func (p Param) MarshalAmino() (string, error) { + return p.String(), nil +} + +func (p Param) register(ctx sdk.Context, prk params.ParamsKeeperI) { + key := p.key + "." + p.kind + switch p.kind { + case ParamKindString: + prk.SetString(ctx, key, p.value.(string)) + case ParamKindInt64: + prk.SetInt64(ctx, key, p.value.(int64)) + case ParamKindUint64: + prk.SetUint64(ctx, key, p.value.(uint64)) + case ParamKindBool: + prk.SetBool(ctx, key, p.value.(bool)) + case ParamKindBytes: + prk.SetBytes(ctx, key, p.value.([]byte)) + default: + panic("invalid param kind: " + p.kind) + } +} diff --git a/gno.land/pkg/gnoland/param_test.go b/gno.land/pkg/gnoland/param_test.go new file mode 100644 index 00000000000..5d17aab40da --- /dev/null +++ b/gno.land/pkg/gnoland/param_test.go @@ -0,0 +1,41 @@ +package gnoland + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParam_Parse(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry string + expected Param + expectErr bool + }{ + {"valid string", "foo.string=hello", Param{key: "foo", kind: "string", value: "hello"}, false}, + {"valid int64", "foo.int64=-1337", Param{key: "foo", kind: "int64", value: int64(-1337)}, false}, + {"valid uint64", "foo.uint64=42", Param{key: "foo", kind: "uint64", value: uint64(42)}, false}, + {"valid bool", "foo.bool=true", Param{key: "foo", kind: "bool", value: true}, false}, + {"valid bytes", "foo.bytes=AAAA", Param{key: "foo", kind: "bytes", value: []byte{0xaa, 0xaa}}, false}, + {"invalid key", "invalidkey=foo", Param{}, true}, + {"invalid kind", "invalid.kind=foo", Param{}, true}, + {"invalid int64", "invalid.int64=foobar", Param{}, true}, + {"invalid uint64", "invalid.uint64=-42", Param{}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + param := Param{} + err := param.Parse(tc.entry) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, param) + } + }) + } +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 6bc4417d8e0..a5f76fdcef7 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -27,6 +27,7 @@ func ProtoGnoAccount() std.Account { type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []TxWithMetadata `json:"txs"` + Params []Param `json:"params"` } type TxWithMetadata struct { diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 56b298e4541..235b9581ae0 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -134,6 +134,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // This genesis will be use when node is started. genesis := &gnoland.GnoGenesisState{ Balances: LoadDefaultGenesisBalanceFile(t, gnoRootDir), + Params: LoadDefaultGenesisParamFile(t, gnoRootDir), Txs: []gnoland.TxWithMetadata{}, } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index ef7e05d0787..7e34049d352 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -59,6 +59,7 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + params := LoadDefaultGenesisParamFile(t, gnoroot) balances := LoadDefaultGenesisBalanceFile(t, gnoroot) txs := make([]gnoland.TxWithMetadata, 0) txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) @@ -67,6 +68,7 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW cfg.Genesis.AppState = gnoland.GnoGenesisState{ Balances: balances, Txs: txs, + Params: params, } return cfg, creator @@ -118,10 +120,11 @@ func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey Balances: []gnoland.Balance{ { Address: crypto.MustAddressFromString(DefaultAccount_Address), - Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), + Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), }, }, - Txs: []gnoland.TxWithMetadata{}, + Txs: []gnoland.TxWithMetadata{}, + Params: []gnoland.Param{}, }, } } @@ -147,6 +150,16 @@ func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balanc return genesisBalances } +// LoadDefaultGenesisParamFile loads the default genesis balance file for testing. +func LoadDefaultGenesisParamFile(t TestingTS, gnoroot string) []gnoland.Param { + paramFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_params.toml") + + genesisParams, err := gnoland.LoadGenesisParamsFile(paramFile) + require.NoError(t, err) + + return genesisParams +} + // LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing. func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []gnoland.TxWithMetadata { txsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl") diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index ff924610627..3a11d97c235 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -75,7 +75,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(93825), gasDeliver) + assert.Equal(t, int64(92825), gasDeliver) } // Enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 5921e3eb3bb..5fa2075b8f7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -228,15 +228,15 @@ func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore // Namespace can be either a user or crypto address. var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) -const ( - sysUsersPkgParamKey = "vm/gno.land/r/sys/params.string" - sysUsersPkgDefault = "gno.land/r/sys/users" -) +const sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { - sysUsersPkg := sysUsersPkgDefault - vm.prmk.GetString(ctx, sysUsersPkgParamKey, &sysUsersPkg) + var sysUsersPkg string + vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) + if sysUsersPkg == "" { + return nil + } store := vm.getGnoTransactionStore(ctx) diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 98e54114af9..98dbab6ac0e 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -58,6 +58,7 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(send) // >= send. banker := newTestBanker(pkgAddr.Bech32(), pkgCoins) + params := newTestParams() ctx := stdlibs.ExecContext{ ChainID: "dev", Height: 123, @@ -68,6 +69,7 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { OrigSend: send, OrigSendSpent: new(std.Coins), Banker: banker, + Params: params, EventLogger: sdk.NewEventLogger(), } return &teststd.TestExecContext{ @@ -632,6 +634,20 @@ func trimTrailingSpaces(result string) string { return strings.Join(lines, "\n") } +// ---------------------------------------- +// testParams +type testParams struct{} + +func newTestParams() *testParams { + return &testParams{} +} + +func (tp *testParams) SetBool(key string, val bool) { /* noop */ } +func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ } +func (tp *testParams) SetInt64(key string, val int64) { /* noop */ } +func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ } +func (tp *testParams) SetString(key string, val string) { /* noop */ } + // ---------------------------------------- // testBanker diff --git a/gnovm/tests/files/std12_stdlibs.gno b/gnovm/tests/files/std12_stdlibs.gno new file mode 100644 index 00000000000..a06fd841f91 --- /dev/null +++ b/gnovm/tests/files/std12_stdlibs.gno @@ -0,0 +1,15 @@ +package main + +import "std" + +func main() { + std.SetParamString("foo.string", "hello") + std.SetParamInt64("bar.int64", -12345) + std.SetParamUint64("baz.uint64", 12345) + std.SetParamBool("oof.bool", true) + std.SetParamBytes("rab.bytes", []byte("world")) + println("done") +} + +// Output: +// done diff --git a/tm2/pkg/sdk/params/handler_test.go b/tm2/pkg/sdk/params/handler_test.go index 1fff5d007d3..071eb12b52b 100644 --- a/tm2/pkg/sdk/params/handler_test.go +++ b/tm2/pkg/sdk/params/handler_test.go @@ -4,12 +4,12 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/sdk" tu "github.com/gnolang/gno/tm2/pkg/sdk/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestInvalidMsg(t *testing.T) { @@ -27,21 +27,42 @@ func TestQuery(t *testing.T) { env := setupTestEnv() h := NewHandler(env.keeper) - req := abci.RequestQuery{ - Path: "params/params_test/foo/bar.string", + tcs := []struct { + path string + expected string + }{ + {path: "params/params_test/foo/bar.string", expected: `"baz"`}, + {path: "params/params_test/foo/bar.int64", expected: `"-12345"`}, + {path: "params/params_test/foo/bar.uint64", expected: `"4242"`}, + {path: "params/params_test/foo/bar.bool", expected: "true"}, + {path: "params/params_test/foo/bar.bytes", expected: `"YmF6"`}, } - res := h.Query(env.ctx, req) - require.Nil(t, res.Error) - require.NotNil(t, res) - require.Nil(t, res.Data) + for _, tc := range tcs { + req := abci.RequestQuery{ + Path: tc.path, + } + res := h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + require.Nil(t, res.Data) + } env.keeper.SetString(env.ctx, "foo/bar.string", "baz") + env.keeper.SetInt64(env.ctx, "foo/bar.int64", -12345) + env.keeper.SetUint64(env.ctx, "foo/bar.uint64", 4242) + env.keeper.SetBool(env.ctx, "foo/bar.bool", true) + env.keeper.SetBytes(env.ctx, "foo/bar.bytes", []byte("baz")) - res = h.Query(env.ctx, req) - require.Nil(t, res.Error) - require.NotNil(t, res) - require.Equal(t, string(res.Data), `"baz"`) + for _, tc := range tcs { + req := abci.RequestQuery{ + Path: tc.path, + } + res := h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + assert.Equal(t, string(res.Data), tc.expected) + } } func TestQuerierRouteNotFound(t *testing.T) { diff --git a/tm2/pkg/sdk/params/keeper.go b/tm2/pkg/sdk/params/keeper.go index ffeb1775acb..523e8d54f69 100644 --- a/tm2/pkg/sdk/params/keeper.go +++ b/tm2/pkg/sdk/params/keeper.go @@ -152,6 +152,6 @@ func checkSuffix(key, expectedSuffix string) { // XXX: additional sanity checks? ) if noSuffix || noName { - panic(`key should be like "` + expectedSuffix + `"`) + panic(`key should be like "` + expectedSuffix + `" (` + key + `)`) } } diff --git a/tm2/pkg/sdk/params/keeper_test.go b/tm2/pkg/sdk/params/keeper_test.go index 45a97ae44ad..832d16229ee 100644 --- a/tm2/pkg/sdk/params/keeper_test.go +++ b/tm2/pkg/sdk/params/keeper_test.go @@ -78,11 +78,11 @@ func TestKeeper(t *testing.T) { require.Equal(t, param5, []byte("bye")) // invalid sets - require.PanicsWithValue(t, `key should be like ".string"`, func() { keeper.SetString(ctx, "invalid.int64", "hello") }) - require.PanicsWithValue(t, `key should be like ".int64"`, func() { keeper.SetInt64(ctx, "invalid.string", int64(42)) }) - require.PanicsWithValue(t, `key should be like ".uint64"`, func() { keeper.SetUint64(ctx, "invalid.int64", uint64(42)) }) - require.PanicsWithValue(t, `key should be like ".bool"`, func() { keeper.SetBool(ctx, "invalid.int64", true) }) - require.PanicsWithValue(t, `key should be like ".bytes"`, func() { keeper.SetBytes(ctx, "invalid.int64", []byte("hello")) }) + require.PanicsWithValue(t, `key should be like ".string" (invalid.int64)`, func() { keeper.SetString(ctx, "invalid.int64", "hello") }) + require.PanicsWithValue(t, `key should be like ".int64" (invalid.string)`, func() { keeper.SetInt64(ctx, "invalid.string", int64(42)) }) + require.PanicsWithValue(t, `key should be like ".uint64" (invalid.int64)`, func() { keeper.SetUint64(ctx, "invalid.int64", uint64(42)) }) + require.PanicsWithValue(t, `key should be like ".bool" (invalid.int64)`, func() { keeper.SetBool(ctx, "invalid.int64", true) }) + require.PanicsWithValue(t, `key should be like ".bytes" (invalid.int64)`, func() { keeper.SetBytes(ctx, "invalid.int64", []byte("hello")) }) } // adapted from TestKeeperSubspace from Cosmos SDK, but adapted to a subspace-less Keeper. From 3bb666c0bc6538418f0fae4cec784b734b20b62c Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:41:48 +0100 Subject: [PATCH 225/345] feat(examples): grc20 refactor (#2983) This PR extracts the grc20 refactor from #2551, which is a meta PR containing several contract improvements and additions that depend on new Gnovm features that haven't been merged yet. Please review this grc20 refactor with a focus on its API. Several valuable comments can be found in #2551. Additionally, you can discover new contracts using grc20 in #2551, such as `minidex`, `atomicswap`, `grc20reg`, `test20`, and `vault`. Addresses #1832 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Morgan Co-authored-by: Morgan Bazalgette Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- examples/gno.land/p/demo/fqname/fqname.gno | 4 +- .../gno.land/p/demo/fqname/fqname_test.gno | 4 +- examples/gno.land/p/demo/grc/grc20/banker.gno | 215 --------------- .../gno.land/p/demo/grc/grc20/banker_test.gno | 51 ---- .../p/demo/grc/grc20/examples_test.gno | 18 ++ examples/gno.land/p/demo/grc/grc20/gno.mod | 1 + examples/gno.land/p/demo/grc/grc20/mock.gno | 3 + .../gno.land/p/demo/grc/grc20/tellers.gno | 139 ++++++++++ .../p/demo/grc/grc20/tellers_test.gno | 130 ++++++++++ examples/gno.land/p/demo/grc/grc20/token.gno | 245 ++++++++++++++++-- .../gno.land/p/demo/grc/grc20/token_test.gno | 125 +++++---- examples/gno.land/p/demo/grc/grc20/types.gno | 61 ++++- examples/gno.land/r/demo/bar20/bar20.gno | 11 +- examples/gno.land/r/demo/bar20/bar20_test.gno | 4 +- examples/gno.land/r/demo/foo20/foo20.gno | 44 ++-- examples/gno.land/r/demo/foo20/foo20_test.gno | 12 +- examples/gno.land/r/demo/grc20factory/gno.mod | 1 + .../r/demo/grc20factory/grc20factory.gno | 61 +++-- .../r/demo/grc20factory/grc20factory_test.gno | 76 +++--- examples/gno.land/r/demo/wugnot/wugnot.gno | 25 +- 20 files changed, 780 insertions(+), 450 deletions(-) delete mode 100644 examples/gno.land/p/demo/grc/grc20/banker.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/banker_test.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/examples_test.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/mock.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/tellers.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/tellers_test.gno diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno index d28453e5c1b..8cccdb9e8b7 100644 --- a/examples/gno.land/p/demo/fqname/fqname.gno +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -43,9 +43,9 @@ func Parse(fqname string) (pkgpath, name string) { // Construct a qualified identifier. // -// fqName := fqname.Construct("gno.land/r/demo/foo20", "GRC20") +// fqName := fqname.Construct("gno.land/r/demo/foo20", "Token") // fmt.Println("Fully Qualified Name:", fqName) -// // Output: gno.land/r/demo/foo20.GRC20 +// // Output: gno.land/r/demo/foo20.Token func Construct(pkgpath, name string) string { // TODO: ensure pkgpath is valid - and as such last part does not contain a dot. if name == "" { diff --git a/examples/gno.land/p/demo/fqname/fqname_test.gno b/examples/gno.land/p/demo/fqname/fqname_test.gno index 55a220776be..5f0f83968a3 100644 --- a/examples/gno.land/p/demo/fqname/fqname_test.gno +++ b/examples/gno.land/p/demo/fqname/fqname_test.gno @@ -36,7 +36,7 @@ func TestConstruct(t *testing.T) { name string expected string }{ - {"gno.land/r/demo/foo20", "GRC20", "gno.land/r/demo/foo20.GRC20"}, + {"gno.land/r/demo/foo20", "Token", "gno.land/r/demo/foo20.Token"}, {"gno.land/r/demo/foo20", "", "gno.land/r/demo/foo20"}, {"path", "", "path"}, {"path", "Split", "path.Split"}, @@ -62,7 +62,7 @@ func TestRenderLink(t *testing.T) { {"gno.land/p/demo/avl", "", "[gno.land/p/demo/avl](/p/demo/avl)"}, {"github.com/a/b", "C", "github.com/a/b.C"}, {"example.com/pkg", "Func", "example.com/pkg.Func"}, - {"gno.land/r/demo/foo20", "GRC20", "[gno.land/r/demo/foo20](/r/demo/foo20).GRC20"}, + {"gno.land/r/demo/foo20", "Token", "[gno.land/r/demo/foo20](/r/demo/foo20).Token"}, {"gno.land/r/demo/foo20", "", "[gno.land/r/demo/foo20](/r/demo/foo20)"}, {"", "", ""}, } diff --git a/examples/gno.land/p/demo/grc/grc20/banker.gno b/examples/gno.land/p/demo/grc/grc20/banker.gno deleted file mode 100644 index 7a3ebb18ef5..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/banker.gno +++ /dev/null @@ -1,215 +0,0 @@ -package grc20 - -import ( - "std" - "strconv" - - "gno.land/p/demo/avl" - "gno.land/p/demo/ufmt" -) - -// Banker implements a token banker with admin privileges. -// -// The Banker is intended to be used in two main ways: -// 1. as a temporary object used to make the initial minting, then deleted. -// 2. preserved in an unexported variable to support conditional administrative -// tasks protected by the contract. -type Banker struct { - name string - symbol string - decimals uint - totalSupply uint64 - balances avl.Tree // std.Address(owner) -> uint64 - allowances avl.Tree // string(owner+":"+spender) -> uint64 - token *token // to share the same pointer -} - -func NewBanker(name, symbol string, decimals uint) *Banker { - if name == "" { - panic("name should not be empty") - } - if symbol == "" { - panic("symbol should not be empty") - } - // XXX additional checks (length, characters, limits, etc) - - b := Banker{ - name: name, - symbol: symbol, - decimals: decimals, - } - t := &token{banker: &b} - b.token = t - return &b -} - -func (b Banker) Token() Token { return b.token } // Token returns a grc20 safe-object implementation. -func (b Banker) GetName() string { return b.name } -func (b Banker) GetSymbol() string { return b.symbol } -func (b Banker) GetDecimals() uint { return b.decimals } -func (b Banker) TotalSupply() uint64 { return b.totalSupply } -func (b Banker) KnownAccounts() int { return b.balances.Size() } - -func (b *Banker) Mint(address std.Address, amount uint64) error { - if !address.IsValid() { - return ErrInvalidAddress - } - - // TODO: check for overflow - - b.totalSupply += amount - currentBalance := b.BalanceOf(address) - newBalance := currentBalance + amount - - b.balances.Set(string(address), newBalance) - - std.Emit( - MintEvent, - "from", "", - "to", string(address), - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b *Banker) Burn(address std.Address, amount uint64) error { - if !address.IsValid() { - return ErrInvalidAddress - } - // TODO: check for overflow - - currentBalance := b.BalanceOf(address) - if currentBalance < amount { - return ErrInsufficientBalance - } - - b.totalSupply -= amount - newBalance := currentBalance - amount - - b.balances.Set(string(address), newBalance) - - std.Emit( - BurnEvent, - "from", string(address), - "to", "", - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b Banker) BalanceOf(address std.Address) uint64 { - balance, found := b.balances.Get(address.String()) - if !found { - return 0 - } - return balance.(uint64) -} - -func (b *Banker) SpendAllowance(owner, spender std.Address, amount uint64) error { - if !owner.IsValid() { - return ErrInvalidAddress - } - if !spender.IsValid() { - return ErrInvalidAddress - } - - currentAllowance := b.Allowance(owner, spender) - if currentAllowance < amount { - return ErrInsufficientAllowance - } - - key := allowanceKey(owner, spender) - newAllowance := currentAllowance - amount - - if newAllowance == 0 { - b.allowances.Remove(key) - } else { - b.allowances.Set(key, newAllowance) - } - - return nil -} - -func (b *Banker) Transfer(from, to std.Address, amount uint64) error { - if !from.IsValid() { - return ErrInvalidAddress - } - if !to.IsValid() { - return ErrInvalidAddress - } - if from == to { - return ErrCannotTransferToSelf - } - - toBalance := b.BalanceOf(to) - fromBalance := b.BalanceOf(from) - - if fromBalance < amount { - return ErrInsufficientBalance - } - - newToBalance := toBalance + amount - newFromBalance := fromBalance - amount - - b.balances.Set(string(to), newToBalance) - b.balances.Set(string(from), newFromBalance) - - std.Emit( - TransferEvent, - "from", from.String(), - "to", to.String(), - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b *Banker) TransferFrom(spender, from, to std.Address, amount uint64) error { - if err := b.SpendAllowance(from, spender, amount); err != nil { - return err - } - return b.Transfer(from, to, amount) -} - -func (b *Banker) Allowance(owner, spender std.Address) uint64 { - allowance, found := b.allowances.Get(allowanceKey(owner, spender)) - if !found { - return 0 - } - return allowance.(uint64) -} - -func (b *Banker) Approve(owner, spender std.Address, amount uint64) error { - if !owner.IsValid() { - return ErrInvalidAddress - } - if !spender.IsValid() { - return ErrInvalidAddress - } - - b.allowances.Set(allowanceKey(owner, spender), amount) - - std.Emit( - ApprovalEvent, - "owner", string(owner), - "spender", string(spender), - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b *Banker) RenderHome() string { - str := "" - str += ufmt.Sprintf("# %s ($%s)\n\n", b.name, b.symbol) - str += ufmt.Sprintf("* **Decimals**: %d\n", b.decimals) - str += ufmt.Sprintf("* **Total supply**: %d\n", b.totalSupply) - str += ufmt.Sprintf("* **Known accounts**: %d\n", b.KnownAccounts()) - return str -} - -func allowanceKey(owner, spender std.Address) string { - return owner.String() + ":" + spender.String() -} diff --git a/examples/gno.land/p/demo/grc/grc20/banker_test.gno b/examples/gno.land/p/demo/grc/grc20/banker_test.gno deleted file mode 100644 index 00a1e75df1f..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/banker_test.gno +++ /dev/null @@ -1,51 +0,0 @@ -package grc20 - -import ( - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" - "gno.land/p/demo/urequire" -) - -func TestBankerImpl(t *testing.T) { - dummy := NewBanker("Dummy", "DUMMY", 4) - urequire.False(t, dummy == nil, "dummy should not be nil") -} - -func TestAllowance(t *testing.T) { - var ( - owner = testutils.TestAddress("owner") - spender = testutils.TestAddress("spender") - dest = testutils.TestAddress("dest") - ) - - b := NewBanker("Dummy", "DUMMY", 6) - urequire.NoError(t, b.Mint(owner, 100000000)) - urequire.NoError(t, b.Approve(owner, spender, 5000000)) - urequire.Error(t, b.TransferFrom(spender, owner, dest, 10000000), ErrInsufficientAllowance.Error(), "should not be able to transfer more than approved") - - tests := []struct { - spend uint64 - exp uint64 - }{ - {3, 4999997}, - {999997, 4000000}, - {4000000, 0}, - } - - for _, tt := range tests { - b0 := b.BalanceOf(dest) - urequire.NoError(t, b.TransferFrom(spender, owner, dest, tt.spend)) - a := b.Allowance(owner, spender) - urequire.Equal(t, a, tt.exp, ufmt.Sprintf("allowance exp: %d, got %d", tt.exp, a)) - b := b.BalanceOf(dest) - expB := b0 + tt.spend - urequire.Equal(t, b, expB, ufmt.Sprintf("balance exp: %d, got %d", expB, b)) - } - - urequire.Error(t, b.TransferFrom(spender, owner, dest, 1), "no allowance") - key := allowanceKey(owner, spender) - urequire.False(t, b.allowances.Has(key), "allowance should be removed") - urequire.Equal(t, b.Allowance(owner, spender), uint64(0), "allowance should be 0") -} diff --git a/examples/gno.land/p/demo/grc/grc20/examples_test.gno b/examples/gno.land/p/demo/grc/grc20/examples_test.gno new file mode 100644 index 00000000000..6a2bfa11d8c --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/examples_test.gno @@ -0,0 +1,18 @@ +package grc20 + +// XXX: write Examples + +func ExampleInit() {} +func ExampleExposeBankForMaketxRunOrImports() {} +func ExampleCustomTellerImpl() {} +func ExampleAllowance() {} +func ExampleRealmBanker() {} +func ExamplePrevRealmBanker() {} +func ExampleAccountBanker() {} +func ExampleTransfer() {} +func ExampleApprove() {} +func ExampleTransferFrom() {} +func ExampleMint() {} +func ExampleBurn() {} + +// ... diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index e872d80ec12..91b430d3d2f 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -4,6 +4,7 @@ require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/grc/exts v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc20/mock.gno b/examples/gno.land/p/demo/grc/grc20/mock.gno new file mode 100644 index 00000000000..4952470d665 --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/mock.gno @@ -0,0 +1,3 @@ +package grc20 + +// XXX: func Mock(t *Token) diff --git a/examples/gno.land/p/demo/grc/grc20/tellers.gno b/examples/gno.land/p/demo/grc/grc20/tellers.gno new file mode 100644 index 00000000000..ee5d2d7fcca --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/tellers.gno @@ -0,0 +1,139 @@ +package grc20 + +import ( + "std" +) + +// CallerTeller returns a GRC20 compatible teller that checks the PrevRealm +// caller for each call. It's usually safe to expose it publicly to let users +// manipulate their tokens directly, or for realms to use their allowance. +func (tok *Token) CallerTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + return &fnTeller{ + accountFn: func() std.Address { + caller := std.PrevRealm().Addr() + return caller + }, + Token: tok, + } +} + +// ReadonlyTeller is a GRC20 compatible teller that panics for any write operation. +func (tok *Token) ReadonlyTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + return &fnTeller{ + accountFn: nil, + Token: tok, + } +} + +// RealmTeller returns a GRC20 compatible teller that will store the +// caller realm permanently. Calling anything through this teller will +// result in allowance or balance changes for the realm that initialized the teller. +// The initializer of this teller should usually never share the resulting Teller from +// this method except maybe for advanced delegation flows such as a DAO treasury +// management. +func (tok *Token) RealmTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + caller := std.PrevRealm().Addr() + + return &fnTeller{ + accountFn: func() std.Address { + return caller + }, + Token: tok, + } +} + +// RealmSubTeller is like RealmTeller but uses the provided slug to derive a +// subaccount. +func (tok *Token) RealmSubTeller(slug string) Teller { + if tok == nil { + panic("Token cannot be nil") + } + + caller := std.PrevRealm().Addr() + account := accountSlugAddr(caller, slug) + + return &fnTeller{ + accountFn: func() std.Address { + return account + }, + Token: tok, + } +} + +// ImpersonateTeller returns a GRC20 compatible teller that impersonates as a +// specified address. This allows operations to be performed as if they were +// executed by the given address, enabling the caller to manipulate tokens on +// behalf of that address. +// +// It is particularly useful in scenarios where a contract needs to perform +// actions on behalf of a user or another account, without exposing the +// underlying logic or requiring direct access to the user's account. The +// returned teller will use the provided address for all operations, effectively +// masking the original caller. +// +// This method should be used with caution, as it allows for potentially +// sensitive operations to be performed under the guise of another address. +func (ledger *PrivateLedger) ImpersonateTeller(addr std.Address) Teller { + if ledger == nil { + panic("Ledger cannot be nil") + } + + return &fnTeller{ + accountFn: func() std.Address { + return addr + }, + Token: ledger.token, + } +} + +// generic tellers methods. +// + +func (ft *fnTeller) Transfer(to std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + caller := ft.accountFn() + return ft.Token.ledger.Transfer(caller, to, amount) +} + +func (ft *fnTeller) Approve(spender std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + caller := ft.accountFn() + return ft.Token.ledger.Approve(caller, spender, amount) +} + +func (ft *fnTeller) TransferFrom(owner, to std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + spender := ft.accountFn() + return ft.Token.ledger.TransferFrom(owner, spender, to, amount) +} + +// helpers +// + +// accountSlugAddr returns the address derived from the specified address and slug. +func accountSlugAddr(addr std.Address, slug string) std.Address { + // XXX: use a new `std.XXX` call for this. + if slug == "" { + return addr + } + key := addr.String() + "/" + slug + return std.DerivePkgAddr(key) // temporarily using this helper +} diff --git a/examples/gno.land/p/demo/grc/grc20/tellers_test.gno b/examples/gno.land/p/demo/grc/grc20/tellers_test.gno new file mode 100644 index 00000000000..2a724964edc --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/tellers_test.gno @@ -0,0 +1,130 @@ +package grc20 + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +func TestCallerTellerImpl(t *testing.T) { + tok, _ := NewToken("Dummy", "DUMMY", 4) + teller := tok.CallerTeller() + urequire.False(t, tok == nil) + var _ Teller = teller +} + +func TestTeller(t *testing.T) { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carl = testutils.TestAddress("carl") + ) + + token, ledger := NewToken("Dummy", "DUMMY", 6) + + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := token.BalanceOf(alice) + bobGB := token.BalanceOf(bob) + carlGB := token.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := token.Allowance(alice, bob) + acGB := token.Allowance(alice, carl) + baGB := token.Allowance(bob, alice) + bcGB := token.Allowance(bob, carl) + caGB := token.Allowance(carl, alice) + cbGB := token.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") + } + + checkBalances(0, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Mint(alice, 1000)) + urequire.NoError(t, ledger.Mint(alice, 100)) + checkBalances(1100, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Approve(alice, bob, 99999999)) + checkBalances(1100, 0, 0) + checkAllowances(99999999, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Approve(alice, bob, 400)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.Error(t, ledger.TransferFrom(alice, bob, carl, 100000000)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.TransferFrom(alice, bob, carl, 100)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.Error(t, ledger.SpendAllowance(alice, bob, 2000000)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.SpendAllowance(alice, bob, 100)) + checkBalances(1000, 0, 100) + checkAllowances(200, 0, 0, 0, 0, 0) +} + +func TestCallerTeller(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + carl := testutils.TestAddress("carl") + + token, ledger := NewToken("Dummy", "DUMMY", 6) + teller := token.CallerTeller() + + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := token.BalanceOf(alice) + bobGB := token.BalanceOf(bob) + carlGB := token.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := token.Allowance(alice, bob) + acGB := token.Allowance(alice, carl) + baGB := token.Allowance(bob, alice) + bcGB := token.Allowance(bob, carl) + caGB := token.Allowance(carl, alice) + cbGB := token.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") + } + + urequire.NoError(t, ledger.Mint(alice, 1000)) + checkBalances(1000, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + std.TestSetOrigCaller(alice) + urequire.NoError(t, teller.Approve(bob, 600)) + checkBalances(1000, 0, 0) + checkAllowances(600, 0, 0, 0, 0, 0) + + std.TestSetOrigCaller(bob) + urequire.Error(t, teller.TransferFrom(alice, carl, 700)) + checkBalances(1000, 0, 0) + checkAllowances(600, 0, 0, 0, 0, 0) + urequire.NoError(t, teller.TransferFrom(alice, carl, 400)) + checkBalances(600, 0, 400) + checkAllowances(200, 0, 0, 0, 0, 0) +} diff --git a/examples/gno.land/p/demo/grc/grc20/token.gno b/examples/gno.land/p/demo/grc/grc20/token.gno index c9e125261b5..4634bae933b 100644 --- a/examples/gno.land/p/demo/grc/grc20/token.gno +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -1,45 +1,240 @@ package grc20 import ( + "math/overflow" "std" + "strconv" + + "gno.land/p/demo/ufmt" ) -// token implements the Token interface. -// -// It is generated with Banker.Token(). -// It can safely be exposed publicly. -type token struct { - banker *Banker +// NewToken creates a new Token. +// It returns a pointer to the Token and a pointer to the Ledger. +// Expected usage: Token, admin := NewToken("Dummy", "DUMMY", 4) +func NewToken(name, symbol string, decimals uint) (*Token, *PrivateLedger) { + if name == "" { + panic("name should not be empty") + } + if symbol == "" { + panic("symbol should not be empty") + } + // XXX additional checks (length, characters, limits, etc) + + ledger := &PrivateLedger{} + token := &Token{ + name: name, + symbol: symbol, + decimals: decimals, + ledger: ledger, + } + ledger.token = token + return token, ledger } -// var _ Token = (*token)(nil) -func (t *token) GetName() string { return t.banker.name } -func (t *token) GetSymbol() string { return t.banker.symbol } -func (t *token) GetDecimals() uint { return t.banker.decimals } -func (t *token) TotalSupply() uint64 { return t.banker.totalSupply } +// GetName returns the name of the token. +func (tok Token) GetName() string { return tok.name } + +// GetSymbol returns the symbol of the token. +func (tok Token) GetSymbol() string { return tok.symbol } + +// GetDecimals returns the number of decimals used to get the token's precision. +func (tok Token) GetDecimals() uint { return tok.decimals } + +// TotalSupply returns the total supply of the token. +func (tok Token) TotalSupply() uint64 { return tok.ledger.totalSupply } -func (t *token) BalanceOf(owner std.Address) uint64 { - return t.banker.BalanceOf(owner) +// KnownAccounts returns the number of known accounts in the bank. +func (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() } + +// BalanceOf returns the balance of the specified address. +func (tok Token) BalanceOf(address std.Address) uint64 { + return tok.ledger.balanceOf(address) } -func (t *token) Transfer(to std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.banker.Transfer(caller, to, amount) +// Allowance returns the allowance of the specified owner and spender. +func (tok Token) Allowance(owner, spender std.Address) uint64 { + return tok.ledger.allowance(owner, spender) } -func (t *token) Allowance(owner, spender std.Address) uint64 { - return t.banker.Allowance(owner, spender) +func (tok *Token) RenderHome() string { + str := "" + str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol) + str += ufmt.Sprintf("* **Decimals**: %d\n", tok.decimals) + str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply) + str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts()) + return str } -func (t *token) Approve(spender std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.banker.Approve(caller, spender, amount) +// SpendAllowance decreases the allowance of the specified owner and spender. +func (led *PrivateLedger) SpendAllowance(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() { + return ErrInvalidAddress + } + if !spender.IsValid() { + return ErrInvalidAddress + } + + currentAllowance := led.allowance(owner, spender) + if currentAllowance < amount { + return ErrInsufficientAllowance + } + + key := allowanceKey(owner, spender) + newAllowance := currentAllowance - amount + + if newAllowance == 0 { + led.allowances.Remove(key) + } else { + led.allowances.Set(key, newAllowance) + } + + return nil } -func (t *token) TransferFrom(from, to std.Address, amount uint64) error { - spender := std.PrevRealm().Addr() - if err := t.banker.SpendAllowance(from, spender, amount); err != nil { +// Transfer transfers tokens from the specified from address to the specified to address. +func (led *PrivateLedger) Transfer(from, to std.Address, amount uint64) error { + if !from.IsValid() { + return ErrInvalidAddress + } + if !to.IsValid() { + return ErrInvalidAddress + } + if from == to { + return ErrCannotTransferToSelf + } + + var ( + toBalance = led.balanceOf(to) + fromBalance = led.balanceOf(from) + ) + + if fromBalance < amount { + return ErrInsufficientBalance + } + + var ( + newToBalance = toBalance + amount + newFromBalance = fromBalance - amount + ) + + led.balances.Set(string(to), newToBalance) + led.balances.Set(string(from), newFromBalance) + + std.Emit( + TransferEvent, + "from", from.String(), + "to", to.String(), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// TransferFrom transfers tokens from the specified owner to the specified to address. +// It first checks if the owner has sufficient balance and then decreases the allowance. +func (led *PrivateLedger) TransferFrom(owner, spender, to std.Address, amount uint64) error { + if led.balanceOf(owner) < amount { + return ErrInsufficientBalance + } + if err := led.SpendAllowance(owner, spender, amount); err != nil { return err } - return t.banker.Transfer(from, to, amount) + // XXX: since we don't "panic", we should take care of rollbacking spendAllowance if transfer fails. + return led.Transfer(owner, to, amount) +} + +// Approve sets the allowance of the specified owner and spender. +func (led *PrivateLedger) Approve(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() || !spender.IsValid() { + return ErrInvalidAddress + } + + led.allowances.Set(allowanceKey(owner, spender), amount) + + std.Emit( + ApprovalEvent, + "owner", string(owner), + "spender", string(spender), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// Mint increases the total supply of the token and adds the specified amount to the specified address. +func (led *PrivateLedger) Mint(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + // XXX: math/overflow is not supporting uint64. + // This checks prevents overflow but makes the totalSupply limited to a uint63. + sum, ok := overflow.Add64(int64(led.totalSupply), int64(amount)) + if !ok { + return ErrOverflow + } + + led.totalSupply = uint64(sum) + currentBalance := led.balanceOf(address) + newBalance := currentBalance + amount + + led.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", "", + "to", string(address), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// Burn decreases the total supply of the token and subtracts the specified amount from the specified address. +func (led *PrivateLedger) Burn(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + currentBalance := led.balanceOf(address) + if currentBalance < amount { + return ErrInsufficientBalance + } + + led.totalSupply -= amount + newBalance := currentBalance - amount + + led.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", string(address), + "to", "", + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// balanceOf returns the balance of the specified address. +func (led PrivateLedger) balanceOf(address std.Address) uint64 { + balance, found := led.balances.Get(address.String()) + if !found { + return 0 + } + return balance.(uint64) +} + +// allowance returns the allowance of the specified owner and spender. +func (led PrivateLedger) allowance(owner, spender std.Address) uint64 { + allowance, found := led.allowances.Get(allowanceKey(owner, spender)) + if !found { + return 0 + } + return allowance.(uint64) +} + +// allowanceKey returns the key for the allowance of the specified owner and spender. +func allowanceKey(owner, spender std.Address) string { + return owner.String() + ":" + spender.String() } diff --git a/examples/gno.land/p/demo/grc/grc20/token_test.gno b/examples/gno.land/p/demo/grc/grc20/token_test.gno index 713ad734ed8..c68513554f0 100644 --- a/examples/gno.land/p/demo/grc/grc20/token_test.gno +++ b/examples/gno.land/p/demo/grc/grc20/token_test.gno @@ -1,72 +1,89 @@ package grc20 import ( - "std" "testing" "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" ) -func TestUserTokenImpl(t *testing.T) { - bank := NewBanker("Dummy", "DUMMY", 4) - tok := bank.Token() - _ = tok +func TestTestImpl(t *testing.T) { + bank, _ := NewToken("Dummy", "DUMMY", 4) + urequire.False(t, bank == nil, "dummy should not be nil") } -func TestUserApprove(t *testing.T) { - owner := testutils.TestAddress("owner") - spender := testutils.TestAddress("spender") - dest := testutils.TestAddress("dest") - - bank := NewBanker("Dummy", "DUMMY", 6) - tok := bank.Token() - - // Set owner as the original caller - std.TestSetOrigCaller(owner) - // Mint 100000000 tokens for owner - urequire.NoError(t, bank.Mint(owner, 100000000)) - - // Approve spender to spend 5000000 tokens - urequire.NoError(t, tok.Approve(spender, 5000000)) - - // Set spender as the original caller - std.TestSetOrigCaller(spender) - // Try to transfer 10000000 tokens from owner to dest, should fail because it exceeds allowance - urequire.Error(t, - tok.TransferFrom(owner, dest, 10000000), - ErrInsufficientAllowance.Error(), - "should not be able to transfer more than approved", +func TestToken(t *testing.T) { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carl = testutils.TestAddress("carl") ) - // Define a set of test data with spend amount and expected remaining allowance - tests := []struct { - spend uint64 // Spend amount - exp uint64 // Remaining allowance - }{ - {3, 4999997}, - {999997, 4000000}, - {4000000, 0}, - } + bank, adm := NewToken("Dummy", "DUMMY", 6) - // perform transfer operation,and check if allowance and balance are correct - for _, tt := range tests { - b0 := tok.BalanceOf(dest) - // Perform transfer from owner to dest - urequire.NoError(t, tok.TransferFrom(owner, dest, tt.spend)) - a := tok.Allowance(owner, spender) - // Check if allowance equals expected value - urequire.True(t, a == tt.exp, ufmt.Sprintf("allowance exp: %d,got %d", tt.exp, a)) - - // Get dest current balance - b := tok.BalanceOf(dest) - // Calculate expected balance ,should be initial balance plus transfer amount - expB := b0 + tt.spend - // Check if balance equals expected value - urequire.True(t, b == expB, ufmt.Sprintf("balance exp: %d,got %d", expB, b)) + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := bank.BalanceOf(alice) + bobGB := bank.BalanceOf(bob) + carlGB := bank.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := bank.Allowance(alice, bob) + acGB := bank.Allowance(alice, carl) + baGB := bank.Allowance(bob, alice) + bcGB := bank.Allowance(bob, carl) + caGB := bank.Allowance(carl, alice) + cbGB := bank.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") } - // Try to transfer one token from owner to dest ,should fail because no allowance left - urequire.Error(t, tok.TransferFrom(owner, dest, 1), ErrInsufficientAllowance.Error(), "no allowance") + checkBalances(0, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Mint(alice, 1000)) + urequire.NoError(t, adm.Mint(alice, 100)) + checkBalances(1100, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Approve(alice, bob, 99999999)) + checkBalances(1100, 0, 0) + checkAllowances(99999999, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Approve(alice, bob, 400)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.Error(t, adm.TransferFrom(alice, bob, carl, 100000000)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.TransferFrom(alice, bob, carl, 100)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.Error(t, adm.SpendAllowance(alice, bob, 2000000)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.SpendAllowance(alice, bob, 100)) + checkBalances(1000, 0, 100) + checkAllowances(200, 0, 0, 0, 0, 0) +} + +func TestOverflow(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + tok, adm := NewToken("Dummy", "DUMMY", 6) + + urequire.NoError(t, adm.Mint(alice, 2<<62)) + urequire.Equal(t, tok.BalanceOf(alice), uint64(2<<62)) + urequire.Error(t, adm.Mint(bob, 2<<62)) } diff --git a/examples/gno.land/p/demo/grc/grc20/types.gno b/examples/gno.land/p/demo/grc/grc20/types.gno index 201c6638914..cf67858ccf3 100644 --- a/examples/gno.land/p/demo/grc/grc20/types.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -4,17 +4,17 @@ import ( "errors" "std" + "gno.land/p/demo/avl" "gno.land/p/demo/grc/exts" ) -var ( - ErrInsufficientBalance = errors.New("insufficient balance") - ErrInsufficientAllowance = errors.New("insufficient allowance") - ErrInvalidAddress = errors.New("invalid address") - ErrCannotTransferToSelf = errors.New("cannot send transfer to self") -) - -type Token interface { +// Teller interface defines the methods that a GRC20 token must implement. It +// extends the TokenMetadata interface to include methods for managing token +// transfers, allowances, and querying balances. +// +// The Teller interface is designed to ensure that any token adhering to this +// standard provides a consistent API for interacting with fungible tokens. +type Teller interface { exts.TokenMetadata // Returns the amount of tokens in existence. @@ -55,9 +55,54 @@ type Token interface { TransferFrom(from, to std.Address, amount uint64) error } +// Token represents a fungible token with a name, symbol, and a certain number +// of decimal places. It maintains a ledger for tracking balances and allowances +// of addresses. +// +// The Token struct provides methods for retrieving token metadata, such as the +// name, symbol, and decimals, as well as methods for interacting with the +// ledger, including checking balances and allowances. +type Token struct { + name string // Name of the token (e.g., "Dummy Token"). + symbol string // Symbol of the token (e.g., "DUMMY"). + decimals uint // Number of decimal places used for the token's precision. + ledger *PrivateLedger // Pointer to the PrivateLedger that manages balances and allowances. +} + +// PrivateLedger is a struct that holds the balances and allowances for the +// token. It provides administrative functions for minting, burning, +// transferring tokens, and managing allowances. +// +// The PrivateLedger is not safe to expose publicly, as it contains sensitive +// information regarding token balances and allowances, and allows direct, +// unrestricted access to all administrative functions. +type PrivateLedger struct { + totalSupply uint64 // Total supply of the token managed by this ledger. + balances avl.Tree // std.Address -> uint64 + allowances avl.Tree // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 + token *Token // Pointer to the associated Token struct +} + +var ( + ErrInsufficientBalance = errors.New("insufficient balance") + ErrInsufficientAllowance = errors.New("insufficient allowance") + ErrInvalidAddress = errors.New("invalid address") + ErrCannotTransferToSelf = errors.New("cannot send transfer to self") + ErrReadonly = errors.New("banker is readonly") + ErrRestrictedTokenOwner = errors.New("restricted to bank owner") + ErrOverflow = errors.New("Mint overflow") +) + const ( MintEvent = "Mint" BurnEvent = "Burn" TransferEvent = "Transfer" ApprovalEvent = "Approval" ) + +type fnTeller struct { + accountFn func() std.Address + *Token +} + +var _ Teller = (*fnTeller)(nil) diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index 1d6ecd3d378..de51b8b47d9 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -12,18 +12,17 @@ import ( ) var ( - banker *grc20.Banker // private banker. - Token grc20.Token // public safe-object. + Token, adm = grc20.NewToken("Bar", "BAR", 4) + UserTeller = Token.CallerTeller() ) func init() { - banker = grc20.NewBanker("Bar", "BAR", 4) - Token = banker.Token() + // XXX: grc20reg.Register(Token, "") } func Faucet() string { caller := std.PrevRealm().Addr() - if err := banker.Mint(caller, 1_000_000); err != nil { + if err := adm.Mint(caller, 1_000_000); err != nil { return "error: " + err.Error() } return "OK" @@ -35,7 +34,7 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() // XXX: should be Token.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) balance := Token.BalanceOf(owner) diff --git a/examples/gno.land/r/demo/bar20/bar20_test.gno b/examples/gno.land/r/demo/bar20/bar20_test.gno index 20349258c1b..0561d13c865 100644 --- a/examples/gno.land/r/demo/bar20/bar20_test.gno +++ b/examples/gno.land/r/demo/bar20/bar20_test.gno @@ -13,7 +13,7 @@ func TestPackage(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) std.TestSetOrigCaller(alice) // XXX: should not need this - urequire.Equal(t, Token.BalanceOf(alice), uint64(0)) + urequire.Equal(t, UserTeller.BalanceOf(alice), uint64(0)) urequire.Equal(t, Faucet(), "OK") - urequire.Equal(t, Token.BalanceOf(alice), uint64(1_000_000)) + urequire.Equal(t, UserTeller.BalanceOf(alice), uint64(1_000_000)) } diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 9d4e5d40193..fe099117215 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -1,5 +1,5 @@ -// foo20 is a GRC20 token contract where all the GRC20 methods are proxified -// with top-level functions. see also gno.land/r/demo/bar20. +// foo20 is a GRC20 token contract where all the grc20.Teller methods are +// proxified with top-level functions. see also gno.land/r/demo/bar20. package foo20 import ( @@ -14,45 +14,45 @@ import ( ) var ( - banker *grc20.Banker - admin *ownable.Ownable - token grc20.Token + Token, privateLedger = grc20.NewToken("Foo", "FOO", 4) + UserTeller = Token.CallerTeller() + owner = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred ) func init() { - admin = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred - banker = grc20.NewBanker("Foo", "FOO", 4) - banker.Mint(admin.Owner(), 1000000*10000) // @administrator (1M) - token = banker.Token() + privateLedger.Mint(owner.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) + // XXX: grc20reg.Register(Token, "") } -func TotalSupply() uint64 { return token.TotalSupply() } +func TotalSupply() uint64 { + return UserTeller.TotalSupply() +} func BalanceOf(owner pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) - return token.BalanceOf(ownerAddr) + return UserTeller.BalanceOf(ownerAddr) } func Allowance(owner, spender pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) spenderAddr := users.Resolve(spender) - return token.Allowance(ownerAddr, spenderAddr) + return UserTeller.Allowance(ownerAddr, spenderAddr) } func Transfer(to pusers.AddressOrName, amount uint64) { toAddr := users.Resolve(to) - checkErr(token.Transfer(toAddr, amount)) + checkErr(UserTeller.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { spenderAddr := users.Resolve(spender) - checkErr(token.Approve(spenderAddr, amount)) + checkErr(UserTeller.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { fromAddr := users.Resolve(from) toAddr := users.Resolve(to) - checkErr(token.TransferFrom(fromAddr, toAddr, amount)) + checkErr(UserTeller.TransferFrom(fromAddr, toAddr, amount)) } // Faucet is distributing foo20 tokens without restriction (unsafe). @@ -60,19 +60,19 @@ func TransferFrom(from, to pusers.AddressOrName, amount uint64) { func Faucet() { caller := std.PrevRealm().Addr() amount := uint64(1_000 * 10_000) // 1k - checkErr(banker.Mint(caller, amount)) + checkErr(privateLedger.Mint(caller, amount)) } func Mint(to pusers.AddressOrName, amount uint64) { - admin.AssertCallerIsOwner() + owner.AssertCallerIsOwner() toAddr := users.Resolve(to) - checkErr(banker.Mint(toAddr, amount)) + checkErr(privateLedger.Mint(toAddr, amount)) } func Burn(from pusers.AddressOrName, amount uint64) { - admin.AssertCallerIsOwner() + owner.AssertCallerIsOwner() fromAddr := users.Resolve(from) - checkErr(banker.Burn(fromAddr, amount)) + checkErr(privateLedger.Burn(fromAddr, amount)) } func Render(path string) string { @@ -81,11 +81,11 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := pusers.AddressOrName(parts[1]) ownerAddr := users.Resolve(owner) - balance := banker.BalanceOf(ownerAddr) + balance := UserTeller.BalanceOf(ownerAddr) return ufmt.Sprintf("%d\n", balance) default: return "404\n" diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index 77c99d0525e..b3346296b04 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -71,10 +71,18 @@ func TestErrConditions(t *testing.T) { fn func() } - std.TestSetOrigCaller(users.Resolve(admin)) + privateLedger.Mint(std.Address(admin), 10000) { tests := []test{ - {"Transfer(admin, 1)", "cannot send transfer to self", func() { Transfer(admin, 1) }}, + {"Transfer(admin, 1)", "cannot send transfer to self", func() { + // XXX: should replace with: Transfer(admin, 1) + // but there is currently a limitation in manipulating the frame stack and simulate + // calling this package from an outside point of view. + adminAddr := std.Address(admin) + if err := privateLedger.Transfer(adminAddr, adminAddr, 1); err != nil { + panic(err) + } + }}, {"Approve(empty, 1))", "invalid address", func() { Approve(empty, 1) }}, } for _, tc := range tests { diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index 8d0fbd0c46b..bf5e9c9ec96 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -4,6 +4,7 @@ require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/grc/grc20 v0.0.0-latest gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index f37a9370a9e..901a9b9f33c 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -12,6 +12,13 @@ import ( var instances avl.Tree // symbol -> instance +type instance struct { + token *grc20.Token + ledger *grc20.PrivateLedger + admin *ownable.Ownable + faucet uint64 // per-request amount. disabled if 0. +} + func New(name, symbol string, decimals uint, initialMint, faucet uint64) { caller := std.PrevRealm().Addr() NewWithAdmin(name, symbol, decimals, initialMint, faucet, caller) @@ -23,56 +30,68 @@ func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64 panic("token already exists") } - banker := grc20.NewBanker(name, symbol, decimals) + token, ledger := grc20.NewToken(name, symbol, decimals) if initialMint > 0 { - banker.Mint(admin, initialMint) + ledger.Mint(admin, initialMint) } inst := instance{ - banker: banker, + token: token, + ledger: ledger, admin: ownable.NewWithAddress(admin), faucet: faucet, } - instances.Set(symbol, &inst) + // XXX: grc20reg.Register(token, symbol) } -type instance struct { - banker *grc20.Banker - admin *ownable.Ownable - faucet uint64 // per-request amount. disabled if 0. +func (inst instance) Token() *grc20.Token { + return inst.token +} + +func (inst instance) CallerTeller() grc20.Teller { + return inst.token.CallerTeller() } -func (inst instance) Token() grc20.Token { return inst.banker.Token() } +func Bank(symbol string) *grc20.Token { + inst := mustGetInstance(symbol) + return inst.token +} func TotalSupply(symbol string) uint64 { inst := mustGetInstance(symbol) - return inst.Token().TotalSupply() + return inst.token.ReadonlyTeller().TotalSupply() } func BalanceOf(symbol string, owner std.Address) uint64 { inst := mustGetInstance(symbol) - return inst.Token().BalanceOf(owner) + return inst.token.ReadonlyTeller().BalanceOf(owner) } func Allowance(symbol string, owner, spender std.Address) uint64 { inst := mustGetInstance(symbol) - return inst.Token().Allowance(owner, spender) + return inst.token.ReadonlyTeller().Allowance(owner, spender) } func Transfer(symbol string, to std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().Transfer(to, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.Transfer(to, amount)) } func Approve(symbol string, spender std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().Approve(spender, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.Approve(spender, amount)) } func TransferFrom(symbol string, from, to std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().TransferFrom(from, to, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.TransferFrom(from, to, amount)) } // faucet. @@ -84,19 +103,19 @@ func Faucet(symbol string) { // FIXME: add limits? // FIXME: add payment in gnot? caller := std.PrevRealm().Addr() - checkErr(inst.banker.Mint(caller, inst.faucet)) + checkErr(inst.ledger.Mint(caller, inst.faucet)) } func Mint(symbol string, to std.Address, amount uint64) { inst := mustGetInstance(symbol) inst.admin.AssertCallerIsOwner() - checkErr(inst.banker.Mint(to, amount)) + checkErr(inst.ledger.Mint(to, amount)) } func Burn(symbol string, from std.Address, amount uint64) { inst := mustGetInstance(symbol) inst.admin.AssertCallerIsOwner() - checkErr(inst.banker.Burn(from, amount)) + checkErr(inst.ledger.Burn(from, amount)) } func Render(path string) string { @@ -109,12 +128,12 @@ func Render(path string) string { case c == 1: symbol := parts[0] inst := mustGetInstance(symbol) - return inst.banker.RenderHome() + return inst.token.RenderHome() case c == 3 && parts[1] == "balance": symbol := parts[0] inst := mustGetInstance(symbol) owner := std.Address(parts[2]) - balance := inst.Token().BalanceOf(owner) + balance := inst.token.CallerTeller().BalanceOf(owner) return ufmt.Sprintf("%d", balance) default: return "404\n" @@ -131,6 +150,6 @@ func mustGetInstance(symbol string) *instance { func checkErr(err error) { if err != nil { - panic(err) + panic(err.Error()) } } diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno index 5dfb6a760cc..46fc07fabf2 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno @@ -4,16 +4,16 @@ import ( "std" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" ) func TestReadOnlyPublicMethods(t *testing.T) { - admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - manfred := std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - unknown := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // valid but never used. - NewWithAdmin("Foo", "FOO", 4, 10_000*1_000_000, 0, admin) - NewWithAdmin("Bar", "BAR", 4, 10_000*1_000, 0, admin) - mustGetInstance("FOO").banker.Mint(manfred, 100_000_000) + std.TestSetOrigPkgAddr("gno.land/r/demo/grc20factory") + admin := testutils.TestAddress("admin") + bob := testutils.TestAddress("bob") + carl := testutils.TestAddress("carl") type test struct { name string @@ -21,36 +21,52 @@ func TestReadOnlyPublicMethods(t *testing.T) { fn func() uint64 } - // check balances #1. - { + checkBalances := func(step string, totSup, balAdm, balBob, allowAdmBob, balCarl uint64) { tests := []test{ - {"TotalSupply", 10_100_000_000, func() uint64 { return TotalSupply("FOO") }}, - {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf("FOO", admin) }}, - {"BalanceOf(manfred)", 100_000_000, func() uint64 { return BalanceOf("FOO", manfred) }}, - {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, - {"BalanceOf(unknown)", 0, func() uint64 { return BalanceOf("FOO", unknown) }}, + {"TotalSupply", totSup, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", balAdm, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(bob)", balBob, func() uint64 { return BalanceOf("FOO", bob) }}, + {"Allowance(admin, bob)", allowAdmBob, func() uint64 { return Allowance("FOO", admin, bob) }}, + {"BalanceOf(carl)", balCarl, func() uint64 { return BalanceOf("FOO", carl) }}, } for _, tc := range tests { - uassert.Equal(t, tc.balance, tc.fn(), "balance does not match") + reason := ufmt.Sprintf("%s.%s - %s", step, tc.name, "balances do not match") + uassert.Equal(t, tc.balance, tc.fn(), reason) } } - return - // unknown uses the faucet. - std.TestSetOrigCaller(unknown) + // admin creates FOO and BAR. + std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) + NewWithAdmin("Foo", "FOO", 3, 1_111_111_000, 5_555, admin) + NewWithAdmin("Bar", "BAR", 3, 2_222_000, 6_666, admin) + checkBalances("step1", 1_111_111_000, 1_111_111_000, 0, 0, 0) + + // admin mints to bob. + mustGetInstance("FOO").ledger.Mint(bob, 333_333_000) + checkBalances("step2", 1_444_444_000, 1_111_111_000, 333_333_000, 0, 0) + + // carl uses the faucet. + std.TestSetOrigCaller(carl) + std.TestSetRealm(std.NewUserRealm(carl)) Faucet("FOO") + checkBalances("step3", 1_444_449_555, 1_111_111_000, 333_333_000, 0, 5_555) - // check balances #2. - { - tests := []test{ - {"TotalSupply", 10_110_000_000, func() uint64 { return TotalSupply("FOO") }}, - {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf("FOO", admin) }}, - {"BalanceOf(manfred)", 100_000_000, func() uint64 { return BalanceOf("FOO", manfred) }}, - {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, - {"BalanceOf(unknown)", 10_000_000, func() uint64 { return BalanceOf("FOO", unknown) }}, - } - for _, tc := range tests { - uassert.Equal(t, tc.balance, tc.fn(), "balance does not match") - } - } + // admin gives to bob some allowance. + std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) + Approve("FOO", bob, 1_000_000) + checkBalances("step4", 1_444_449_555, 1_111_111_000, 333_333_000, 1_000_000, 5_555) + + // bob uses a part of the allowance. + std.TestSetOrigCaller(bob) + std.TestSetRealm(std.NewUserRealm(bob)) + TransferFrom("FOO", admin, carl, 400_000) + checkBalances("step5", 1_444_449_555, 1_110_711_000, 333_333_000, 600_000, 405_555) + + // bob uses a part of the allowance. + std.TestSetOrigCaller(bob) + std.TestSetRealm(std.NewUserRealm(bob)) + TransferFrom("FOO", admin, carl, 600_000) + checkBalances("step6", 1_444_449_555, 1_110_111_000, 333_333_000, 0, 1_005_555) } diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index e1028530c8c..bb109644778 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -10,23 +10,25 @@ import ( "gno.land/r/demo/users" ) -var ( - banker *grc20.Banker = grc20.NewBanker("wrapped GNOT", "wugnot", 0) - Token = banker.Token() -) +var Token, adm = grc20.NewToken("wrapped GNOT", "wugnot", 0) const ( ugnotMinDeposit uint64 = 1000 wugnotMinDeposit uint64 = 1 ) +func init() { + // XXX: grc20reg.Register(Token, "") +} + func Deposit() { caller := std.PrevRealm().Addr() sent := std.GetOrigSend() amount := sent.AmountOf("ugnot") require(uint64(amount) >= ugnotMinDeposit, ufmt.Sprintf("Deposit below minimum: %d/%d ugnot.", amount, ugnotMinDeposit)) - checkErr(banker.Mint(caller, uint64(amount))) + + checkErr(adm.Mint(caller, uint64(amount))) } func Withdraw(amount uint64) { @@ -41,7 +43,7 @@ func Withdraw(amount uint64) { stdBanker := std.GetBanker(std.BankerTypeRealmSend) send := std.Coins{{"ugnot", int64(amount)}} stdBanker.SendCoins(pkgaddr, caller, send) - checkErr(banker.Burn(caller, amount)) + checkErr(adm.Burn(caller, amount)) } func Render(path string) string { @@ -50,7 +52,7 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) balance := Token.BalanceOf(owner) @@ -75,18 +77,21 @@ func Allowance(owner, spender pusers.AddressOrName) uint64 { func Transfer(to pusers.AddressOrName, amount uint64) { toAddr := users.Resolve(to) - checkErr(Token.Transfer(toAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { spenderAddr := users.Resolve(spender) - checkErr(Token.Approve(spenderAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { fromAddr := users.Resolve(from) toAddr := users.Resolve(to) - checkErr(Token.TransferFrom(fromAddr, toAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.TransferFrom(fromAddr, toAddr, amount)) } func require(condition bool, msg string) { From bd1d76e0cbc3963b20fb0365c54efcf089e85b14 Mon Sep 17 00:00:00 2001 From: Nathan Toups <612924+n2p5@users.noreply.github.com> Date: Wed, 13 Nov 2024 00:08:40 -0700 Subject: [PATCH 226/345] feat(examples): add haystack package and realm (#3082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Haystack is a permissionless , immutable, content-addressed, append-only, fixed-length key-value store for small payloads. This is an experiment to port over a storage and validation implementation of a tool I'd written several years ago called [haystack](https://github.com/nomasters/haystack). My goal in porting this over to gno is to provide a simple, correct, and test covered implementation that is composable with other tools I plan to port over as well. ## Overview You store a needle in the haystack. A Needle is 192 bytes. It is composed of 32 bytes for a sha256 hash, and the 160 byte fixed-length payload. ``` hash | payload ---------|---------- 32 bytes | 160 bytes ``` The Haystack storage server supports two calls, other than Render, you may "Add" a needle, by its hex-encoded string, or you may "Get" a needle by its hex encoded hash. The add operation ensures that the needle is valid and that it has not been added to the storage before. The Get operation will return the full hex encoded needle if the hash exists in the database, otherwise it will panic. The structure is broken down into 2 packages and 1 very simple realm ### packages - https://gno.land/p/demo/haystack/needle/ - https://gno.land/p/demo/haystack/ ### realm - https://gno.land/r/demo/haystack ## How to Try it out? You can generate your own synthetic needle from the CLI by using this magical one-liner. ```shell ➜ ~ (dd if=/dev/urandom bs=160 count=1 2>/dev/null | tee >(sha256sum | cut -d' ' -f1) | od -An -t x1) | tr -d ' \n' 5d82091003a6749b46a96c38b2597ca96e9b0b272594249099dcf2ade188346679ca753999661820dad7beb351559c89a275ed4935a82245fda290906670ec7535b0b856dfccadd62e5f5399892455d2b524724ffdef8e58be03e9da4762c6ab582ce91c29a9e26ea9cc38b66953fdc425ad37baeb12c712e049ae6d456e682b6b63eea74ebf7a9d506ba486d08c9c54c5161d38a7fbc5fcbb1cdac370682ad6a59579167fd1aa1cd1fc109660a7eba36775d6b06058d72aa57debe63d0144b8 ``` This leverages `dd`, `/dev/urandom`, `tee`, `cut`, `od`, and `tr` to generate a properly formatted hex-encoded needle. ### Getting a needle I've already stored the above needle in Haystack, so you can read it by running this command from the CLI ```shell gnokey maketx call -pkgpath "gno.land/r/demo/haystack" \ -func "Get" \ -gas-fee 1000000ugnot \ -gas-wanted 2000000 \ -send "" \ -broadcast \ -chainid "portal-loop" \ -args "5d82091003a6749b46a96c38b2597ca96e9b0b272594249099dcf2ade1883466" \ -remote "https://rpc.gno.land:443" \ $YOUR_WALLET_ADDRESS ```
Contributors' checklist... - [X] Added new tests, or not needed, or not feasible - [X] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [X] Updated the official documentation or not needed - [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [X] Added references to related issues and PRs - [X] Provided any useful hints for running manual tests
--- examples/gno.land/p/n2p5/haystack/gno.mod | 6 + .../gno.land/p/n2p5/haystack/haystack.gno | 99 +++++++++++ .../p/n2p5/haystack/haystack_test.gno | 94 +++++++++++ .../gno.land/p/n2p5/haystack/needle/gno.mod | 1 + .../p/n2p5/haystack/needle/needle.gno | 91 ++++++++++ .../p/n2p5/haystack/needle/needle_test.gno | 157 ++++++++++++++++++ examples/gno.land/r/n2p5/haystack/gno.mod | 8 + .../gno.land/r/n2p5/haystack/haystack.gno | 32 ++++ .../r/n2p5/haystack/haystack_test.gno | 70 ++++++++ 9 files changed, 558 insertions(+) create mode 100644 examples/gno.land/p/n2p5/haystack/gno.mod create mode 100644 examples/gno.land/p/n2p5/haystack/haystack.gno create mode 100644 examples/gno.land/p/n2p5/haystack/haystack_test.gno create mode 100644 examples/gno.land/p/n2p5/haystack/needle/gno.mod create mode 100644 examples/gno.land/p/n2p5/haystack/needle/needle.gno create mode 100644 examples/gno.land/p/n2p5/haystack/needle/needle_test.gno create mode 100644 examples/gno.land/r/n2p5/haystack/gno.mod create mode 100644 examples/gno.land/r/n2p5/haystack/haystack.gno create mode 100644 examples/gno.land/r/n2p5/haystack/haystack_test.gno diff --git a/examples/gno.land/p/n2p5/haystack/gno.mod b/examples/gno.land/p/n2p5/haystack/gno.mod new file mode 100644 index 00000000000..ebd0d07a987 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/n2p5/haystack + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/n2p5/haystack/needle v0.0.0-latest +) diff --git a/examples/gno.land/p/n2p5/haystack/haystack.gno b/examples/gno.land/p/n2p5/haystack/haystack.gno new file mode 100644 index 00000000000..0ab4953acb6 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/haystack.gno @@ -0,0 +1,99 @@ +package haystack + +import ( + "encoding/hex" + "errors" + + "gno.land/p/demo/avl" + "gno.land/p/n2p5/haystack/needle" +) + +var ( + // ErrorNeedleNotFound is returned when a needle is not found in the haystack. + ErrorNeedleNotFound = errors.New("needle not found") + // ErrorNeedleLength is returned when a needle is not the correct length. + ErrorNeedleLength = errors.New("invalid needle length") + // ErrorHashLength is returned when a needle hash is not the correct length. + ErrorHashLength = errors.New("invalid hash length") + // ErrorDuplicateNeedle is returned when a needle already exists in the haystack. + ErrorDuplicateNeedle = errors.New("needle already exists") + // ErrorHashMismatch is returned when a needle hash does not match the needle. This should + // never happen and indicates a critical internal storage error. + ErrorHashMismatch = errors.New("storage error: hash mismatch") + // ErrorValueInvalidType is returned when a needle value is not a byte slice. This should + // never happen and indicates a critical internal storage error. + ErrorValueInvalidType = errors.New("storage error: invalid value type, expected []byte") +) + +const ( + // EncodedHashLength is the length of the hex-encoded needle hash. + EncodedHashLength = needle.HashLength * 2 + // EncodedPayloadLength is the length of the hex-encoded needle payload. + EncodedPayloadLength = needle.PayloadLength * 2 + // EncodedNeedleLength is the length of the hex-encoded needle. + EncodedNeedleLength = EncodedHashLength + EncodedPayloadLength +) + +// Haystack is a permissionless, append-only, content-addressed key-value store for fix +// length messages known as needles. A needle is a 192 byte byte slice with a 32 byte +// hash (sha256) and a 160 byte payload. +type Haystack struct{ internal *avl.Tree } + +// New creates a new instance of a Haystack key-value store. +func New() *Haystack { + return &Haystack{ + internal: avl.NewTree(), + } +} + +// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value +// store. The key is the first 32 bytes of the needle hash (64 bytes hex-encoded) of the +// sha256 sum of the payload. The value is the 160 byte byte slice of the needle payload. +// An error is returned if the needle is found to be invalid. +func (h *Haystack) Add(needleHex string) error { + if len(needleHex) != EncodedNeedleLength { + return ErrorNeedleLength + } + b, err := hex.DecodeString(needleHex) + if err != nil { + return err + } + n, err := needle.FromBytes(b) + if err != nil { + return err + } + if h.internal.Has(needleHex[:EncodedHashLength]) { + return ErrorDuplicateNeedle + } + h.internal.Set(needleHex[:EncodedHashLength], n.Payload()) + return nil +} + +// Get takes a hex-encoded needle hash and returns the complete hex-encoded needle bytes +// and an error. Errors covers errors that span from the needle not being found, internal +// storage error inconsistencies, and invalid value types. +func (h *Haystack) Get(hash string) (string, error) { + if len(hash) != EncodedHashLength { + return "", ErrorHashLength + } + if _, err := hex.DecodeString(hash); err != nil { + return "", err + } + v, ok := h.internal.Get(hash) + if !ok { + return "", ErrorNeedleNotFound + } + b, ok := v.([]byte) + if !ok { + return "", ErrorValueInvalidType + } + n, err := needle.New(b) + if err != nil { + return "", err + } + needleHash := hex.EncodeToString(n.Hash()) + if needleHash != hash { + return "", ErrorHashMismatch + } + return hex.EncodeToString(n.Bytes()), nil +} diff --git a/examples/gno.land/p/n2p5/haystack/haystack_test.gno b/examples/gno.land/p/n2p5/haystack/haystack_test.gno new file mode 100644 index 00000000000..8291a101d73 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/haystack_test.gno @@ -0,0 +1,94 @@ +package haystack + +import ( + "encoding/hex" + "testing" + + "gno.land/p/n2p5/haystack/needle" +) + +func TestHaystack(t *testing.T) { + t.Parallel() + + t.Run("New", func(t *testing.T) { + t.Parallel() + h := New() + if h == nil { + t.Error("New returned nil") + } + }) + + t.Run("Add", func(t *testing.T) { + t.Parallel() + h := New() + n, _ := needle.New(make([]byte, needle.PayloadLength)) + validNeedleHex := hex.EncodeToString(n.Bytes()) + + testTable := []struct { + needleHex string + err error + }{ + {validNeedleHex, nil}, + {validNeedleHex, ErrorDuplicateNeedle}, + {"bad" + validNeedleHex[3:], needle.ErrorInvalidHash}, + {"XXX" + validNeedleHex[3:], hex.InvalidByteError('X')}, + {validNeedleHex[:len(validNeedleHex)-2], ErrorNeedleLength}, + {validNeedleHex + "00", ErrorNeedleLength}, + {"000", ErrorNeedleLength}, + } + for _, tt := range testTable { + err := h.Add(tt.needleHex) + if err != tt.err { + t.Error(tt.needleHex, err.Error(), "!=", tt.err.Error()) + } + } + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + h := New() + + // genNeedleHex returns a hex-encoded needle and its hash for a given index. + genNeedleHex := func(i int) (string, string) { + b := make([]byte, needle.PayloadLength) + b[0] = byte(i) + n, _ := needle.New(b) + return hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash()) + } + + // Add a valid needle to the haystack. + validNeedleHex, validHash := genNeedleHex(0) + h.Add(validNeedleHex) + + // Add a needle and break the value type. + _, brokenHashValueType := genNeedleHex(1) + h.internal.Set(brokenHashValueType, 0) + + // Add a needle with invalid hash. + _, invalidHash := genNeedleHex(2) + h.internal.Set(invalidHash, make([]byte, needle.PayloadLength)) + + testTable := []struct { + hash string + expected string + err error + }{ + {validHash, validNeedleHex, nil}, + {validHash[:len(validHash)-2], "", ErrorHashLength}, + {validHash + "00", "", ErrorHashLength}, + {"XXX" + validHash[3:], "", hex.InvalidByteError('X')}, + {"bad" + validHash[3:], "", ErrorNeedleNotFound}, + {brokenHashValueType, "", ErrorValueInvalidType}, + {invalidHash, "", ErrorHashMismatch}, + } + for _, tt := range testTable { + actual, err := h.Get(tt.hash) + if err != tt.err { + t.Error(tt.hash, err.Error(), "!=", tt.err.Error()) + } + if actual != tt.expected { + t.Error(tt.hash, actual, "!=", tt.expected) + } + } + }) +} diff --git a/examples/gno.land/p/n2p5/haystack/needle/gno.mod b/examples/gno.land/p/n2p5/haystack/needle/gno.mod new file mode 100644 index 00000000000..91f489282cf --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/needle/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/haystack/needle diff --git a/examples/gno.land/p/n2p5/haystack/needle/needle.gno b/examples/gno.land/p/n2p5/haystack/needle/needle.gno new file mode 100644 index 00000000000..971bc31599a --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/needle/needle.gno @@ -0,0 +1,91 @@ +package needle + +import ( + "bytes" + "crypto/sha256" + "errors" +) + +const ( + // HashLength is the length in bytes of the hash prefix in any message + HashLength = 32 + // PayloadLength is the length of the remaining bytes of the message. + PayloadLength = 160 + // NeedleLength is the number of bytes required for a valid needle. + NeedleLength = HashLength + PayloadLength +) + +// Needle is a container for a 160 byte payload +// and a 32 byte sha256 hash of the payload. +type Needle struct { + hash [HashLength]byte + payload [PayloadLength]byte +} + +var ( + // ErrorInvalidHash is an error for in invalid hash + ErrorInvalidHash = errors.New("invalid hash") + // ErrorByteSliceLength is an error for an invalid byte slice length passed in to New or FromBytes + ErrorByteSliceLength = errors.New("invalid byte slice length") +) + +// New creates a Needle used for submitting a payload to a Haystack sever. It takes a Payload +// byte slice that is 160 bytes in length and returns a reference to a +// Needle and an error. The purpose of this function is to make it +// easy to create a new Needle from a payload. This function handles creating a sha256 +// hash of the payload, which is used by the Needle to submit to a haystack server. +func New(p []byte) (*Needle, error) { + if len(p) != PayloadLength { + return nil, ErrorByteSliceLength + } + var n Needle + sum := sha256.Sum256(p) + copy(n.hash[:], sum[:]) + copy(n.payload[:], p) + return &n, nil +} + +// FromBytes is intended convert raw bytes (from UDP or storage) into a Needle. +// It takes a byte slice and expects it to be exactly the length of NeedleLength. +// The byte slice should consist of the first 32 bytes being the sha256 hash of the +// payload and the payload bytes. This function verifies the length of the byte slice, +// copies the bytes into a private [192]byte array, and validates the Needle. It returns +// a reference to a Needle and an error. +func FromBytes(b []byte) (*Needle, error) { + if len(b) != NeedleLength { + return nil, ErrorByteSliceLength + } + var n Needle + copy(n.hash[:], b[:HashLength]) + copy(n.payload[:], b[HashLength:]) + if err := n.validate(); err != nil { + return nil, err + } + return &n, nil +} + +// Hash returns a copy of the bytes of the sha256 256 hash of the Needle payload. +func (n *Needle) Hash() []byte { + return n.Bytes()[:HashLength] +} + +// Payload returns a byte slice of the Needle payload +func (n *Needle) Payload() []byte { + return n.Bytes()[HashLength:] +} + +// Bytes returns a byte slice of the entire 192 byte hash + payload +func (n *Needle) Bytes() []byte { + b := make([]byte, NeedleLength) + copy(b, n.hash[:]) + copy(b[HashLength:], n.payload[:]) + return b +} + +// validate checks that a Needle has a valid hash, it returns either nil or an error. +func (n *Needle) validate() error { + if hash := sha256.Sum256(n.Payload()); !bytes.Equal(n.Hash(), hash[:]) { + return ErrorInvalidHash + } + return nil +} diff --git a/examples/gno.land/p/n2p5/haystack/needle/needle_test.gno b/examples/gno.land/p/n2p5/haystack/needle/needle_test.gno new file mode 100644 index 00000000000..aa81750fc00 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/needle/needle_test.gno @@ -0,0 +1,157 @@ +package needle + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "testing" +) + +func TestNeedle(t *testing.T) { + t.Parallel() + t.Run("Bytes", func(t *testing.T) { + t.Parallel() + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + n, _ := New(p) + b := n.Bytes() + b[0], b[1], b[2], b[3] = 0, 0, 0, 0 + if bytes.Equal(n.Bytes(), b) { + t.Error("mutating Bytes() changed needle bytes") + } + }) + t.Run("Payload", func(t *testing.T) { + t.Parallel() + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + n, _ := New(p) + payload := n.Payload() + if !bytes.Equal(p, payload) { + t.Error("payload imported by New does not match needle.Payload()") + } + payload[0] = 0 + pl := n.Payload() + if bytes.Equal(pl, payload) { + t.Error("mutating Payload() changed needle payload") + } + }) + t.Run("Hash", func(t *testing.T) { + t.Parallel() + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + n, _ := New(p) + hash := n.Hash() + h := sha256.Sum256(p) + if !bytes.Equal(h[:], hash) { + t.Error("exported hash is invalid") + } + hash[0] = 0 + h2 := n.Hash() + if bytes.Equal(h2, hash) { + t.Error("mutating Hash() changed needle hash") + } + }) +} + +func TestNew(t *testing.T) { + t.Parallel() + + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + expected, _ := hex.DecodeString("f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + + testTable := []struct { + payload []byte + expected []byte + hasError bool + description string + }{ + { + payload: p, + expected: expected, + hasError: false, + description: "expected payload", + }, + { + payload: p[:PayloadLength-1], + expected: nil, + hasError: true, + description: "payload invalid length (too small)", + }, + { + payload: append(p, byte(1)), + expected: nil, + hasError: true, + description: "payload invalid length (too large)", + }, + } + + for _, test := range testTable { + n, err := New(test.payload) + if err != nil { + if !test.hasError { + t.Errorf("test: %v had error: %v", test.description, err) + } + } else if !bytes.Equal(n.Bytes(), test.expected) { + t.Errorf("%v, bytes not equal\n%x\n%x", test.description, n.Bytes(), test.expected) + } + } +} + +func TestFromBytes(t *testing.T) { + t.Parallel() + + validRaw, _ := hex.DecodeString("f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + validExpected, _ := hex.DecodeString("f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + invalidHash, _ := hex.DecodeString("182e0ca0d2fb1da76da6caf36a9d0d2838655632e85891216dc8b545d8f1410940e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + + testTable := []struct { + rawBytes []byte + expected []byte + hasError bool + description string + }{ + { + rawBytes: validRaw, + expected: validExpected, + hasError: false, + description: "valid raw bytes", + }, + { + rawBytes: make([]byte, 0), + expected: nil, + hasError: true, + description: "empty bytes", + }, + { + rawBytes: make([]byte, NeedleLength-1), + expected: nil, + hasError: true, + description: "too few bytes, one less than expected", + }, + { + rawBytes: make([]byte, 0), + expected: nil, + hasError: true, + description: "too few bytes, no bytes", + }, + { + rawBytes: make([]byte, NeedleLength+1), + expected: nil, + hasError: true, + description: "too many bytes", + }, + { + rawBytes: invalidHash, + expected: nil, + hasError: true, + description: "invalid hash", + }, + } + for _, test := range testTable { + n, err := FromBytes(test.rawBytes) + if err != nil { + if !test.hasError { + t.Errorf("test: %v had error: %v", test.description, err) + } + } else if !bytes.Equal(n.Bytes(), test.expected) { + t.Errorf("%v, bytes not equal\n%x\n%x", test.description, n.Bytes(), test.expected) + } + } +} diff --git a/examples/gno.land/r/n2p5/haystack/gno.mod b/examples/gno.land/r/n2p5/haystack/gno.mod new file mode 100644 index 00000000000..9203eb2d3b1 --- /dev/null +++ b/examples/gno.land/r/n2p5/haystack/gno.mod @@ -0,0 +1,8 @@ +module gno.land/r/n2p5/haystack + +require ( + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/p/n2p5/haystack v0.0.0-latest + gno.land/p/n2p5/haystack/needle v0.0.0-latest +) diff --git a/examples/gno.land/r/n2p5/haystack/haystack.gno b/examples/gno.land/r/n2p5/haystack/haystack.gno new file mode 100644 index 00000000000..397de1e3e3d --- /dev/null +++ b/examples/gno.land/r/n2p5/haystack/haystack.gno @@ -0,0 +1,32 @@ +package haystack + +import ( + "gno.land/p/n2p5/haystack" +) + +var storage = haystack.New() + +func Render(path string) string { + return ` +Put a Needle in the Haystack. +` +} + +// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value store. +// If storage encounters an error, it will panic. +func Add(needleHex string) { + err := storage.Add(needleHex) + if err != nil { + panic(err) + } +} + +// Get takes a fixed-length hex-encoded needle hash and returns the hex-encoded needle bytes. +// If storage encounters an error, it will panic. +func Get(hashHex string) string { + needleHex, err := storage.Get(hashHex) + if err != nil { + panic(err) + } + return needleHex +} diff --git a/examples/gno.land/r/n2p5/haystack/haystack_test.gno b/examples/gno.land/r/n2p5/haystack/haystack_test.gno new file mode 100644 index 00000000000..52dadf8bf9e --- /dev/null +++ b/examples/gno.land/r/n2p5/haystack/haystack_test.gno @@ -0,0 +1,70 @@ +package haystack + +import ( + "encoding/hex" + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" + "gno.land/p/n2p5/haystack" + "gno.land/p/n2p5/haystack/needle" +) + +func TestHaystack(t *testing.T) { + t.Parallel() + // needleHex returns a hex-encoded needle and its hash for a given index. + genNeedleHex := func(i int) (string, string) { + b := make([]byte, needle.PayloadLength) + b[0] = byte(i) + n, _ := needle.New(b) + return hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash()) + } + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + + t.Run("Add", func(t *testing.T) { + t.Parallel() + + n1, _ := genNeedleHex(1) + n2, _ := genNeedleHex(2) + n3, _ := genNeedleHex(3) + + std.TestSetOrigCaller(u1) + urequire.NotPanics(t, func() { Add(n1) }) + urequire.PanicsWithMessage(t, + haystack.ErrorDuplicateNeedle.Error(), + func() { + Add(n1) + }) + std.TestSetOrigCaller(u2) + urequire.NotPanics(t, func() { Add(n2) }) + urequire.NotPanics(t, func() { Add(n3) }) + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + + n1, h1 := genNeedleHex(4) + _, h2 := genNeedleHex(5) + + std.TestSetOrigCaller(u1) + urequire.NotPanics(t, func() { Add(n1) }) + urequire.NotPanics(t, func() { + result := Get(h1) + urequire.Equal(t, n1, result) + }) + + std.TestSetOrigCaller(u2) + urequire.NotPanics(t, func() { + result := Get(h1) + urequire.Equal(t, n1, result) + }) + urequire.PanicsWithMessage(t, + haystack.ErrorNeedleNotFound.Error(), + func() { + Get(h2) + }) + }) +} From 6c3cc02902eb8b0842ea49f78998a2134ad49d7a Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:00:41 -0600 Subject: [PATCH 227/345] chore(examples): update README (#3116) Updates the examples README.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: Danny --- examples/README.md | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/examples/README.md b/examples/README.md index b112e564d13..758f0f586e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,21 +1,37 @@ -# Gnolang examples +# Examples -This folder showcases Gnolang realms and library demos. These examples not only aid in engine testing but also provide a glimpse into the potential of Gnolang's capabilities. +This folder showcases example Gno realms (smart contracts) and pure packages (libraries). +These examples provide a glimpse into the potential of gno.land and the capabilities of Gno, +while also serving as a test suite for the GnoVM. -While sharing contracts here can enhance engine testing, it's not mandatory. If considering a separate repository for contracts, be aware that this might restrict the experience due to the continuous efforts around `gno mod` support. A key point to note is that the main repository cannot reference separate code, which might pose developmental challenges. +Pure packages and realms in this folder are pre-deployed to gno.land testnets, +making them readily available for on-chain use. However, **there is no guarantee +that the code is bug-free, so it should be used with caution and an understanding of potential risks.** -## Personal Realms & Shared Content - -**Prioritizing Shared Content:** As we expand our examples and use-cases, it's essential to prioritize shared content that benefits the broader community. These examples serve as a foundation and reference for all users. - -**Personal Realms Inclusion:** We're open to personal realms, but they must exemplify best practices and inspire others. To maintain our repository's organization, we may decline some realms. If so, consider uploading onchain and keeping source code separately. For higher acceptance odds, offer useful or original examples. +## Structure -**Recommended Approach:** -- Use `r/demo` and `p/demo` for generic examples and components that can be imported by others. These are meant to be easily referenced and utilized by the community. -- Personal realms are welcomed if they are easily maintainable with the Continuous Integration (CI) system. If a personal realm becomes cumbersome to maintain or doesn't align with the CI's checks, it might be relocated to a less prominent location or even removed. +This folder mimics the gno.land package path system; the "root" of the system is +the `gno.land` folder. Next, it branches out to `p/` and `r/`, which contain +pure packages and realms, respectively. -## Usage - -Our recommendation is to use the [gno](../gnovm/cmd/gno) utility to develop contracts locally before publishing them on-chain. This approach offers a faster and streamlined workflow, along with additional debugging features. Simply fork or create new contracts and refer to the Makefile. Once everything looks good locally, you can then publish it on a localnet or testnet. +## Personal Realms & Shared Content -For further guidance and insights, please refer to the [`awesome-gno` tutorials](https://github.com/gnolang/awesome-gno#tutorials). +**Prioritizing Shared Content:** As we expand our examples and use-cases, it's +essential to prioritize shared content that benefits the broader community. +These examples serve as a foundation and reference for all users. + +**Personal Realms & Pure Packages:** We welcome personal realms that +exemplify best practices and inspire others. To maintain the organization +of the monorepo, some submissions may be declined. If so, consider uploading +[permissionlessly](../docs/gno-tooling/cli/gnokey/state-changing-calls.md#addpackage) +and storing the source code in a separate repo. For higher +acceptance odds, offer useful and original examples. + +**Recommended Approach:** +- Use `r/demo` and `p/demo` for generic examples and components that can be + imported by others. These are meant to be easily referenced and utilized by the + community. +- Packages under personal namespaces, such as in [r/leon](./gno.land/r/leon), + are welcome if they are easily maintainable with the Continuous Integration (CI) + system. If a personal realm becomes cumbersome to maintain or doesn't align with + the CI's checks, it might be relocated to a less prominent location or even removed. \ No newline at end of file From 1993c69c84f8adc8824934f07c8f6180789a8aad Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:12:23 -0600 Subject: [PATCH 228/345] feat(examples): hall of fame (#2842) ## Description Depends on #2584 for `avlpager` Introduces the `r/demo/hof` realm. The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by importing the Hall of Fame realm and calling `hof.Register()` from their `init` function. The realm is moderated and the registrations be paused at will. ![Screenshot 2024-10-07 at 20 09 43](https://github.com/user-attachments/assets/9beeefc6-d22a-4e81-aa2d-e336d0e6edf8)
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests - [x] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Antonio Navarro Perez --- examples/gno.land/p/demo/fqname/fqname.gno | 7 +- examples/gno.land/p/demo/ownable/ownable.gno | 6 +- .../gno.land/p/demo/pausable/pausable.gno | 10 +- .../gno.land/r/demo/hof/administration.gno | 24 ++++ examples/gno.land/r/demo/hof/errors.gno | 11 ++ examples/gno.land/r/demo/hof/gno.mod | 15 ++ examples/gno.land/r/demo/hof/hof.gno | 132 +++++++++++++++++ examples/gno.land/r/demo/hof/hof_test.gno | 134 ++++++++++++++++++ examples/gno.land/r/demo/hof/render.gno | 113 +++++++++++++++ examples/gno.land/r/gnoland/home/gno.mod | 1 + examples/gno.land/r/gnoland/home/home.gno | 14 +- .../gno.land/r/gnoland/home/home_filetest.gno | 6 +- examples/gno.land/r/leon/home/gno.mod | 1 + examples/gno.land/r/leon/home/home.gno | 3 + examples/gno.land/r/manfred/home/gno.mod | 5 +- examples/gno.land/r/manfred/home/home.gno | 6 +- examples/gno.land/r/morgan/home/gno.mod | 2 + examples/gno.land/r/morgan/home/home.gno | 4 + 18 files changed, 482 insertions(+), 12 deletions(-) create mode 100644 examples/gno.land/r/demo/hof/administration.gno create mode 100644 examples/gno.land/r/demo/hof/errors.gno create mode 100644 examples/gno.land/r/demo/hof/gno.mod create mode 100644 examples/gno.land/r/demo/hof/hof.gno create mode 100644 examples/gno.land/r/demo/hof/hof_test.gno create mode 100644 examples/gno.land/r/demo/hof/render.gno diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno index 8cccdb9e8b7..07d9e4b4621 100644 --- a/examples/gno.land/p/demo/fqname/fqname.gno +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -4,7 +4,9 @@ // package-level declaration. package fqname -import "strings" +import ( + "strings" +) // Parse splits a fully qualified identifier into its package path and name // components. It handles cases with and without slashes in the package path. @@ -63,10 +65,13 @@ func RenderLink(pkgPath, slug string) string { if slug != "" { return "[" + pkgPath + "](" + pkgLink + ")." + slug } + return "[" + pkgPath + "](" + pkgLink + ")" } + if slug != "" { return pkgPath + "." + slug } + return pkgPath } diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index a77b22461a9..48a1c15fffa 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -37,8 +37,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error { o.owner = newOwner std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), - "to", string(newOwner), + "from", prevOwner.String(), + "to", newOwner.String(), ) return nil @@ -58,7 +58,7 @@ func (o *Ownable) DropOwnership() error { std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), + "from", prevOwner.String(), "to", "", ) diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index eae3456ba61..e9cce63c1e3 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -1,6 +1,10 @@ package pausable -import "gno.land/p/demo/ownable" +import ( + "std" + + "gno.land/p/demo/ownable" +) type Pausable struct { *ownable.Ownable @@ -35,6 +39,8 @@ func (p *Pausable) Pause() error { } p.paused = true + std.Emit("Paused", "account", p.Owner().String()) + return nil } @@ -45,5 +51,7 @@ func (p *Pausable) Unpause() error { } p.paused = false + std.Emit("Unpaused", "account", p.Owner().String()) + return nil } diff --git a/examples/gno.land/r/demo/hof/administration.gno b/examples/gno.land/r/demo/hof/administration.gno new file mode 100644 index 00000000000..4b5b212eddf --- /dev/null +++ b/examples/gno.land/r/demo/hof/administration.gno @@ -0,0 +1,24 @@ +package hof + +import "std" + +// Exposing the ownable & pausable APIs +// Should not be needed as soon as MsgCall supports calling methods on exported variables + +func Pause() error { + return exhibition.Pause() +} + +func Unpause() error { + return exhibition.Unpause() +} + +func GetOwner() std.Address { + return owner.Owner() +} + +func TransferOwnership(newOwner std.Address) { + if err := owner.TransferOwnership(newOwner); err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/demo/hof/errors.gno b/examples/gno.land/r/demo/hof/errors.gno new file mode 100644 index 00000000000..7277f65fa76 --- /dev/null +++ b/examples/gno.land/r/demo/hof/errors.gno @@ -0,0 +1,11 @@ +package hof + +import ( + "errors" +) + +var ( + ErrNoSuchItem = errors.New("hof: no such item exists") + ErrDoubleUpvote = errors.New("hof: cannot upvote twice") + ErrDoubleDownvote = errors.New("hof: cannot downvote twice") +) diff --git a/examples/gno.land/r/demo/hof/gno.mod b/examples/gno.land/r/demo/hof/gno.mod new file mode 100644 index 00000000000..ac5c91295a6 --- /dev/null +++ b/examples/gno.land/r/demo/hof/gno.mod @@ -0,0 +1,15 @@ +module gno.land/r/demo/hof + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/avl/pager v0.0.0-latest + gno.land/p/demo/fqname v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/pausable v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/p/moul/txlink v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/hof/hof.gno b/examples/gno.land/r/demo/hof/hof.gno new file mode 100644 index 00000000000..2722c019497 --- /dev/null +++ b/examples/gno.land/r/demo/hof/hof.gno @@ -0,0 +1,132 @@ +// Package hof is the hall of fame realm. +// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by +// importing the Hall of Fame realm and calling hof.Register() from their init function. +package hof + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/pausable" + "gno.land/p/demo/seqid" +) + +var ( + exhibition *Exhibition + owner *ownable.Ownable +) + +type ( + Exhibition struct { + itemCounter seqid.ID + description string + items *avl.Tree // pkgPath > Item + itemsSorted *avl.Tree // same data but sorted, storing pointers + *pausable.Pausable + } + + Item struct { + id seqid.ID + pkgpath string + blockNum int64 + upvote *avl.Tree // std.Addr > struct{}{} + downvote *avl.Tree // std.Addr > struct{}{} + } +) + +func init() { + exhibition = &Exhibition{ + items: avl.NewTree(), + itemsSorted: avl.NewTree(), + } + + owner = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + exhibition.Pausable = pausable.NewFromOwnable(owner) +} + +// Register registers your realm to the Hall of Fame +// Should be called from within code +func Register() { + if exhibition.IsPaused() { + return + } + + submission := std.PrevRealm() + pkgpath := submission.PkgPath() + + // Must be called from code + if submission.IsUser() { + return + } + + // Must not yet exist + if exhibition.items.Has(pkgpath) { + return + } + + id := exhibition.itemCounter.Next() + i := &Item{ + id: id, + pkgpath: pkgpath, + blockNum: std.GetHeight(), + upvote: avl.NewTree(), + downvote: avl.NewTree(), + } + + exhibition.items.Set(pkgpath, i) + exhibition.itemsSorted.Set(id.String(), i) + + std.Emit("Registration") +} + +func Upvote(pkgpath string) { + rawItem, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + item := rawItem.(*Item) + caller := std.PrevRealm().Addr().String() + + if item.upvote.Has(caller) { + panic(ErrDoubleUpvote.Error()) + } + + item.upvote.Set(caller, struct{}{}) +} + +func Downvote(pkgpath string) { + rawItem, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + item := rawItem.(*Item) + caller := std.PrevRealm().Addr().String() + + if item.downvote.Has(caller) { + panic(ErrDoubleDownvote.Error()) + } + + item.downvote.Set(caller, struct{}{}) +} + +func Delete(pkgpath string) { + if err := owner.CallerIsOwner(); err != nil { + panic(err) + } + + i, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed { + panic(ErrNoSuchItem.Error()) + } + + if _, removed := exhibition.items.Remove(pkgpath); !removed { + panic(ErrNoSuchItem.Error()) + } +} diff --git a/examples/gno.land/r/demo/hof/hof_test.gno b/examples/gno.land/r/demo/hof/hof_test.gno new file mode 100644 index 00000000000..72e8d2159be --- /dev/null +++ b/examples/gno.land/r/demo/hof/hof_test.gno @@ -0,0 +1,134 @@ +package hof + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +const rlmPath = "gno.land/r/gnoland/home" + +var ( + admin = owner.Owner() + adminRealm = std.NewUserRealm(admin) + alice = testutils.TestAddress("alice") +) + +func TestRegister(t *testing.T) { + // Test user realm register + aliceRealm := std.NewUserRealm(alice) + std.TestSetRealm(aliceRealm) + + Register() + uassert.False(t, itemExists(t, rlmPath)) + + // Test register while paused + std.TestSetRealm(adminRealm) + Pause() + + // Set legitimate caller + std.TestSetRealm(std.NewCodeRealm(rlmPath)) + + Register() + uassert.False(t, itemExists(t, rlmPath)) + + // Unpause + std.TestSetRealm(adminRealm) + Unpause() + + // Set legitimate caller + std.TestSetRealm(std.NewCodeRealm(rlmPath)) + Register() + + // Find registered items + uassert.True(t, itemExists(t, rlmPath)) +} + +func TestUpvote(t *testing.T) { + raw, _ := exhibition.items.Get(rlmPath) + item := raw.(*Item) + + rawSorted, _ := exhibition.itemsSorted.Get(item.id.String()) + itemSorted := rawSorted.(*Item) + + // 0 upvotes by default + urequire.Equal(t, item.upvote.Size(), 0) + + std.TestSetRealm(adminRealm) + + urequire.NotPanics(t, func() { + Upvote(rlmPath) + }) + + // Check both trees for 1 upvote + uassert.Equal(t, item.upvote.Size(), 1) + uassert.Equal(t, itemSorted.upvote.Size(), 1) + + // Check double upvote + uassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() { + Upvote(rlmPath) + }) +} + +func TestDownvote(t *testing.T) { + raw, _ := exhibition.items.Get(rlmPath) + item := raw.(*Item) + + rawSorted, _ := exhibition.itemsSorted.Get(item.id.String()) + itemSorted := rawSorted.(*Item) + + // 0 downvotes by default + urequire.Equal(t, item.downvote.Size(), 0) + + userRealm := std.NewUserRealm(alice) + std.TestSetRealm(userRealm) + + urequire.NotPanics(t, func() { + Downvote(rlmPath) + }) + + // Check both trees for 1 upvote + uassert.Equal(t, item.downvote.Size(), 1) + uassert.Equal(t, itemSorted.downvote.Size(), 1) + + // Check double downvote + uassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() { + Downvote(rlmPath) + }) +} + +func TestDelete(t *testing.T) { + userRealm := std.NewUserRealm(admin) + std.TestSetRealm(userRealm) + std.TestSetOrigCaller(admin) + + uassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() { + Delete("nonexistentpkgpath") + }) + + i, _ := exhibition.items.Get(rlmPath) + id := i.(*Item).id + + uassert.NotPanics(t, func() { + Delete(rlmPath) + }) + + uassert.False(t, exhibition.items.Has(rlmPath)) + uassert.False(t, exhibition.itemsSorted.Has(id.String())) +} + +func itemExists(t *testing.T, rlmPath string) bool { + t.Helper() + + i, ok1 := exhibition.items.Get(rlmPath) + ok2 := false + + if ok1 { + _, ok2 = exhibition.itemsSorted.Get(i.(*Item).id.String()) + } + + return ok1 && ok2 +} diff --git a/examples/gno.land/r/demo/hof/render.gno b/examples/gno.land/r/demo/hof/render.gno new file mode 100644 index 00000000000..6b06ef04051 --- /dev/null +++ b/examples/gno.land/r/demo/hof/render.gno @@ -0,0 +1,113 @@ +package hof + +import ( + "strings" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/fqname" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +const ( + pageSize = 5 +) + +func Render(path string) string { + out := "# Hall of Fame\n\n" + + dashboardEnabled := path == "dashboard" + + if dashboardEnabled { + out += renderDashboard() + } + + out += exhibition.Render(path, dashboardEnabled) + + return out +} + +func (e Exhibition) Render(path string, dashboard bool) string { + out := ufmt.Sprintf("%s\n\n", e.description) + + if e.items.Size() == 0 { + out += "No items in this exhibition currently.\n\n" + return out + } + + out += "
\n\n" + + page := pager.NewPager(e.itemsSorted, pageSize).MustGetPageByPath(path) + + for i := len(page.Items) - 1; i >= 0; i-- { + item := page.Items[i] + + out += "
\n\n" + id, _ := seqid.FromString(item.Key) + out += ufmt.Sprintf("### Submission #%d\n\n", int(id)) + out += item.Value.(*Item).Render(dashboard) + out += "
" + } + + out += "
\n\n" + + out += page.Selector() + + return out +} + +func (i Item) Render(dashboard bool) string { + out := ufmt.Sprintf("\n```\n%s\n```\n\n", i.pkgpath) + out += ufmt.Sprintf("by %s\n\n", strings.Split(i.pkgpath, "/")[2]) + out += ufmt.Sprintf("[View realm](%s)\n\n", strings.TrimPrefix(i.pkgpath, "gno.land")) // gno.land/r/leon/home > /r/leon/home + out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) + + out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n", + i.upvote.Size(), txlink.URL("Upvote", "pkgpath", i.pkgpath), + i.downvote.Size(), txlink.URL("Downvote", "pkgpath", i.pkgpath), + ) + + if dashboard { + out += ufmt.Sprintf("[Delete](%s)", txlink.URL("Delete", "pkgpath", i.pkgpath)) + } + + return out +} + +func renderDashboard() string { + out := "---\n\n" + out += "## Dashboard\n\n" + out += ufmt.Sprintf("Total submissions: %d\n\n", exhibition.items.Size()) + + out += ufmt.Sprintf("Exhibition admin: %s\n\n", owner.Owner().String()) + + if !exhibition.IsPaused() { + out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.URL("Pause")) + } else { + out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.URL("Unpause")) + } + + out += "---\n\n" + + return out +} + +func RenderExhibWidget(itemsToRender int) string { + if itemsToRender < 1 { + return "" + } + + out := "" + i := 0 + exhibition.items.Iterate("", "", func(key string, value interface{}) bool { + item := value.(*Item) + + out += ufmt.Sprintf("- %s\n", fqname.RenderLink(item.pkgpath, "")) + + i++ + return i >= itemsToRender + }) + + return out +} diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index c208ad421c9..ff52ef4c8b1 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -4,6 +4,7 @@ require ( gno.land/p/demo/ownable v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/ui v0.0.0-latest + gno.land/r/demo/hof v0.0.0-latest gno.land/r/gnoland/blog v0.0.0-latest gno.land/r/gnoland/events v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index c6b3929a16c..ce976923ef5 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" "gno.land/p/demo/ui" + "gno.land/r/demo/hof" blog "gno.land/r/gnoland/blog" events "gno.land/r/gnoland/events" ) @@ -37,7 +38,7 @@ func Render(_ string) string { ui.Columns{3, []ui.Element{ lastBlogposts(4), upcomingEvents(), - lastContributions(4), + latestHOFItems(5), }}, ) @@ -90,6 +91,15 @@ func upcomingEvents() ui.Element { } } +func latestHOFItems(num int) ui.Element { + submissions := hof.RenderExhibWidget(num) + + return ui.Element{ + ui.H3("[Hall of Fame](/r/demo/hof)"), + ui.Text(submissions), + } +} + func introSection() ui.Element { return ui.Element{ ui.H3("We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts."), @@ -270,7 +280,7 @@ func discoverLinks() ui.Element { - [Gnoscan](https://gnoscan.io) - [Portal Loop](https://docs.gno.land/concepts/portal-loop) - [Testnet 4](https://test4.gno.land/) -- Testnet Faucet Hub (soon) +- [Faucet Hub](https://faucet.gno.land) `), diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index b22c22567b3..c587af9b817 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -57,7 +57,7 @@ func main() { // - [Gnoscan](https://gnoscan.io) // - [Portal Loop](https://docs.gno.land/concepts/portal-loop) // - [Testnet 4](https://test4.gno.land/) -// - Testnet Faucet Hub (soon) +// - [Faucet Hub](https://faucet.gno.land) // // // @@ -78,9 +78,9 @@ func main() { // //
// -// ### Latest Contributions +// ### [Hall of Fame](/r/demo/hof) +// // -// [View latest contributions](https://github.com/gnolang/gno/pulls) //
// // diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index 48cf64a9d0a..4649cf4abe6 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -4,5 +4,6 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/r/demo/art/gnoface v0.0.0-latest gno.land/r/demo/art/millipede v0.0.0-latest + gno.land/r/demo/hof v0.0.0-latest gno.land/r/leon/config v0.0.0-latest ) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index ba688792a4c..aea8b43e9cd 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -8,6 +8,7 @@ import ( "gno.land/r/demo/art/gnoface" "gno.land/r/demo/art/millipede" + "gno.land/r/demo/hof" "gno.land/r/leon/config" ) @@ -31,6 +32,8 @@ My contributions to gno.land can mainly be found TODO import r/gh `, } + + hof.Register() } func UpdatePFP(url, caption string) { diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod index 6e7aac70cc7..9885cac19c2 100644 --- a/examples/gno.land/r/manfred/home/gno.mod +++ b/examples/gno.land/r/manfred/home/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/manfred/home -require gno.land/r/manfred/config v0.0.0-latest +require ( + gno.land/r/demo/hof v0.0.0-latest + gno.land/r/manfred/config v0.0.0-latest +) diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno index 720796a2201..4766f54e51f 100644 --- a/examples/gno.land/r/manfred/home/home.gno +++ b/examples/gno.land/r/manfred/home/home.gno @@ -1,6 +1,9 @@ package home -import "gno.land/r/manfred/config" +import ( + "gno.land/r/demo/hof" + "gno.land/r/manfred/config" +) var ( todos []string @@ -12,6 +15,7 @@ func init() { todos = append(todos, "fill this todo list...") status = "Online" // Initial status set to "Online" memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" + hof.Register() } func Render(path string) string { diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod index 573a7e139e7..35e2fbb2119 100644 --- a/examples/gno.land/r/morgan/home/gno.mod +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -1 +1,3 @@ module gno.land/r/morgan/home + +require gno.land/r/demo/hof v0.0.0-latest diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno index 33d7e0b2df7..571f14ed5ec 100644 --- a/examples/gno.land/r/morgan/home/home.gno +++ b/examples/gno.land/r/morgan/home/home.gno @@ -1,10 +1,14 @@ package home +import "gno.land/r/demo/hof" + const staticHome = `# morgan's (gn)home - [📝 sign my guestbook](/r/morgan/guestbook) ` +func init() { hof.Register() } + func Render(path string) string { return staticHome } From 38736e754a46cc9f91ae22516d6fd100783271df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:47:01 +0100 Subject: [PATCH 229/345] chore(deps): bump the actions group across 1 directory with 2 updates (#3114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions group with 2 updates in the / directory: [coursier/setup-action](https://github.com/coursier/setup-action) and [anchore/sbom-action](https://github.com/anchore/sbom-action). Updates `coursier/setup-action` from 1.3.6 to 1.3.8
Release notes

Sourced from coursier/setup-action's releases.

v1.3.8

What's Changed

Updates / maintenance

Full Changelog: https://github.com/coursier/setup-action/compare/v1...v1.3.8

v1.3.7

What's Changed

Updates / maintenance

Full Changelog: https://github.com/coursier/setup-action/compare/v1...v1.3.7

Commits
  • c741af3 Update dist (#708)
  • 2a75cb1 Run CI on Mac ARM too (#707)
  • 56a39e8 Run CI against JDK 21 rather than 17 (#706)
  • e52a786 Merge pull request #705 from alexarchambault/coursier-2.1.17
  • 1f0cf93 Update default coursier version to 2.1.17
  • fb8e01e Use Mac ARM launchers from main repo for coursier >= 2.1.16
  • f4e9717 build(deps-dev): bump @​typescript-eslint/eslint-plugin
  • 1583ce1 build(deps-dev): bump @​typescript-eslint/parser from 8.12.2 to 8.13.0
  • 3f834ee build(deps-dev): bump @​types/node from 22.8.6 to 22.9.0
  • 97ee911 chore: Update macos since it's no longer supported
  • Additional commits viewable in compare view

Updates `anchore/sbom-action` from 0.17.5 to 0.17.7
Release notes

Sourced from anchore/sbom-action's releases.

v0.17.7

Changes in v0.17.7

v0.17.6

Changes in v0.17.6

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/fossa.yml | 2 +- .github/workflows/releaser-master.yml | 2 +- .github/workflows/releaser-nightly.yml | 2 +- .github/workflows/releaser.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 9de8d536b29..f9d3110ba82 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -31,7 +31,7 @@ jobs: uses: coursier/cache-action@v6.4.6 - name: Set up JDK 17 - uses: coursier/setup-action@v1.3.6 + uses: coursier/setup-action@v1.3.8 with: jvm: temurin:1.17 diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 7f81ef1ad1a..eb5698e9d8f 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -27,7 +27,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.5 + - uses: anchore/sbom-action/download-syft@v0.17.7 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index fd4eaa86b0e..aed56526a2f 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -24,7 +24,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.5 + - uses: anchore/sbom-action/download-syft@v0.17.7 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 8bbc9323cad..aeda7ed2c7e 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -24,7 +24,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.5 + - uses: anchore/sbom-action/download-syft@v0.17.7 - uses: docker/login-action@v3 with: From 6c5329d7cd548c5c9b83fc19e675c1c7c6abb925 Mon Sep 17 00:00:00 2001 From: Reza Rahemtola <49811529+RezaRahemtola@users.noreply.github.com> Date: Fri, 15 Nov 2024 03:39:28 +0100 Subject: [PATCH 230/345] docs(gno-js): Add provider instantiation docs (#2427) While using `gno-js` and reading the [related docs](https://docs.gno.land/reference/gno-js-client/gno-js-provider), I saw the message saying that it's based on `tm2-js-client` with related link. This is useful to understand how it works and see the available methods from the base Provider classes, but doesn't inform the developer about how he should instantiate a provider with `gno-js-client`, the name of the providers isn't mentioned anywhere and I had to guess it (or use my IDE autocomplete) and look at other projects using it to find out. This PR adds a small section to the documentation page with explicit instantiation examples to fix this (without repeating too much info, parameters for example are linked to `tm2-js-client`, but could be extended in the future and documented here).
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- docs/reference/gno-js-client/gno-provider.md | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/reference/gno-js-client/gno-provider.md b/docs/reference/gno-js-client/gno-provider.md index 1b9cbd53652..c76bfebfe31 100644 --- a/docs/reference/gno-js-client/gno-provider.md +++ b/docs/reference/gno-js-client/gno-provider.md @@ -7,6 +7,38 @@ id: gno-js-provider The `Gno Provider` is an extension on the `tm2-js-client` `Provider`, outlined [here](../tm2-js-client/Provider/provider.md). Both JSON-RPC and WS providers are included with the package. +## Instantiation + +### new GnoWSProvider + +Creates a new instance of the Gno WebSocket Provider, based on [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md). + +#### Parameters + +Same as [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md). + +#### Usage + +```ts +new GnoWSProvider('ws://staging.gno.land:26657/ws'); +// provider with WS connection is created +``` + +### new GnoJSONRPCProvider + +Creates a new instance of the Gno JSON-RPC Provider, based on [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-provider.md). + +#### Parameters + +Same as [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-provider.md). + +#### Usage + +```ts +new GnoJSONRPCProvider('http://staging.gno.land:36657'); +// provider is created +``` + ## Realm Methods ### getRenderOutput From a1812af67d379e91189c7c28a255116a9bc02e0d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:05:16 +0100 Subject: [PATCH 231/345] feat: add p/moul/mdtable (#3100) Can be useful for #3096. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/mdtable/gno.mod | 3 + examples/gno.land/p/moul/mdtable/mdtable.gno | 66 ++++++++ .../gno.land/p/moul/mdtable/mdtable_test.gno | 158 ++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 examples/gno.land/p/moul/mdtable/gno.mod create mode 100644 examples/gno.land/p/moul/mdtable/mdtable.gno create mode 100644 examples/gno.land/p/moul/mdtable/mdtable_test.gno diff --git a/examples/gno.land/p/moul/mdtable/gno.mod b/examples/gno.land/p/moul/mdtable/gno.mod new file mode 100644 index 00000000000..0cea0458895 --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/moul/mdtable + +require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/p/moul/mdtable/mdtable.gno b/examples/gno.land/p/moul/mdtable/mdtable.gno new file mode 100644 index 00000000000..13812bd973d --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/mdtable.gno @@ -0,0 +1,66 @@ +// Package mdtable provides a simple way to create Markdown tables. +// +// Example usage: +// +// import "gno.land/p/moul/mdtable" +// +// func Render(path string) string { +// table := mdtable.Table{ +// Headers: []string{"ID", "Title", "Status", "Date"}, +// } +// table.Append([]string{"#1", "Add a new validator", "succeed", "2024-01-01"}) +// table.Append([]string{"#2", "Change parameter", "timed out", "2024-01-02"}) +// return table.String() +// } +// +// Output: +// +// | ID | Title | Status | Date | +// | --- | --- | --- | --- | +// | #1 | Add a new validator | succeed | 2024-01-01 | +// | #2 | Change parameter | timed out | 2024-01-02 | +package mdtable + +import ( + "strings" +) + +type Table struct { + Headers []string + Rows [][]string + // XXX: optional headers alignment. +} + +func (t *Table) Append(row []string) { + t.Rows = append(t.Rows, row) +} + +func (t Table) String() string { + // XXX: switch to using text/tabwriter when porting to Gno to support + // better-formatted raw Markdown output. + + if len(t.Headers) == 0 && len(t.Rows) == 0 { + return "" + } + + var sb strings.Builder + + if len(t.Headers) == 0 { + t.Headers = make([]string, len(t.Rows[0])) + } + + // Print header. + sb.WriteString("| " + strings.Join(t.Headers, " | ") + " |\n") + sb.WriteString("|" + strings.Repeat(" --- |", len(t.Headers)) + "\n") + + // Print rows. + for _, row := range t.Rows { + escapedRow := make([]string, len(row)) + for i, cell := range row { + escapedRow[i] = strings.ReplaceAll(cell, "|", "|") // Escape pipe characters. + } + sb.WriteString("| " + strings.Join(escapedRow, " | ") + " |\n") + } + + return sb.String() +} diff --git a/examples/gno.land/p/moul/mdtable/mdtable_test.gno b/examples/gno.land/p/moul/mdtable/mdtable_test.gno new file mode 100644 index 00000000000..87836a3ab11 --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/mdtable_test.gno @@ -0,0 +1,158 @@ +package mdtable_test + +import ( + "testing" + + "gno.land/p/demo/urequire" + "gno.land/p/moul/mdtable" +) + +// XXX: switch to `func Example() {}` when supported. +func TestExample(t *testing.T) { + table := mdtable.Table{ + Headers: []string{"ID", "Title", "Status"}, + Rows: [][]string{ + {"#1", "Add a new validator", "succeed"}, + {"#2", "Change parameter", "timed out"}, + {"#3", "Fill pool", "active"}, + }, + } + + got := table.String() + expected := `| ID | Title | Status | +| --- | --- | --- | +| #1 | Add a new validator | succeed | +| #2 | Change parameter | timed out | +| #3 | Fill pool | active | +` + + urequire.Equal(t, got, expected) +} + +func TestTableString(t *testing.T) { + tests := []struct { + name string + table mdtable.Table + expected string + }{ + { + name: "With Headers and Rows", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Add a new validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "Without Headers", + table: mdtable.Table{ + Rows: [][]string{ + {"#1", "Add a new validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| | | | | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "Without Rows", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +`, + }, + { + name: "With Pipe Character in Content", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Add a new | validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new | validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "With Varying Row Sizes", // XXX: should we have a different behavior? + table: mdtable.Table{ + Headers: []string{"ID", "Title"}, + Rows: [][]string{ + {"#1", "Add a new validator"}, + {"#2", "Change parameter", "Extra Column"}, + {"#3", "Fill pool"}, + }, + }, + expected: `| ID | Title | +| --- | --- | +| #1 | Add a new validator | +| #2 | Change parameter | Extra Column | +| #3 | Fill pool | +`, + }, + { + name: "With UTF-8 Characters", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Café", "succeed", "2024-01-01"}, + {"#2", "München", "timed out", "2024-01-02"}, + {"#3", "São Paulo", "active", "2024-01-03"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Café | succeed | 2024-01-01 | +| #2 | München | timed out | 2024-01-02 | +| #3 | São Paulo | active | 2024-01-03 | +`, + }, + { + name: "With no Headers and no Rows", + table: mdtable.Table{}, + expected: ``, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.table.String() + urequire.Equal(t, got, tt.expected) + }) + } +} + +func TestTableAppend(t *testing.T) { + table := mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + } + + // Use the Append method to add rows to the table + table.Append([]string{"#1", "Add a new validator", "succeed", "2024-01-01"}) + table.Append([]string{"#2", "Change parameter", "timed out", "2024-01-02"}) + table.Append([]string{"#3", "Fill pool", "active", "2024-01-03"}) + got := table.String() + + expected := `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +| #3 | Fill pool | active | 2024-01-03 | +` + urequire.Equal(t, got, expected) +} From a1a7cb3a1a4934c8476f753238bb9e5980dc35ee Mon Sep 17 00:00:00 2001 From: Nemanja Aleksic Date: Sat, 16 Nov 2024 12:34:45 +0900 Subject: [PATCH 232/345] feat: add bug label automatically to the bug report template (#3132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `🐞 bug` label automatically whenever the "bug report" template is used for a new issue. --- .github/ISSUE_TEMPLATE/BUG-REPORT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/.github/ISSUE_TEMPLATE/BUG-REPORT.md index 70a20a4c47e..a63b450d678 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.md @@ -1,6 +1,7 @@ --- name: Bug Report Template about: Create a bug report +labels: "🐞 bug" # NOTE: keep in sync with gnovm/cmd/gno/bug.go --- From 6a13619da958c18b5b907f5dc110417568c61ec2 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:40:29 -0600 Subject: [PATCH 233/345] feat(examples): add hello_world, update `r/demo/event` (#3130) ## Description We don't have a clean & simple hello_world realm. I also updated the doc on the `r/demo/events` realm, and also renamed it to emit.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/r/demo/emit/emit.gno | 12 ++++++++++++ examples/gno.land/r/demo/emit/gno.mod | 1 + .../r/demo/{event => emit}/z1_filetest.gno | 14 +++++++------- examples/gno.land/r/demo/event/event.gno | 9 --------- examples/gno.land/r/demo/event/gno.mod | 1 - examples/gno.land/r/demo/hello_world/gno.mod | 1 + .../gno.land/r/demo/hello_world/hello.gno | 17 +++++++++++++++++ .../r/demo/hello_world/hello_test.gno | 19 +++++++++++++++++++ 8 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 examples/gno.land/r/demo/emit/emit.gno create mode 100644 examples/gno.land/r/demo/emit/gno.mod rename examples/gno.land/r/demo/{event => emit}/z1_filetest.gno (61%) delete mode 100644 examples/gno.land/r/demo/event/event.gno delete mode 100644 examples/gno.land/r/demo/event/gno.mod create mode 100644 examples/gno.land/r/demo/hello_world/gno.mod create mode 100644 examples/gno.land/r/demo/hello_world/hello.gno create mode 100644 examples/gno.land/r/demo/hello_world/hello_test.gno diff --git a/examples/gno.land/r/demo/emit/emit.gno b/examples/gno.land/r/demo/emit/emit.gno new file mode 100644 index 00000000000..a3de8f764a5 --- /dev/null +++ b/examples/gno.land/r/demo/emit/emit.gno @@ -0,0 +1,12 @@ +// Package emit demonstrates how to use the std.Emit() function +// to emit Gno events that can be used to track data changes off-chain. +// std.Emit is variadic; apart from the event name, it can take in any number of key-value pairs to emit. +package emit + +import ( + "std" +) + +func Emit(value string) { + std.Emit("EventName", "key", value) +} diff --git a/examples/gno.land/r/demo/emit/gno.mod b/examples/gno.land/r/demo/emit/gno.mod new file mode 100644 index 00000000000..cf9c2b6b98e --- /dev/null +++ b/examples/gno.land/r/demo/emit/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/emit diff --git a/examples/gno.land/r/demo/event/z1_filetest.gno b/examples/gno.land/r/demo/emit/z1_filetest.gno similarity index 61% rename from examples/gno.land/r/demo/event/z1_filetest.gno rename to examples/gno.land/r/demo/emit/z1_filetest.gno index b138aa4351c..7dcdbf8e0a3 100644 --- a/examples/gno.land/r/demo/event/z1_filetest.gno +++ b/examples/gno.land/r/demo/emit/z1_filetest.gno @@ -1,34 +1,34 @@ package main -import "gno.land/r/demo/event" +import "gno.land/r/demo/emit" func main() { - event.Emit("foo") - event.Emit("bar") + emit.Emit("foo") + emit.Emit("bar") } // Events: // [ // { -// "type": "TAG", +// "type": "EventName", // "attrs": [ // { // "key": "key", // "value": "foo" // } // ], -// "pkg_path": "gno.land/r/demo/event", +// "pkg_path": "gno.land/r/demo/emit", // "func": "Emit" // }, // { -// "type": "TAG", +// "type": "EventName", // "attrs": [ // { // "key": "key", // "value": "bar" // } // ], -// "pkg_path": "gno.land/r/demo/event", +// "pkg_path": "gno.land/r/demo/emit", // "func": "Emit" // } // ] diff --git a/examples/gno.land/r/demo/event/event.gno b/examples/gno.land/r/demo/event/event.gno deleted file mode 100644 index 9e5de540734..00000000000 --- a/examples/gno.land/r/demo/event/event.gno +++ /dev/null @@ -1,9 +0,0 @@ -package event - -import ( - "std" -) - -func Emit(value string) { - std.Emit("TAG", "key", value) -} diff --git a/examples/gno.land/r/demo/event/gno.mod b/examples/gno.land/r/demo/event/gno.mod deleted file mode 100644 index 64987d43d79..00000000000 --- a/examples/gno.land/r/demo/event/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/event diff --git a/examples/gno.land/r/demo/hello_world/gno.mod b/examples/gno.land/r/demo/hello_world/gno.mod new file mode 100644 index 00000000000..9561cd4f077 --- /dev/null +++ b/examples/gno.land/r/demo/hello_world/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/hello_world diff --git a/examples/gno.land/r/demo/hello_world/hello.gno b/examples/gno.land/r/demo/hello_world/hello.gno new file mode 100644 index 00000000000..312520de44d --- /dev/null +++ b/examples/gno.land/r/demo/hello_world/hello.gno @@ -0,0 +1,17 @@ +// Package hello_world demonstrates the usage of the Render() function. +// Render() can be called via the vm/qrender ABCI query off-chain to +// retrieve realm state or any other custom data defined by the realm +// developer. The vm/qrender query allows for additional data to be +// passed in with the call, which can be utilized as the path argument +// to the Render() function. This allows developers to create different +// "renders" of their realms depending on the data which is passed in, +// such as pagination, admin dashboards, and more. +package hello_world + +func Render(path string) string { + if path == "" { + return "# Hello, 世界!" + } + + return "# Hello, " + path + "!" +} diff --git a/examples/gno.land/r/demo/hello_world/hello_test.gno b/examples/gno.land/r/demo/hello_world/hello_test.gno new file mode 100644 index 00000000000..4c3d86c556a --- /dev/null +++ b/examples/gno.land/r/demo/hello_world/hello_test.gno @@ -0,0 +1,19 @@ +package hello_world + +import ( + "testing" +) + +func TestHello(t *testing.T) { + expected := "# Hello, 世界!" + got := Render("") + if got != expected { + t.Fatalf("Expected %s, got %s", expected, got) + } + + got = Render("world") + expected = "# Hello, world!" + if got != expected { + t.Fatalf("Expected %s, got %s", expected, got) + } +} From 02467612e35726ed641e35200f61736929a891c0 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Mon, 18 Nov 2024 10:08:06 +0100 Subject: [PATCH 234/345] fix: branch stmts (#3043) Adds validation for `break`, `continue` and `fallthrough` to disallow invalid code. Moves existing validation from the runtime to the preprocessor. Closes https://github.com/gnolang/gno/issues/2973 --- gnovm/pkg/gnolang/nodes.go | 21 ++++---- gnovm/pkg/gnolang/op_exec.go | 16 ++---- gnovm/pkg/gnolang/preprocess.go | 90 ++++++++++++++++++++++----------- gnovm/tests/files/break0.gno | 2 +- gnovm/tests/files/cont3.gno | 2 +- gnovm/tests/files/for21.gno | 17 +++++++ gnovm/tests/files/for22.gno | 17 +++++++ gnovm/tests/files/for23.gno | 17 +++++++ gnovm/tests/files/for24.gno | 19 +++++++ gnovm/tests/files/switch8.gno | 2 +- gnovm/tests/files/switch8b.gno | 2 +- gnovm/tests/files/switch8c.gno | 2 +- gnovm/tests/files/switch9.gno | 2 +- 13 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 gnovm/tests/files/for21.gno create mode 100644 gnovm/tests/files/for22.gno create mode 100644 gnovm/tests/files/for23.gno create mode 100644 gnovm/tests/files/for24.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index c282b619fdc..45062f8e14c 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -149,16 +149,17 @@ func (loc Location) IsZero() bool { type GnoAttribute string const ( - ATTR_PREPROCESSED GnoAttribute = "ATTR_PREPROCESSED" - ATTR_PREDEFINED GnoAttribute = "ATTR_PREDEFINED" - ATTR_TYPE_VALUE GnoAttribute = "ATTR_TYPE_VALUE" - ATTR_TYPEOF_VALUE GnoAttribute = "ATTR_TYPEOF_VALUE" - ATTR_IOTA GnoAttribute = "ATTR_IOTA" - ATTR_LOCATIONED GnoAttribute = "ATTR_LOCATIONE" // XXX DELETE - ATTR_GOTOLOOP_STMT GnoAttribute = "ATTR_GOTOLOOP_STMT" // XXX delete? - ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops. - ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used. - ATTR_SHIFT_RHS GnoAttribute = "ATTR_SHIFT_RHS" + ATTR_PREPROCESSED GnoAttribute = "ATTR_PREPROCESSED" + ATTR_PREDEFINED GnoAttribute = "ATTR_PREDEFINED" + ATTR_TYPE_VALUE GnoAttribute = "ATTR_TYPE_VALUE" + ATTR_TYPEOF_VALUE GnoAttribute = "ATTR_TYPEOF_VALUE" + ATTR_IOTA GnoAttribute = "ATTR_IOTA" + ATTR_LOCATIONED GnoAttribute = "ATTR_LOCATIONE" // XXX DELETE + ATTR_GOTOLOOP_STMT GnoAttribute = "ATTR_GOTOLOOP_STMT" // XXX delete? + ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops. + ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used. + ATTR_SHIFT_RHS GnoAttribute = "ATTR_SHIFT_RHS" + ATTR_LAST_BLOCK_STMT GnoAttribute = "ATTR_LAST_BLOCK_STMT" ) type Attributes struct { diff --git a/gnovm/pkg/gnolang/op_exec.go b/gnovm/pkg/gnolang/op_exec.go index a61349b0806..900b5f8e9bb 100644 --- a/gnovm/pkg/gnolang/op_exec.go +++ b/gnovm/pkg/gnolang/op_exec.go @@ -676,25 +676,15 @@ EXEC_SWITCH: bs.Active = bs.Body[cs.BodyIndex] // prefill case FALLTHROUGH: ss, ok := m.LastFrame().Source.(*SwitchStmt) + // this is handled in the preprocessor + // should never happen if !ok { - // fallthrough is only allowed in a switch statement panic("fallthrough statement out of place") } - if ss.IsTypeSwitch { - // fallthrough is not allowed in type switches - panic("cannot fallthrough in type switch") - } + b := m.LastBlock() - if b.bodyStmt.NextBodyIndex != len(b.bodyStmt.Body) { - // fallthrough is not the final statement - panic("fallthrough statement out of place") - } // compute next switch clause from BodyIndex (assigned in preprocess) nextClause := cs.BodyIndex + 1 - if nextClause >= len(ss.Clauses) { - // no more clause after the one executed, this is not allowed - panic("cannot fallthrough final case in switch") - } // expand block size cl := ss.Clauses[nextClause] if nn := cl.GetNumNames(); int(nn) > len(b.Values) { diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 78e4488b2a0..1b85d83296d 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -298,6 +298,11 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { last.Predefine(false, n.VarName) } case *SwitchClauseStmt: + blen := len(n.Body) + if blen > 0 { + n.Body[blen-1].SetAttribute(ATTR_LAST_BLOCK_STMT, true) + } + // parent switch statement. ss := ns[len(ns)-1].(*SwitchStmt) // anything declared in ss are copied, @@ -2137,9 +2142,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { switch n.Op { case BREAK: if n.Label == "" { - if !findBreakableNode(ns) { - panic("cannot break with no parent loop or switch") - } + findBreakableNode(last, store) } else { // Make sure that the label exists, either for a switch or a // BranchStmt. @@ -2149,9 +2152,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } case CONTINUE: if n.Label == "" { - if !findContinuableNode(ns) { - panic("cannot continue with no parent loop") - } + findContinuableNode(last, store) } else { if isSwitchLabel(ns, n.Label) { panic(fmt.Sprintf("invalid continue label %q\n", n.Label)) @@ -2163,17 +2164,36 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { n.Depth = depth n.BodyIndex = index case FALLTHROUGH: - if swchC, ok := last.(*SwitchClauseStmt); ok { - // last is a switch clause, find its index in the switch and assign - // it to the fallthrough node BodyIndex. This will be used at - // runtime to determine the next switch clause to run. - swch := lastSwitch(ns) - for i := range swch.Clauses { - if &swch.Clauses[i] == swchC { - // switch clause found - n.BodyIndex = i - break - } + swchC, ok := last.(*SwitchClauseStmt) + if !ok { + // fallthrough is only allowed in a switch statement + panic("fallthrough statement out of place") + } + + if n.GetAttribute(ATTR_LAST_BLOCK_STMT) != true { + // no more clause after the one executed, this is not allowed + panic("fallthrough statement out of place") + } + + // last is a switch clause, find its index in the switch and assign + // it to the fallthrough node BodyIndex. This will be used at + // runtime to determine the next switch clause to run. + swch := lastSwitch(ns) + + if swch.IsTypeSwitch { + // fallthrough is not allowed in type switches + panic("cannot fallthrough in type switch") + } + + for i := range swch.Clauses { + if i == len(swch.Clauses)-1 { + panic("cannot fallthrough final case in switch") + } + + if &swch.Clauses[i] == swchC { + // switch clause found + n.BodyIndex = i + break } } default: @@ -3272,24 +3292,36 @@ func funcOf(last BlockNode) (BlockNode, *FuncTypeExpr) { } } -func findBreakableNode(ns []Node) bool { - for _, n := range ns { - switch n.(type) { - case *ForStmt, *RangeStmt, *SwitchClauseStmt: - return true +func findBreakableNode(last BlockNode, store Store) { + for last != nil { + switch last.(type) { + case *FuncLitExpr, *FuncDecl: + panic("break statement out of place") + case *ForStmt: + return + case *RangeStmt: + return + case *SwitchClauseStmt: + return } + + last = last.GetParentNode(store) } - return false } -func findContinuableNode(ns []Node) bool { - for _, n := range ns { - switch n.(type) { - case *ForStmt, *RangeStmt: - return true +func findContinuableNode(last BlockNode, store Store) { + for last != nil { + switch last.(type) { + case *FuncLitExpr, *FuncDecl: + panic("continue statement out of place") + case *ForStmt: + return + case *RangeStmt: + return } + + last = last.GetParentNode(store) } - return false } func findBranchLabel(last BlockNode, label Name) ( diff --git a/gnovm/tests/files/break0.gno b/gnovm/tests/files/break0.gno index 17d68dc1dbf..891084c56f9 100644 --- a/gnovm/tests/files/break0.gno +++ b/gnovm/tests/files/break0.gno @@ -5,4 +5,4 @@ func main() { } // Error: -// main/files/break0.gno:4:2: cannot break with no parent loop or switch +// main/files/break0.gno:4:2: break statement out of place diff --git a/gnovm/tests/files/cont3.gno b/gnovm/tests/files/cont3.gno index 8a305d4ceb2..39112697860 100644 --- a/gnovm/tests/files/cont3.gno +++ b/gnovm/tests/files/cont3.gno @@ -5,4 +5,4 @@ func main() { } // Error: -// main/files/cont3.gno:4:2: cannot continue with no parent loop +// main/files/cont3.gno:4:2: continue statement out of place diff --git a/gnovm/tests/files/for21.gno b/gnovm/tests/files/for21.gno new file mode 100644 index 00000000000..74b7d724121 --- /dev/null +++ b/gnovm/tests/files/for21.gno @@ -0,0 +1,17 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + continue + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for21.gno:7:17: continue statement out of place diff --git a/gnovm/tests/files/for22.gno b/gnovm/tests/files/for22.gno new file mode 100644 index 00000000000..dd86ce97cb7 --- /dev/null +++ b/gnovm/tests/files/for22.gno @@ -0,0 +1,17 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + fallthrough + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for22.gno:7:17: fallthrough statement out of place \ No newline at end of file diff --git a/gnovm/tests/files/for23.gno b/gnovm/tests/files/for23.gno new file mode 100644 index 00000000000..cb0f4c104fa --- /dev/null +++ b/gnovm/tests/files/for23.gno @@ -0,0 +1,17 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + break + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for23.gno:7:17: break statement out of place \ No newline at end of file diff --git a/gnovm/tests/files/for24.gno b/gnovm/tests/files/for24.gno new file mode 100644 index 00000000000..c3d49bb86a7 --- /dev/null +++ b/gnovm/tests/files/for24.gno @@ -0,0 +1,19 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + if true { + break + } + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for24.gno:8:21: break statement out of place \ No newline at end of file diff --git a/gnovm/tests/files/switch8.gno b/gnovm/tests/files/switch8.gno index c43c72582c0..f5952354270 100644 --- a/gnovm/tests/files/switch8.gno +++ b/gnovm/tests/files/switch8.gno @@ -7,4 +7,4 @@ func main() { } // Error: -// fallthrough statement out of place +// main/files/switch8.gno:5:2: fallthrough statement out of place diff --git a/gnovm/tests/files/switch8b.gno b/gnovm/tests/files/switch8b.gno index cdf35caf784..079b1b48efe 100644 --- a/gnovm/tests/files/switch8b.gno +++ b/gnovm/tests/files/switch8b.gno @@ -12,4 +12,4 @@ func main() { } // Error: -// cannot fallthrough final case in switch +// main/files/switch8b.gno:10:3: cannot fallthrough final case in switch diff --git a/gnovm/tests/files/switch8c.gno b/gnovm/tests/files/switch8c.gno index 6897b8a88fd..27c8e3ad9d5 100644 --- a/gnovm/tests/files/switch8c.gno +++ b/gnovm/tests/files/switch8c.gno @@ -12,4 +12,4 @@ func main() { } // Error: -// fallthrough statement out of place +// main/files/switch8c.gno:7:3: fallthrough statement out of place diff --git a/gnovm/tests/files/switch9.gno b/gnovm/tests/files/switch9.gno index 5f596de013a..5b05316b0b9 100644 --- a/gnovm/tests/files/switch9.gno +++ b/gnovm/tests/files/switch9.gno @@ -13,4 +13,4 @@ func main() { } // Error: -// cannot fallthrough in type switch +// main/files/switch9.gno:9:3: cannot fallthrough in type switch From 1e2929bb875286bf8dc79d95f69c2902b2c4aa68 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:09:42 +0900 Subject: [PATCH 235/345] feat: bump codecov to v5 (#3152) bump codecov to v5
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- .github/workflows/test_template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml index b032718ff62..ccbae792c78 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -57,11 +57,11 @@ jobs: go tool covdata textfmt -v 1 $filter -i=$GOCOVERDIR,$TXTARCOVERDIR -o gocoverage.out - name: Upload go coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: disable_search: true fail_ci_if_error: true - file: ${{ inputs.modulepath }}/gocoverage.out + files: ${{ inputs.modulepath }}/gocoverage.out flags: ${{ inputs.modulepath }} token: ${{ secrets.codecov-token }} verbose: true # keep this enable as it help debugging when coverage fail randomly on the CI From b3800b7dfb864396ac74dc20390e728bc0b2d88e Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Tue, 19 Nov 2024 04:07:16 -0600 Subject: [PATCH 236/345] feat(examples): mirror realm (#3156) ## Description Adds the mirror realm.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/r/demo/mirror/doc.gno | 3 ++ examples/gno.land/r/demo/mirror/gno.mod | 3 ++ examples/gno.land/r/demo/mirror/mirror.gno | 33 ++++++++++++++++++++++ examples/gno.land/r/leon/home/gno.mod | 1 + examples/gno.land/r/leon/home/home.gno | 2 ++ 5 files changed, 42 insertions(+) create mode 100644 examples/gno.land/r/demo/mirror/doc.gno create mode 100644 examples/gno.land/r/demo/mirror/gno.mod create mode 100644 examples/gno.land/r/demo/mirror/mirror.gno diff --git a/examples/gno.land/r/demo/mirror/doc.gno b/examples/gno.land/r/demo/mirror/doc.gno new file mode 100644 index 00000000000..40fdbd5bc26 --- /dev/null +++ b/examples/gno.land/r/demo/mirror/doc.gno @@ -0,0 +1,3 @@ +// Package mirror demonstrates that users can pass realm functions +// as arguments to other realms. +package mirror diff --git a/examples/gno.land/r/demo/mirror/gno.mod b/examples/gno.land/r/demo/mirror/gno.mod new file mode 100644 index 00000000000..2bf27fd6916 --- /dev/null +++ b/examples/gno.land/r/demo/mirror/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/mirror + +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/mirror/mirror.gno b/examples/gno.land/r/demo/mirror/mirror.gno new file mode 100644 index 00000000000..770fddc4fda --- /dev/null +++ b/examples/gno.land/r/demo/mirror/mirror.gno @@ -0,0 +1,33 @@ +package mirror + +import ( + "gno.land/p/demo/avl" +) + +var store avl.Tree + +func Register(pkgpath string, rndr func(string) string) { + if store.Has(pkgpath) { + return + } + + if rndr == nil { + return + } + + store.Set(pkgpath, rndr) +} + +func Render(path string) string { + if raw, ok := store.Get(path); ok { + return raw.(func(string) string)("") + } + + if store.Size() == 0 { + return "None are fair." + } + + return "Mirror, mirror on the wall, which realm's the fairest of them all?" +} + +// Credits to @jeronimoalbi diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index 4649cf4abe6..7288c176050 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -5,5 +5,6 @@ require ( gno.land/r/demo/art/gnoface v0.0.0-latest gno.land/r/demo/art/millipede v0.0.0-latest gno.land/r/demo/hof v0.0.0-latest + gno.land/r/demo/mirror v0.0.0-latest gno.land/r/leon/config v0.0.0-latest ) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index aea8b43e9cd..632b3f14a62 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -9,6 +9,7 @@ import ( "gno.land/r/demo/art/gnoface" "gno.land/r/demo/art/millipede" "gno.land/r/demo/hof" + "gno.land/r/demo/mirror" "gno.land/r/leon/config" ) @@ -34,6 +35,7 @@ TODO import r/gh } hof.Register() + mirror.Register(std.CurrentRealm().PkgPath(), Render) } func UpdatePFP(url, caption string) { From 7188b1cdb7f8991bf90d9316ff0be847760b7e78 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 20 Nov 2024 02:14:18 +0100 Subject: [PATCH 237/345] feat: add gnohealth cli tool (#3158) This PR adds a tool to the contribs dedicated to adding subcommands for health checks. The first available subcommand simply detects timestamp drift on a given chain. This test was useful in the context of this issue: https://github.com/gnolang/gno/issues/1950. However, it could later run on a dedicated server or on a GitHub Actions cron job to alert us in case significant drift occurs again. Results on test5 : ``` > gnohealth timestamp -ws -remote 'wss://rpc.test5.gno.land:443/websocket' -verbose block 411344 drifted of 3.094940942s (max 10s): OK block 411345 drifted of 2.368750176s (max 10s): OK block 411346 drifted of 2.310184977s (max 10s): OK block 411347 drifted of 2.158713327s (max 10s): OK block 411348 drifted of 2.203484957s (max 10s): OK block 411349 drifted of 2.156479203s (max 10s): OK block 411350 drifted of 2.155613458s (max 10s): OK block 411351 drifted of 2.296832155s (max 10s): OK block 411352 drifted of 2.132230389s (max 10s): OK block 411353 drifted of 2.181071735s (max 10s): OK block 411354 drifted of 2.575055701s (max 10s): OK block 411355 drifted of 2.034728695s (max 10s): OK block 411356 drifted of 2.285932658s (max 10s): OK block 411357 drifted of 2.330991247s (max 10s): OK block 411358 drifted of 2.365136593s (max 10s): OK block 411359 drifted of 2.035198868s (max 10s): OK block 411360 drifted of 2.128274141s (max 10s): OK block 411361 drifted of 2.48608003s (max 10s): OK block 411362 drifted of 2.072144703s (max 10s): OK block 411363 drifted of 2.297280076s (max 10s): OK block 411364 drifted of 2.224310386s (max 10s): OK no timestamp drifted beyond the maximum delta (average 2.280639734s) ``` Results on test4 : ``` > gnohealth timestamp -ws -remote 'wss://rpc.test4.gno.land:443/websocket' -verbose block 3618022 drifted of 3.765561468s (max 10s): OK block 3618023 drifted of 3.697091353s (max 10s): OK block 3618024 drifted of 3.576602477s (max 10s): OK block 3618025 drifted of 3.542585771s (max 10s): OK block 3618026 drifted of 3.72231133s (max 10s): OK block 3618027 drifted of 3.751154575s (max 10s): OK block 3618028 drifted of 8.312827308s (max 10s): OK block 3618029 drifted of 3.712806121s (max 10s): OK block 3618030 drifted of 3.65324572s (max 10s): OK no timestamp drifted beyond the maximum delta (average 4.192687347s) ```
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- contribs/gnohealth/Makefile | 17 ++ contribs/gnohealth/go.mod | 46 ++++ contribs/gnohealth/go.sum | 199 ++++++++++++++++++ contribs/gnohealth/health.go | 24 +++ .../gnohealth/internal/timestamp/timestamp.go | 166 +++++++++++++++ contribs/gnohealth/main.go | 14 ++ 6 files changed, 466 insertions(+) create mode 100644 contribs/gnohealth/Makefile create mode 100644 contribs/gnohealth/go.mod create mode 100644 contribs/gnohealth/go.sum create mode 100644 contribs/gnohealth/health.go create mode 100644 contribs/gnohealth/internal/timestamp/timestamp.go create mode 100644 contribs/gnohealth/main.go diff --git a/contribs/gnohealth/Makefile b/contribs/gnohealth/Makefile new file mode 100644 index 00000000000..61c6e8c79ea --- /dev/null +++ b/contribs/gnohealth/Makefile @@ -0,0 +1,17 @@ +rundep := go run -modfile ../../misc/devdeps/go.mod +golangci_lint := $(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint + + +.PHONY: install +install: + go install . + +.PHONY: build +build: + go build -o build/gnohealth . + +lint: + $(golangci_lint) --config ../../.github/golangci.yml run ./... + +test: + @echo "XXX: add tests" diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod new file mode 100644 index 00000000000..e6d9f119c7b --- /dev/null +++ b/contribs/gnohealth/go.mod @@ -0,0 +1,46 @@ +module github.com/gnolang/gno/contribs/gnohealth + +go 1.22.4 + +replace github.com/gnolang/gno => ../.. + +require github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum new file mode 100644 index 00000000000..116cfbff021 --- /dev/null +++ b/contribs/gnohealth/go.sum @@ -0,0 +1,199 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/gnohealth/health.go b/contribs/gnohealth/health.go new file mode 100644 index 00000000000..5118cac5fa5 --- /dev/null +++ b/contribs/gnohealth/health.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/gnolang/gno/contribs/gnohealth/internal/timestamp" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func newHealthCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + ShortHelp: "gno health check suite", + LongHelp: "Gno health check suite, to verify that different parts of Gno are working correctly", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + timestamp.NewTimestampCmd(io), + ) + + return cmd +} diff --git a/contribs/gnohealth/internal/timestamp/timestamp.go b/contribs/gnohealth/internal/timestamp/timestamp.go new file mode 100644 index 00000000000..50521b9130f --- /dev/null +++ b/contribs/gnohealth/internal/timestamp/timestamp.go @@ -0,0 +1,166 @@ +package timestamp + +import ( + "context" + "flag" + "fmt" + "time" + + rpcClient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +const ( + defaultRemoteAddress = "http://127.0.0.1:26657" + defaultWebSocket = true + defaultCheckDuration = 30 * time.Second + defaultCheckInterval = 50 * time.Millisecond + defaultMaxDelta = 10 * time.Second + defaultVerbose = false +) + +type timestampCfg struct { + remoteAddress string + webSocket bool + checkDuration time.Duration + checkInterval time.Duration + maxDelta time.Duration + verbose bool +} + +// NewTimestampCmd creates the gnohealth timestamp subcommand +func NewTimestampCmd(io commands.IO) *commands.Command { + cfg := ×tampCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "timestamp", + ShortUsage: "[flags]", + ShortHelp: "check if block timestamps are drifting", + LongHelp: "This command checks if block timestamps are drifting on a blockchain by connecting to a specified node via RPC.", + }, + cfg, + func(_ context.Context, _ []string) error { + return execTimestamp(cfg, io) + }, + ) +} + +// RegisterFlags registers command-line flags for the timestamp command +func (c *timestampCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remoteAddress, + "remote", + defaultRemoteAddress, + "the remote address of the node to connect to via RPC", + ) + + fs.BoolVar( + &c.webSocket, + "ws", + defaultWebSocket, + "flag indicating whether to use the WebSocket protocol for RPC", + ) + + fs.DurationVar( + &c.checkDuration, + "duration", + defaultCheckDuration, + "duration for which checks should be performed", + ) + + fs.DurationVar( + &c.checkInterval, + "interval", + defaultCheckInterval, + "interval between consecutive checks", + ) + + fs.DurationVar( + &c.maxDelta, + "max-delta", + defaultMaxDelta, + "maximum allowable time difference between the current time and the last block time", + ) + + fs.BoolVar( + &c.verbose, + "verbose", + defaultVerbose, + "flag indicating whether to enable verbose logging", + ) +} + +func execTimestamp(cfg *timestampCfg, io commands.IO) error { + var ( + client *rpcClient.RPCClient + err error + lastChecked int64 + count uint64 + totalDelta time.Duration + ) + + // Init RPC client + if cfg.webSocket { + if client, err = rpcClient.NewWSClient(cfg.remoteAddress); err != nil { + return fmt.Errorf("unable to create WS client: %w", err) + } + } else { + if client, err = rpcClient.NewHTTPClient(cfg.remoteAddress); err != nil { + return fmt.Errorf("unable to create HTTP client: %w", err) + } + } + + // Create a ticker for check interval + ticker := time.NewTicker(cfg.checkInterval) + defer ticker.Stop() + + // Create a context that will stop this check when specified duration is elapsed + ctx, cancel := context.WithTimeout(context.Background(), cfg.checkDuration) + defer cancel() + + for { + select { + case <-ctx.Done(): + average := totalDelta / time.Duration(count) + io.Printf("no timestamp drifted beyond the maximum delta (average %s)\n", average) + return nil + + case <-ticker.C: + // Fetch the latest block number from the chain + status, err := client.Status() + if err != nil { + return fmt.Errorf("unable to fetch latest block number: %w", err) + } + + latest := status.SyncInfo.LatestBlockHeight + + // Check if there have been blocks since the last check + if lastChecked == latest { + continue + } + + // Fetch the latest block from the chain + lastBlock, err := client.Block(&latest) + if err != nil { + return fmt.Errorf("unable to fetch latest block content: %w", err) + } + + // Check if the last block timestamp is not drifting + delta := time.Until(lastBlock.Block.Time).Abs() + if delta > cfg.maxDelta { + return fmt.Errorf("block %d drifted of %s (max %s): KO", latest, delta, cfg.maxDelta) + } + + // Increment counters to calculate average on exit + count += 1 + totalDelta += delta + + // Update the last checked block number + lastChecked = latest + if cfg.verbose { + io.Printf("block %d drifted of %s (max %s): OK\n", latest, delta, cfg.maxDelta) + } + } + } +} diff --git a/contribs/gnohealth/main.go b/contribs/gnohealth/main.go new file mode 100644 index 00000000000..4325c657976 --- /dev/null +++ b/contribs/gnohealth/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func main() { + cmd := newHealthCmd(commands.NewDefaultIO()) + + cmd.Execute(context.Background(), os.Args[1:]) +} From 732bb0bc7a5dd2fcb8f81376ee269940d05dee38 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 20 Nov 2024 03:36:43 +0100 Subject: [PATCH 238/345] refactor: remove useless code and fix a test (#3159) - Commit https://github.com/gnolang/gno/pull/3159/commits/4b6219c5b89aa21a9ae46ee7fede63779194f9f1 move `ValidateBlock` method from `blockExec` to `state` since `blockExec.db` was an unused parameter. (simplify + remove misleading comment) - Commit https://github.com/gnolang/gno/pull/3159/commits/4f02bcd90f4ba9625818c43d7d806a43f9dffd62 just removes useless `Sprintf` found in this package - Commit https://github.com/gnolang/gno/pull/3159/commits/c34bfd57466d0bf992ac3402865387c89b59393e improves `ValidateBasic` method (adding one more validation test that was marked with a `TODO` comment) - Commit https://github.com/gnolang/gno/pull/3159/commits/df75b32f06d922e85ef493284aa24be0ac4d3524 removes useless case testing the size of a fixed-sized array, see: https://github.com/gnolang/gno/blob/b3800b7dfb864396ac74dc20390e728bc0b2d88e/tm2/pkg/crypto/crypto.go#L24-L30
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- tm2/pkg/bft/consensus/replay.go | 2 +- tm2/pkg/bft/consensus/state.go | 14 +++++++------- tm2/pkg/bft/node/node_test.go | 2 +- tm2/pkg/bft/state/execution.go | 9 +-------- tm2/pkg/bft/state/helpers_test.go | 2 +- tm2/pkg/bft/state/validation.go | 9 +++------ tm2/pkg/bft/state/validation_test.go | 6 +++--- tm2/pkg/bft/store/store.go | 2 +- tm2/pkg/bft/types/block.go | 16 +++++----------- tm2/pkg/bft/types/validator_set_test.go | 4 ++-- tm2/pkg/bft/types/vote.go | 6 ------ 11 files changed, 25 insertions(+), 47 deletions(-) diff --git a/tm2/pkg/bft/consensus/replay.go b/tm2/pkg/bft/consensus/replay.go index 02e6dade72c..2ba297a3d1e 100644 --- a/tm2/pkg/bft/consensus/replay.go +++ b/tm2/pkg/bft/consensus/replay.go @@ -237,7 +237,7 @@ func (h *Handshaker) Handshake(proxyApp appconn.AppConns) error { // Handshake is done via ABCI Info on the query conn. res, err := proxyApp.Query().InfoSync(abci.RequestInfo{}) if err != nil { - return fmt.Errorf("Error calling Info: %w", err) + return fmt.Errorf("error calling Info: %w", err) } blockHeight := res.LastBlockHeight diff --git a/tm2/pkg/bft/consensus/state.go b/tm2/pkg/bft/consensus/state.go index 6faa40be20b..8b2653813e3 100644 --- a/tm2/pkg/bft/consensus/state.go +++ b/tm2/pkg/bft/consensus/state.go @@ -203,7 +203,7 @@ func (cs *ConsensusState) SetEventSwitch(evsw events.EventSwitch) { // String returns a string. func (cs *ConsensusState) String() string { // better not to access shared variables - return fmt.Sprintf("ConsensusState") // (H:%v R:%v S:%v", cs.Height, cs.Round, cs.Step) + return "ConsensusState" // (H:%v R:%v S:%v", cs.Height, cs.Round, cs.Step) } // GetConfig returns a copy of the chain state. @@ -1051,7 +1051,7 @@ func (cs *ConsensusState) defaultDoPrevote(height int64, round int) { } // Validate proposal block - err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock) + err := cs.state.ValidateBlock(cs.ProposalBlock) if err != nil { // ProposalBlock is invalid, prevote nil. logger.Error("enterPrevote: ProposalBlock is invalid", "err", err) @@ -1164,7 +1164,7 @@ func (cs *ConsensusState) enterPrecommit(height int64, round int) { if cs.ProposalBlock.HashesTo(blockID.Hash) { logger.Info("enterPrecommit: +2/3 prevoted proposal block. Locking", "hash", blockID.Hash) // Validate the block. - if err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock); err != nil { + if err := cs.state.ValidateBlock(cs.ProposalBlock); err != nil { panic(fmt.Sprintf("enterPrecommit: +2/3 prevoted for an invalid block: %v", err)) } cs.LockedRound = round @@ -1305,15 +1305,15 @@ func (cs *ConsensusState) finalizeCommit(height int64) { block, blockParts := cs.ProposalBlock, cs.ProposalBlockParts if !ok { - panic(fmt.Sprintf("Cannot finalizeCommit, commit does not have two thirds majority")) + panic("Cannot finalizeCommit, commit does not have two thirds majority") } if !blockParts.HasHeader(blockID.PartsHeader) { - panic(fmt.Sprintf("Expected ProposalBlockParts header to be commit header")) + panic("Expected ProposalBlockParts header to be commit header") } if !block.HashesTo(blockID.Hash) { - panic(fmt.Sprintf("Cannot finalizeCommit, ProposalBlock does not hash to commit hash")) + panic("Cannot finalizeCommit, ProposalBlock does not hash to commit hash") } - if err := cs.blockExec.ValidateBlock(cs.state, block); err != nil { + if err := cs.state.ValidateBlock(block); err != nil { panic(fmt.Sprintf("+2/3 committed an invalid block: %v", err)) } diff --git a/tm2/pkg/bft/node/node_test.go b/tm2/pkg/bft/node/node_test.go index e28464ff711..6e86a0bcc6f 100644 --- a/tm2/pkg/bft/node/node_test.go +++ b/tm2/pkg/bft/node/node_test.go @@ -304,7 +304,7 @@ func TestCreateProposalBlock(t *testing.T) { proposerAddr, ) - err = blockExec.ValidateBlock(state, block) + err = state.ValidateBlock(block) assert.NoError(t, err) } diff --git a/tm2/pkg/bft/state/execution.go b/tm2/pkg/bft/state/execution.go index 15a0f466341..a58a50c1877 100644 --- a/tm2/pkg/bft/state/execution.go +++ b/tm2/pkg/bft/state/execution.go @@ -82,20 +82,13 @@ func (blockExec *BlockExecutor) CreateProposalBlock( return state.MakeBlock(height, txs, commit, proposerAddr) } -// ValidateBlock validates the given block against the given state. -// If the block is invalid, it returns an error. -// Validation does not mutate state, but does require historical information from the stateDB -func (blockExec *BlockExecutor) ValidateBlock(state State, block *types.Block) error { - return validateBlock(blockExec.db, state, block) -} - // ApplyBlock validates the block against the state, executes it against the app, // fires the relevant events, commits the app, and saves the new state and responses. // It's the only function that needs to be called // from outside this package to process and commit an entire block. // It takes a blockID to avoid recomputing the parts hash. func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, block *types.Block) (State, error) { - if err := blockExec.ValidateBlock(state, block); err != nil { + if err := state.ValidateBlock(block); err != nil { return state, InvalidBlockError(err) } diff --git a/tm2/pkg/bft/state/helpers_test.go b/tm2/pkg/bft/state/helpers_test.go index 0b8dba98221..948c1debe6d 100644 --- a/tm2/pkg/bft/state/helpers_test.go +++ b/tm2/pkg/bft/state/helpers_test.go @@ -52,7 +52,7 @@ func makeAndApplyGoodBlock(state sm.State, height int64, lastCommit *types.Commi blockExec *sm.BlockExecutor, ) (sm.State, types.BlockID, error) { block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, proposerAddr) - if err := blockExec.ValidateBlock(state, block); err != nil { + if err := state.ValidateBlock(block); err != nil { return state, types.BlockID{}, err } blockID := types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}} diff --git a/tm2/pkg/bft/state/validation.go b/tm2/pkg/bft/state/validation.go index 13274b6a38c..14191bea0d9 100644 --- a/tm2/pkg/bft/state/validation.go +++ b/tm2/pkg/bft/state/validation.go @@ -6,14 +6,12 @@ import ( "fmt" "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/crypto" - dbm "github.com/gnolang/gno/tm2/pkg/db" ) // ----------------------------------------------------- // Validate block -func validateBlock(stateDB dbm.DB, state State, block *types.Block) error { +func (state State) ValidateBlock(block *types.Block) error { // Validate internal consistency. if err := block.ValidateBasic(); err != nil { return err @@ -137,9 +135,8 @@ func validateBlock(stateDB dbm.DB, state State, block *types.Block) error { // NOTE: We can't actually verify it's the right proposer because we dont // know what round the block was first proposed. So just check that it's - // a legit address and a known validator. - if len(block.ProposerAddress) != crypto.AddressSize || - !state.Validators.HasAddress(block.ProposerAddress) { + // a legit address from a known validator. + if !state.Validators.HasAddress(block.ProposerAddress) { return fmt.Errorf("Block.Header.ProposerAddress, %X, is not a validator", block.ProposerAddress, ) diff --git a/tm2/pkg/bft/state/validation_test.go b/tm2/pkg/bft/state/validation_test.go index 0eadd076be9..1830bb3f6da 100644 --- a/tm2/pkg/bft/state/validation_test.go +++ b/tm2/pkg/bft/state/validation_test.go @@ -142,7 +142,7 @@ func TestValidateBlockHeader(t *testing.T) { for _, tc := range testCases { block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, proposerAddr) tc.malleateBlock(block) - err := blockExec.ValidateBlock(state, block) + err := state.ValidateBlock(block) assert.ErrorContains(t, err, tc.expectedError, tc.name) } @@ -179,7 +179,7 @@ func TestValidateBlockCommit(t *testing.T) { require.NoError(t, err, "height %d", height) wrongHeightCommit := types.NewCommit(state.LastBlockID, []*types.CommitSig{wrongHeightVote.CommitSig()}) block, _ := state.MakeBlock(height, makeTxs(height), wrongHeightCommit, proposerAddr) - err = blockExec.ValidateBlock(state, block) + err = state.ValidateBlock(block) _, isErrInvalidCommitHeight := err.(types.InvalidCommitHeightError) require.True(t, isErrInvalidCommitHeight, "expected InvalidCommitHeightError at height %d but got: %v", height, err) @@ -187,7 +187,7 @@ func TestValidateBlockCommit(t *testing.T) { #2589: test len(block.LastCommit.Precommits) == state.LastValidators.Size() */ block, _ = state.MakeBlock(height, makeTxs(height), wrongPrecommitsCommit, proposerAddr) - err = blockExec.ValidateBlock(state, block) + err = state.ValidateBlock(block) _, isErrInvalidCommitPrecommits := err.(types.InvalidCommitPrecommitsError) require.True(t, isErrInvalidCommitPrecommits, "expected InvalidCommitPrecommitsError at height %d but got: %v", height, err) } diff --git a/tm2/pkg/bft/store/store.go b/tm2/pkg/bft/store/store.go index e7671d8033e..e5b9f57a1af 100644 --- a/tm2/pkg/bft/store/store.go +++ b/tm2/pkg/bft/store/store.go @@ -152,7 +152,7 @@ func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, s panic(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", w, g)) } if !blockParts.IsComplete() { - panic(fmt.Sprintf("BlockStore can only save complete block part sets")) + panic("BlockStore can only save complete block part sets") } // Save block meta diff --git a/tm2/pkg/bft/types/block.go b/tm2/pkg/bft/types/block.go index a8501775c4b..445f169f1d1 100644 --- a/tm2/pkg/bft/types/block.go +++ b/tm2/pkg/bft/types/block.go @@ -10,7 +10,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" typesver "github.com/gnolang/gno/tm2/pkg/bft/types/version" "github.com/gnolang/gno/tm2/pkg/bitarray" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/merkle" "github.com/gnolang/gno/tm2/pkg/crypto/tmhash" "github.com/gnolang/gno/tm2/pkg/errors" @@ -54,10 +53,9 @@ func (b *Block) ValidateBasic() error { ) } - // TODO: fix tests so we can do this - /*if b.TotalTxs < b.NumTxs { + if b.TotalTxs < b.NumTxs { return fmt.Errorf("Header.TotalTxs (%d) is less than Header.NumTxs (%d)", b.TotalTxs, b.NumTxs) - }*/ + } if b.TotalTxs < 0 { return errors.New("Negative Header.TotalTxs") } @@ -115,11 +113,6 @@ func (b *Block) ValidateBasic() error { return fmt.Errorf("wrong Header.LastResultsHash: %w", err) } - if len(b.ProposerAddress) != crypto.AddressSize { - return fmt.Errorf("expected len(Header.ProposerAddress) to be %d, got %d", - crypto.AddressSize, len(b.ProposerAddress)) - } - return nil } @@ -265,8 +258,9 @@ func (h *Header) GetTime() time.Time { return h.Time } func MakeBlock(height int64, txs []Tx, lastCommit *Commit) *Block { block := &Block{ Header: Header{ - Height: height, - NumTxs: int64(len(txs)), + Height: height, + NumTxs: int64(len(txs)), + TotalTxs: int64(len(txs)), }, Data: Data{ Txs: txs, diff --git a/tm2/pkg/bft/types/validator_set_test.go b/tm2/pkg/bft/types/validator_set_test.go index e543104b15d..bc02ae754c5 100644 --- a/tm2/pkg/bft/types/validator_set_test.go +++ b/tm2/pkg/bft/types/validator_set_test.go @@ -249,7 +249,7 @@ func TestProposerSelection3(t *testing.T) { got := vset.GetProposer().Address expected := proposerOrder[j%4].Address if got != expected { - t.Fatalf(fmt.Sprintf("vset.Proposer (%X) does not match expected proposer (%X) for (%d, %d)", got, expected, i, j)) + t.Fatalf("vset.Proposer (%X) does not match expected proposer (%X) for (%d, %d)", got, expected, i, j) } // serialize, deserialize, check proposer @@ -263,7 +263,7 @@ func TestProposerSelection3(t *testing.T) { computed := vset.GetProposer() // findGetProposer() if i != 0 { if got != computed.Address { - t.Fatalf(fmt.Sprintf("vset.Proposer (%X) does not match computed proposer (%X) for (%d, %d)", got, computed.Address, i, j)) + t.Fatalf("vset.Proposer (%X) does not match computed proposer (%X) for (%d, %d)", got, computed.Address, i, j) } } diff --git a/tm2/pkg/bft/types/vote.go b/tm2/pkg/bft/types/vote.go index caaaa3f8c34..66fc40919f1 100644 --- a/tm2/pkg/bft/types/vote.go +++ b/tm2/pkg/bft/types/vote.go @@ -141,12 +141,6 @@ func (vote *Vote) ValidateBasic() error { if !vote.BlockID.IsZero() && !vote.BlockID.IsComplete() { return fmt.Errorf("BlockID must be either empty or complete, got: %v", vote.BlockID) } - if len(vote.ValidatorAddress) != crypto.AddressSize { - return fmt.Errorf("expected ValidatorAddress size to be %d bytes, got %d bytes", - crypto.AddressSize, - len(vote.ValidatorAddress), - ) - } if vote.ValidatorAddress.IsZero() { return fmt.Errorf("expected ValidatorAddress to be non-zero") } From 4646ae677578ae2848f9df84a4ec3d2dfba0bb29 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:31:02 +0100 Subject: [PATCH 239/345] feat: add r/docs/home (#3160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introducing the `r/docs` namespace, where the homepage currently lists subrealms manually. In the future, we may implement a registry, but for now, we’re keeping the source code as lean as possible. The namespace includes several interactive examples to guide users through key concepts. The `r/docs/hello` example provides a simple Render function and invites users to click on "view source" to understand the basics of customization. The `r/docs/avl_pager` example demonstrates path-based interactions, allowing users to explore an avl tree structure with pagination links to navigate between items. Users are encouraged to click on these links for inspiration before manually adjusting parameters in the URL. The added `r/docs/add` example introduces interactivity through transactions, allowing users to adjust a number by submitting transactions, and see the updated result with each interaction. These examples are designed to engage users with Render-based UI interactions, path handling, and transaction-based updates. Once we have more content in r/docs, this section could serve as the main documentation link in the navbar, providing a comprehensive, hands-on introduction to Gno. Addresses #3084 Addresses https://github.com/gnolang/docs-v2/pull/27#discussion_r1848481556 Addresses #2953 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/demo/hello_world/gno.mod | 1 - .../gno.land/r/demo/hello_world/hello.gno | 17 ------ examples/gno.land/r/docs/add/add.gno | 42 ++++++++++++++ examples/gno.land/r/docs/add/add_test.gno | 44 +++++++++++++++ examples/gno.land/r/docs/add/gno.mod | 3 + .../gno.land/r/docs/avl_pager/avl_pager.gno | 40 ++++++++++++++ .../r/docs/avl_pager/avl_pager_test.gno | 55 +++++++++++++++++++ examples/gno.land/r/docs/avl_pager/gno.mod | 6 ++ examples/gno.land/r/docs/hello/gno.mod | 1 + examples/gno.land/r/docs/hello/hello.gno | 11 ++++ .../hello_world => docs/hello}/hello_test.gno | 2 +- examples/gno.land/r/docs/home/gno.mod | 1 + examples/gno.land/r/docs/home/home.gno | 20 +++++++ examples/gno.land/r/docs/home/home_test.gno | 22 ++++++++ 14 files changed, 246 insertions(+), 19 deletions(-) delete mode 100644 examples/gno.land/r/demo/hello_world/gno.mod delete mode 100644 examples/gno.land/r/demo/hello_world/hello.gno create mode 100644 examples/gno.land/r/docs/add/add.gno create mode 100644 examples/gno.land/r/docs/add/add_test.gno create mode 100644 examples/gno.land/r/docs/add/gno.mod create mode 100644 examples/gno.land/r/docs/avl_pager/avl_pager.gno create mode 100644 examples/gno.land/r/docs/avl_pager/avl_pager_test.gno create mode 100644 examples/gno.land/r/docs/avl_pager/gno.mod create mode 100644 examples/gno.land/r/docs/hello/gno.mod create mode 100644 examples/gno.land/r/docs/hello/hello.gno rename examples/gno.land/r/{demo/hello_world => docs/hello}/hello_test.gno (93%) create mode 100644 examples/gno.land/r/docs/home/gno.mod create mode 100644 examples/gno.land/r/docs/home/home.gno create mode 100644 examples/gno.land/r/docs/home/home_test.gno diff --git a/examples/gno.land/r/demo/hello_world/gno.mod b/examples/gno.land/r/demo/hello_world/gno.mod deleted file mode 100644 index 9561cd4f077..00000000000 --- a/examples/gno.land/r/demo/hello_world/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/hello_world diff --git a/examples/gno.land/r/demo/hello_world/hello.gno b/examples/gno.land/r/demo/hello_world/hello.gno deleted file mode 100644 index 312520de44d..00000000000 --- a/examples/gno.land/r/demo/hello_world/hello.gno +++ /dev/null @@ -1,17 +0,0 @@ -// Package hello_world demonstrates the usage of the Render() function. -// Render() can be called via the vm/qrender ABCI query off-chain to -// retrieve realm state or any other custom data defined by the realm -// developer. The vm/qrender query allows for additional data to be -// passed in with the call, which can be utilized as the path argument -// to the Render() function. This allows developers to create different -// "renders" of their realms depending on the data which is passed in, -// such as pagination, admin dashboards, and more. -package hello_world - -func Render(path string) string { - if path == "" { - return "# Hello, 世界!" - } - - return "# Hello, " + path + "!" -} diff --git a/examples/gno.land/r/docs/add/add.gno b/examples/gno.land/r/docs/add/add.gno new file mode 100644 index 00000000000..ffc8f9c6877 --- /dev/null +++ b/examples/gno.land/r/docs/add/add.gno @@ -0,0 +1,42 @@ +package add + +import ( + "strconv" + "time" + + "gno.land/p/moul/txlink" +) + +// Global variables to store the current number and last update timestamp +var ( + number int + lastUpdate time.Time +) + +// Add function to update the number and timestamp +func Add(n int) { + number += n + lastUpdate = time.Now() +} + +// Render displays the current number value, last update timestamp, and a link to call Add with 42 +func Render(path string) string { + // Display the current number and formatted last update time + result := "# Add Example\n\n" + result += "Current Number: " + strconv.Itoa(number) + "\n\n" + result += "Last Updated: " + formatTimestamp(lastUpdate) + "\n\n" + + // Generate a transaction link to call Add with 42 as the default parameter + txLink := txlink.URL("Add", "n", "42") + result += "[Increase Number](" + txLink + ")\n" + + return result +} + +// Helper function to format the timestamp for readability +func formatTimestamp(timestamp time.Time) string { + if timestamp.IsZero() { + return "Never" + } + return timestamp.Format("2006-01-02 15:04:05") +} diff --git a/examples/gno.land/r/docs/add/add_test.gno b/examples/gno.land/r/docs/add/add_test.gno new file mode 100644 index 00000000000..8994b895f7e --- /dev/null +++ b/examples/gno.land/r/docs/add/add_test.gno @@ -0,0 +1,44 @@ +package add + +import ( + "testing" +) + +func TestRenderAndAdd(t *testing.T) { + // Initial Render output + output := Render("") + expected := `# Add Example + +Current Number: 0 + +Last Updated: Never + +[Increase Number](/r/docs/add$help&func=Add&n=42) +` + if output != expected { + t.Errorf("Initial Render failed, got:\n%s", output) + } + + // Call Add with a value of 10 + Add(10) + + // Call Add again with a value of -5 + Add(-5) + + // Render after two Add calls + finalOutput := Render("") + + // Initial Render output + output = Render("") + expected = `# Add Example + +Current Number: 5 + +Last Updated: 2009-02-13 23:31:30 + +[Increase Number](/r/docs/add$help&func=Add&n=42) +` + if output != expected { + t.Errorf("Final Render failed, got:\n%s", output) + } +} diff --git a/examples/gno.land/r/docs/add/gno.mod b/examples/gno.land/r/docs/add/gno.mod new file mode 100644 index 00000000000..a66c63e0910 --- /dev/null +++ b/examples/gno.land/r/docs/add/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/docs/add + +require gno.land/p/moul/txlink v0.0.0-latest diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager.gno b/examples/gno.land/r/docs/avl_pager/avl_pager.gno new file mode 100644 index 00000000000..75807b71981 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager/avl_pager.gno @@ -0,0 +1,40 @@ +package avl_pager + +import ( + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" +) + +// Tree instance for 100 items +var tree *avl.Tree + +// Initialize a tree with 100 items. +func init() { + tree = avl.NewTree() + for i := 1; i <= 100; i++ { + key := "Item" + strconv.Itoa(i) + tree.Set(key, "Value of "+key) + } +} + +// Render paginated content based on the given URL path. +// URL format: `...?page=&size=` (default is page 1 and size 10). +func Render(path string) string { + p := pager.NewPager(tree, 10) // Default page size is 10 + page := p.MustGetPageByPath(path) + + // Header and pagination info + result := "# Paginated Items\n" + result += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + result += page.Selector() + "\n\n" + + // Display items on the current page + for _, item := range page.Items { + result += "- " + item.Key + ": " + item.Value.(string) + "\n" + } + + result += "\n" + page.Selector() // Repeat selector for ease of navigation + return result +} diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager_test.gno b/examples/gno.land/r/docs/avl_pager/avl_pager_test.gno new file mode 100644 index 00000000000..1ffc9a0c3ba --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager/avl_pager_test.gno @@ -0,0 +1,55 @@ +package avl_pager + +import ( + "testing" +) + +func TestRender(t *testing.T) { + // Test default Render output (first page) + output := Render("") + expected := `# Paginated Items +Page 1 of 10 + +**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10) + +- Item1: Value of Item1 +- Item10: Value of Item10 +- Item100: Value of Item100 +- Item11: Value of Item11 +- Item12: Value of Item12 +- Item13: Value of Item13 +- Item14: Value of Item14 +- Item15: Value of Item15 +- Item16: Value of Item16 +- Item17: Value of Item17 + +**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)` + if output != expected { + t.Errorf("Render(\"\") failed, got:\n%s", output) + } +} + +func TestRender_page2(t *testing.T) { + // Test Render output for a custom page (page 2) + output := Render("?page=2&size=10") + expected := `# Paginated Items +Page 2 of 10 + +[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10) + +- Item18: Value of Item18 +- Item19: Value of Item19 +- Item2: Value of Item2 +- Item20: Value of Item20 +- Item21: Value of Item21 +- Item22: Value of Item22 +- Item23: Value of Item23 +- Item24: Value of Item24 +- Item25: Value of Item25 +- Item26: Value of Item26 + +[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)` + if output != expected { + t.Errorf("Render(\"\") failed, got:\n%s", output) + } +} diff --git a/examples/gno.land/r/docs/avl_pager/gno.mod b/examples/gno.land/r/docs/avl_pager/gno.mod new file mode 100644 index 00000000000..0d05b24bcd0 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/docs/avl_pager + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/avl/pager v0.0.0-latest +) diff --git a/examples/gno.land/r/docs/hello/gno.mod b/examples/gno.land/r/docs/hello/gno.mod new file mode 100644 index 00000000000..25ddf30051f --- /dev/null +++ b/examples/gno.land/r/docs/hello/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/hello diff --git a/examples/gno.land/r/docs/hello/hello.gno b/examples/gno.land/r/docs/hello/hello.gno new file mode 100644 index 00000000000..e881c155cdd --- /dev/null +++ b/examples/gno.land/r/docs/hello/hello.gno @@ -0,0 +1,11 @@ +// Package hello_world demonstrates basic usage of Render(). +// Try adding `:World` at the end of the URL, like `.../hello:World`. +package hello + +// Render outputs a greeting. It customizes the message based on the provided path. +func Render(path string) string { + if path == "" { + return "# Hello, 世界!" + } + return "# Hello, " + path + "!" +} diff --git a/examples/gno.land/r/demo/hello_world/hello_test.gno b/examples/gno.land/r/docs/hello/hello_test.gno similarity index 93% rename from examples/gno.land/r/demo/hello_world/hello_test.gno rename to examples/gno.land/r/docs/hello/hello_test.gno index 4c3d86c556a..8159fb1341c 100644 --- a/examples/gno.land/r/demo/hello_world/hello_test.gno +++ b/examples/gno.land/r/docs/hello/hello_test.gno @@ -1,4 +1,4 @@ -package hello_world +package hello import ( "testing" diff --git a/examples/gno.land/r/docs/home/gno.mod b/examples/gno.land/r/docs/home/gno.mod new file mode 100644 index 00000000000..b9f8d060f75 --- /dev/null +++ b/examples/gno.land/r/docs/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/home diff --git a/examples/gno.land/r/docs/home/home.gno b/examples/gno.land/r/docs/home/home.gno new file mode 100644 index 00000000000..2c581019380 --- /dev/null +++ b/examples/gno.land/r/docs/home/home.gno @@ -0,0 +1,20 @@ +package home + +func Render(_ string) string { + return `# Gno Examples Documentation + +Welcome to the Gno examples documentation index. +Explore various examples to learn more about Gno functionality and usage. + +## Examples + +- [Hello World](/r/docs/hello) - A simple introductory example. +- [Add](/r/docs/add) - An interactive example to update a number with transactions. +- [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. +- ... + +## Other resources + +- [Official documentation](https://github.com/gnolang/gno/tree/master/docs) +` +} diff --git a/examples/gno.land/r/docs/home/home_test.gno b/examples/gno.land/r/docs/home/home_test.gno new file mode 100644 index 00000000000..98dc999e005 --- /dev/null +++ b/examples/gno.land/r/docs/home/home_test.gno @@ -0,0 +1,22 @@ +package home + +import ( + "strings" + "testing" +) + +func TestRenderHome(t *testing.T) { + output := Render("") + + // Check for the presence of key sections + if !contains(output, "# Gno Examples Documentation") { + t.Errorf("Render output is missing the title.") + } + if !contains(output, "Official documentation") { + t.Errorf("Render output is missing the official documentation link.") + } +} + +func contains(s, substr string) bool { + return strings.Index(s, substr) >= 0 +} From 2c323f48a62bc49f59c2845908b2d7da894cb17b Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 20 Nov 2024 11:53:09 +0100 Subject: [PATCH 240/345] ci: only run fossa action on workflow_dispatch (#3125) This is only run once every never (ie. when legal requests it) --- .github/workflows/fossa.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index f9d3110ba82..c536b428a5c 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -2,12 +2,6 @@ name: Dependency License Scanning on: workflow_dispatch: - pull_request: - paths: - - ".github/.fossa.yml" - - ".github/workflows/fossa.yml" - schedule: - - cron: '0 0 * * 6' # At 00:00 on saturdays permissions: contents: read @@ -47,4 +41,3 @@ jobs: run: fossa test env: FOSSA_API_KEY: "${{secrets.FOSSA_API_KEY}}" - From 7e5de12cb678c66e1d8e02247b1a316ff0bc5307 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:57:22 +0900 Subject: [PATCH 241/345] chore: move `hof` under `r/leon` (#3167) ## Description Moves `r/demo/hof` under `r/leon`. After discussing internally, we should start utilizing personal namespaces as much as possible. cc @moul
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/r/gnoland/home/gno.mod | 2 +- examples/gno.land/r/gnoland/home/home.gno | 2 +- examples/gno.land/r/{demo => leon}/hof/administration.gno | 0 examples/gno.land/r/{demo => leon}/hof/errors.gno | 0 examples/gno.land/r/{demo => leon}/hof/gno.mod | 2 +- examples/gno.land/r/{demo => leon}/hof/hof.gno | 0 examples/gno.land/r/{demo => leon}/hof/hof_test.gno | 0 examples/gno.land/r/{demo => leon}/hof/render.gno | 0 examples/gno.land/r/leon/home/gno.mod | 2 +- examples/gno.land/r/leon/home/home.gno | 2 +- examples/gno.land/r/manfred/home/gno.mod | 2 +- examples/gno.land/r/manfred/home/home.gno | 2 +- examples/gno.land/r/morgan/home/gno.mod | 2 +- examples/gno.land/r/morgan/home/home.gno | 2 +- 14 files changed, 9 insertions(+), 9 deletions(-) rename examples/gno.land/r/{demo => leon}/hof/administration.gno (100%) rename examples/gno.land/r/{demo => leon}/hof/errors.gno (100%) rename examples/gno.land/r/{demo => leon}/hof/gno.mod (94%) rename examples/gno.land/r/{demo => leon}/hof/hof.gno (100%) rename examples/gno.land/r/{demo => leon}/hof/hof_test.gno (100%) rename examples/gno.land/r/{demo => leon}/hof/render.gno (100%) diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index ff52ef4c8b1..52d01c6d38c 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -4,7 +4,7 @@ require ( gno.land/p/demo/ownable v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/ui v0.0.0-latest - gno.land/r/demo/hof v0.0.0-latest gno.land/r/gnoland/blog v0.0.0-latest gno.land/r/gnoland/events v0.0.0-latest + gno.land/r/leon/hof v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index ce976923ef5..04c549a0d27 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -6,9 +6,9 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" "gno.land/p/demo/ui" - "gno.land/r/demo/hof" blog "gno.land/r/gnoland/blog" events "gno.land/r/gnoland/events" + "gno.land/r/leon/hof" ) // XXX: p/demo/ui API is crappy, we need to make it more idiomatic diff --git a/examples/gno.land/r/demo/hof/administration.gno b/examples/gno.land/r/leon/hof/administration.gno similarity index 100% rename from examples/gno.land/r/demo/hof/administration.gno rename to examples/gno.land/r/leon/hof/administration.gno diff --git a/examples/gno.land/r/demo/hof/errors.gno b/examples/gno.land/r/leon/hof/errors.gno similarity index 100% rename from examples/gno.land/r/demo/hof/errors.gno rename to examples/gno.land/r/leon/hof/errors.gno diff --git a/examples/gno.land/r/demo/hof/gno.mod b/examples/gno.land/r/leon/hof/gno.mod similarity index 94% rename from examples/gno.land/r/demo/hof/gno.mod rename to examples/gno.land/r/leon/hof/gno.mod index ac5c91295a6..feb31992513 100644 --- a/examples/gno.land/r/demo/hof/gno.mod +++ b/examples/gno.land/r/leon/hof/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/demo/hof +module gno.land/r/leon/hof require ( gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno similarity index 100% rename from examples/gno.land/r/demo/hof/hof.gno rename to examples/gno.land/r/leon/hof/hof.gno diff --git a/examples/gno.land/r/demo/hof/hof_test.gno b/examples/gno.land/r/leon/hof/hof_test.gno similarity index 100% rename from examples/gno.land/r/demo/hof/hof_test.gno rename to examples/gno.land/r/leon/hof/hof_test.gno diff --git a/examples/gno.land/r/demo/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno similarity index 100% rename from examples/gno.land/r/demo/hof/render.gno rename to examples/gno.land/r/leon/hof/render.gno diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index 7288c176050..e7ffc49a37f 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -4,7 +4,7 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/r/demo/art/gnoface v0.0.0-latest gno.land/r/demo/art/millipede v0.0.0-latest - gno.land/r/demo/hof v0.0.0-latest gno.land/r/demo/mirror v0.0.0-latest gno.land/r/leon/config v0.0.0-latest + gno.land/r/leon/hof v0.0.0-latest ) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index 632b3f14a62..cf33260cc6b 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -8,9 +8,9 @@ import ( "gno.land/r/demo/art/gnoface" "gno.land/r/demo/art/millipede" - "gno.land/r/demo/hof" "gno.land/r/demo/mirror" "gno.land/r/leon/config" + "gno.land/r/leon/hof" ) var ( diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod index 9885cac19c2..0ef23834fb5 100644 --- a/examples/gno.land/r/manfred/home/gno.mod +++ b/examples/gno.land/r/manfred/home/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/manfred/home require ( - gno.land/r/demo/hof v0.0.0-latest + gno.land/r/leon/hof v0.0.0-latest gno.land/r/manfred/config v0.0.0-latest ) diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno index 4766f54e51f..3e29636439d 100644 --- a/examples/gno.land/r/manfred/home/home.gno +++ b/examples/gno.land/r/manfred/home/home.gno @@ -1,7 +1,7 @@ package home import ( - "gno.land/r/demo/hof" + "gno.land/r/leon/hof" "gno.land/r/manfred/config" ) diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod index 35e2fbb2119..412666e4171 100644 --- a/examples/gno.land/r/morgan/home/gno.mod +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -1,3 +1,3 @@ module gno.land/r/morgan/home -require gno.land/r/demo/hof v0.0.0-latest +require gno.land/r/leon/hof v0.0.0-latest diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno index 571f14ed5ec..20b66b895e3 100644 --- a/examples/gno.land/r/morgan/home/home.gno +++ b/examples/gno.land/r/morgan/home/home.gno @@ -1,6 +1,6 @@ package home -import "gno.land/r/demo/hof" +import "gno.land/r/leon/hof" const staticHome = `# morgan's (gn)home From 889082fad93961dc404723f09f3698c46312a6c2 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:52:40 +0900 Subject: [PATCH 242/345] feat(examples): add source code view doc, add `r/` README (#3163) ## Description Related to #3084 Adds a realm that teaches the user about the source code viewer in `gnoweb`. It also adds a `r/` root README.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan --- examples/gno.land/r/README.md | 10 ++++++++++ .../r/docs/{add/add.gno => adder/adder.gno} | 2 +- .../{add/add_test.gno => adder/adder_test.gno} | 8 ++++---- examples/gno.land/r/docs/{add => adder}/gno.mod | 2 +- examples/gno.land/r/docs/home/home.gno | 5 +++-- examples/gno.land/r/docs/source/gno.mod | 1 + examples/gno.land/r/docs/source/source.gno | 17 +++++++++++++++++ 7 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 examples/gno.land/r/README.md rename examples/gno.land/r/docs/{add/add.gno => adder/adder.gno} (98%) rename examples/gno.land/r/docs/{add/add_test.gno => adder/adder_test.gno} (74%) rename examples/gno.land/r/docs/{add => adder}/gno.mod (61%) create mode 100644 examples/gno.land/r/docs/source/gno.mod create mode 100644 examples/gno.land/r/docs/source/source.gno diff --git a/examples/gno.land/r/README.md b/examples/gno.land/r/README.md new file mode 100644 index 00000000000..b12a996d781 --- /dev/null +++ b/examples/gno.land/r/README.md @@ -0,0 +1,10 @@ +# `r/` + +This directory primarily contains realms. It further branches out into namespaces: +- `demo` - realms meant to demonstrate Gno functionality +- `docs` - realms meant to teach about specific packages and concepts +- `gnoland` - official gno.land realms +- `gov` - governance realms +- `sys` - system realms +- `x` - experimental realms +- `*` - can include personal namespaces, such as `manfred`, `leon`, etc. \ No newline at end of file diff --git a/examples/gno.land/r/docs/add/add.gno b/examples/gno.land/r/docs/adder/adder.gno similarity index 98% rename from examples/gno.land/r/docs/add/add.gno rename to examples/gno.land/r/docs/adder/adder.gno index ffc8f9c6877..cd96d241692 100644 --- a/examples/gno.land/r/docs/add/add.gno +++ b/examples/gno.land/r/docs/adder/adder.gno @@ -1,4 +1,4 @@ -package add +package adder import ( "strconv" diff --git a/examples/gno.land/r/docs/add/add_test.gno b/examples/gno.land/r/docs/adder/adder_test.gno similarity index 74% rename from examples/gno.land/r/docs/add/add_test.gno rename to examples/gno.land/r/docs/adder/adder_test.gno index 8994b895f7e..327908ab2d3 100644 --- a/examples/gno.land/r/docs/add/add_test.gno +++ b/examples/gno.land/r/docs/adder/adder_test.gno @@ -1,4 +1,4 @@ -package add +package adder import ( "testing" @@ -13,7 +13,7 @@ Current Number: 0 Last Updated: Never -[Increase Number](/r/docs/add$help&func=Add&n=42) +[Increase Number](/r/docs/adder$help&func=Add&n=42) ` if output != expected { t.Errorf("Initial Render failed, got:\n%s", output) @@ -36,9 +36,9 @@ Current Number: 5 Last Updated: 2009-02-13 23:31:30 -[Increase Number](/r/docs/add$help&func=Add&n=42) +[Increase Number](/r/docs/adder$help&func=Add&n=42) ` if output != expected { - t.Errorf("Final Render failed, got:\n%s", output) + t.Errorf("Final Render failed, got:\n%s\nexpected:\n%s", output, finalOutput) } } diff --git a/examples/gno.land/r/docs/add/gno.mod b/examples/gno.land/r/docs/adder/gno.mod similarity index 61% rename from examples/gno.land/r/docs/add/gno.mod rename to examples/gno.land/r/docs/adder/gno.mod index a66c63e0910..f8bbf9d6fe8 100644 --- a/examples/gno.land/r/docs/add/gno.mod +++ b/examples/gno.land/r/docs/adder/gno.mod @@ -1,3 +1,3 @@ -module gno.land/r/docs/add +module gno.land/r/docs/adder require gno.land/p/moul/txlink v0.0.0-latest diff --git a/examples/gno.land/r/docs/home/home.gno b/examples/gno.land/r/docs/home/home.gno index 2c581019380..6e61f08c11a 100644 --- a/examples/gno.land/r/docs/home/home.gno +++ b/examples/gno.land/r/docs/home/home.gno @@ -9,8 +9,9 @@ Explore various examples to learn more about Gno functionality and usage. ## Examples - [Hello World](/r/docs/hello) - A simple introductory example. -- [Add](/r/docs/add) - An interactive example to update a number with transactions. -- [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. +- [Adder](/r/docs/adder) - An interactive example to update a number with transactions. +- [Source](/r/docs/source) - View realm source code. +- [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. - ... ## Other resources diff --git a/examples/gno.land/r/docs/source/gno.mod b/examples/gno.land/r/docs/source/gno.mod new file mode 100644 index 00000000000..a2b5ad313c0 --- /dev/null +++ b/examples/gno.land/r/docs/source/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/source diff --git a/examples/gno.land/r/docs/source/source.gno b/examples/gno.land/r/docs/source/source.gno new file mode 100644 index 00000000000..45db3c98f06 --- /dev/null +++ b/examples/gno.land/r/docs/source/source.gno @@ -0,0 +1,17 @@ +package source + +// Welcome to the source code of this realm! + +func Render(_ string) string { + return `# Viewing source code +gno.land makes it easy to view the source code of any pure +package or realm, by using ABCI queries. + +gno.land's web frontend, ` + "`gnoweb`, " + ` makes this easy by +providing a intuitive UI that fetches the source of the +realm, that you can inspect anywhere by simply clicking +on the [source] button. + +Check it out in the top right corner! +` +} From 7718bc3734d594d5a98e689bed53e6f1590d8eca Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Thu, 21 Nov 2024 17:56:09 +0900 Subject: [PATCH 243/345] feat(p/int256): Optimize `int256` with two's complement implementation (#2846) # Description This PR optimizes the implementation of `int256` type. Key changes include: - Changed from storing sign and value separately in the Int256 struct to an implementation using two's complement method. - This reduces unnecessary operations and improves overall performance. ## Performance Result - Basic arithmetic operations (addition, subtraction, etc.): About 3x performance improvement (based on Go benchmarks, may differ slightly in gno) - Division operations: Up to 5x performance decrease compared to the previous implementation (can be improved by directly manipulating array fields, but not applied to avoid duplication with p/demo/uint256) ## Additional improvements: - Increased test coverage to 95%. **This change is expected to improve performance for most int256 operations. However, please note the performance degradation in division operations.** ## See Also https://github.com/gnolang/gno/pull/2750#pullrequestreview-2300695281
Contributors' checklist... - [X] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- examples/gno.land/p/demo/int256/LICENSE | 21 - examples/gno.land/p/demo/int256/README.md | 6 - examples/gno.land/p/demo/int256/absolute.gno | 18 - .../gno.land/p/demo/int256/absolute_test.gno | 105 ----- .../gno.land/p/demo/int256/arithmetic.gno | 436 ++++++++++++------ .../p/demo/int256/arithmetic_test.gno | 329 ++++++++----- examples/gno.land/p/demo/int256/bitwise.gno | 112 ++--- .../gno.land/p/demo/int256/bitwise_test.gno | 263 +++++------ examples/gno.land/p/demo/int256/cmp.gno | 85 ++-- examples/gno.land/p/demo/int256/cmp_test.gno | 34 +- .../gno.land/p/demo/int256/conversion.gno | 112 +++-- .../p/demo/int256/conversion_test.gno | 223 +++++---- examples/gno.land/p/demo/int256/doc.gno | 73 +++ examples/gno.land/p/demo/int256/int256.gno | 159 +++---- .../gno.land/p/demo/int256/int256_test.gno | 202 +++++++- .../p/demo/uint256/arithmetic_test.gno | 24 +- .../gno.land/p/demo/uint256/bitwise_test.gno | 16 +- examples/gno.land/p/demo/uint256/cmp_test.gno | 2 +- .../gno.land/p/demo/uint256/conversion.gno | 2 +- .../p/demo/uint256/conversion_test.gno | 2 +- .../gno.land/p/demo/uint256/uint256_test.gno | 8 +- 21 files changed, 1247 insertions(+), 985 deletions(-) delete mode 100644 examples/gno.land/p/demo/int256/LICENSE delete mode 100644 examples/gno.land/p/demo/int256/README.md delete mode 100644 examples/gno.land/p/demo/int256/absolute.gno delete mode 100644 examples/gno.land/p/demo/int256/absolute_test.gno create mode 100644 examples/gno.land/p/demo/int256/doc.gno diff --git a/examples/gno.land/p/demo/int256/LICENSE b/examples/gno.land/p/demo/int256/LICENSE deleted file mode 100644 index fc7e78a4875..00000000000 --- a/examples/gno.land/p/demo/int256/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Trịnh Đức Bảo Linh(Kevin) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/examples/gno.land/p/demo/int256/README.md b/examples/gno.land/p/demo/int256/README.md deleted file mode 100644 index be467471199..00000000000 --- a/examples/gno.land/p/demo/int256/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Fixed size signed 256-bit math library - -1. This is a library specialized at replacing the big.Int library for math based on signed 256-bit types. -2. It uses [uint256](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/uint256) as the underlying type. - -ported from [mempooler/int256](https://github.com/mempooler/int256) diff --git a/examples/gno.land/p/demo/int256/absolute.gno b/examples/gno.land/p/demo/int256/absolute.gno deleted file mode 100644 index 825dd60c62a..00000000000 --- a/examples/gno.land/p/demo/int256/absolute.gno +++ /dev/null @@ -1,18 +0,0 @@ -package int256 - -import "gno.land/p/demo/uint256" - -// Abs returns |z| -func (z *Int) Abs() *uint256.Uint { - return z.abs.Clone() -} - -// AbsGt returns true if |z| > x, where x is a uint256 -func (z *Int) AbsGt(x *uint256.Uint) bool { - return z.abs.Gt(x) -} - -// AbsLt returns true if |z| < x, where x is a uint256 -func (z *Int) AbsLt(x *uint256.Uint) bool { - return z.abs.Lt(x) -} diff --git a/examples/gno.land/p/demo/int256/absolute_test.gno b/examples/gno.land/p/demo/int256/absolute_test.gno deleted file mode 100644 index 55f6e41d0c8..00000000000 --- a/examples/gno.land/p/demo/int256/absolute_test.gno +++ /dev/null @@ -1,105 +0,0 @@ -package int256 - -import ( - "testing" - - "gno.land/p/demo/uint256" -) - -func TestAbs(t *testing.T) { - tests := []struct { - x, want string - }{ - {"0", "0"}, - {"1", "1"}, - {"-1", "1"}, - {"-2", "2"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := x.Abs() - - if got.ToString() != tc.want { - t.Errorf("Abs(%s) = %v, want %v", tc.x, got.ToString(), tc.want) - } - } -} - -func TestAbsGt(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "0", "false"}, - {"1", "0", "true"}, - {"-1", "0", "true"}, - {"-1", "1", "false"}, - {"-2", "1", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.AbsGt(y) - - if got != (tc.want == "true") { - t.Errorf("AbsGt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestAbsLt(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "0", "false"}, - {"1", "0", "false"}, - {"-1", "0", "false"}, - {"-1", "1", "false"}, - {"-2", "1", "false"}, - {"-5", "10", "true"}, - {"31330", "31337", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "false"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "false"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.AbsLt(y) - - if got != (tc.want == "true") { - t.Errorf("AbsLt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} diff --git a/examples/gno.land/p/demo/int256/arithmetic.gno b/examples/gno.land/p/demo/int256/arithmetic.gno index 8926fe1d6de..572dd15e7e6 100644 --- a/examples/gno.land/p/demo/int256/arithmetic.gno +++ b/examples/gno.land/p/demo/int256/arithmetic.gno @@ -1,202 +1,350 @@ package int256 -import "gno.land/p/demo/uint256" +import ( + "gno.land/p/demo/uint256" +) -func (z *Int) Add(x, y *Int) *Int { - z.initiateAbs() - - if x.neg == y.neg { - // If both numbers have the same sign, add their absolute values - z.abs.Add(x.abs, y.abs) - z.neg = x.neg - } else { - switch x.abs.Cmp(y.abs) { - case 1: // x > y - z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - case -1: // x < y - z.abs.Sub(y.abs, x.abs) - z.neg = y.neg - case 0: // x == y - z.abs = uint256.NewUint(0) - } - } +const divisionByZeroError = "division by zero" +// Add adds two int256 values and saves the result in z. +func (z *Int) Add(x, y *Int) *Int { + z.value.Add(&x.value, &y.value) return z } -// AddUint256 set z to the sum x + y, where y is a uint256, and returns z +// AddUint256 adds int256 and uint256 values and saves the result in z. func (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int { - if x.neg { - if x.abs.Gt(y) { - z.abs.Sub(x.abs, y) - z.neg = true - } else { - z.abs.Sub(y, x.abs) - z.neg = false - } - } else { - z.abs.Add(x.abs, y) - z.neg = false - } + z.value.Add(&x.value, y) return z } -// Sets z to the sum x + y, where z and x are uint256s and y is an int256. -func AddDelta(z, x *uint256.Uint, y *Int) { - if y.neg { - z.Sub(x, y.abs) - } else { - z.Add(x, y.abs) - } -} - -// Sets z to the sum x + y, where z and x are uint256s and y is an int256. -func AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool { - var overflow bool - if y.neg { - _, overflow = z.SubOverflow(x, y.abs) - } else { - _, overflow = z.AddOverflow(x, y.abs) - } - return overflow -} - -// Sub sets z to the difference x-y and returns z. +// Sub subtracts two int256 values and saves the result in z. func (z *Int) Sub(x, y *Int) *Int { - z.initiateAbs() - - if x.neg != y.neg { - // If sign are different, add the absolute values - z.abs.Add(x.abs, y.abs) - z.neg = x.neg - } else { - switch x.abs.Cmp(y.abs) { - case 1: // x > y - z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - case -1: // x < y - z.abs.Sub(y.abs, x.abs) - z.neg = !x.neg - case 0: // x == y - z.abs = uint256.NewUint(0) - } - } - - // Ensure zero is always positive - if z.abs.IsZero() { - z.neg = false - } + z.value.Sub(&x.value, &y.value) return z } -// SubUint256 set z to the difference x - y, where y is a uint256, and returns z +// SubUint256 subtracts uint256 and int256 values and saves the result in z. func (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int { - if x.neg { - z.abs.Add(x.abs, y) - z.neg = true - } else { - if x.abs.Lt(y) { - z.abs.Sub(y, x.abs) - z.neg = true - } else { - z.abs.Sub(x.abs, y) - z.neg = false - } - } + z.value.Sub(&x.value, y) return z } -// Mul sets z to the product x*y and returns z. +// Mul multiplies two int256 values and saves the result in z. +// +// It considers the signs of the operands to determine the sign of the result. func (z *Int) Mul(x, y *Int) *Int { - z.initiateAbs() + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() + + z.value.Mul(xAbs, yAbs) + + if xSign != ySign { + z.value.Neg(&z.value) + } - z.abs = z.abs.Mul(x.abs, y.abs) - z.neg = x.neg != y.neg && !z.abs.IsZero() // 0 has no sign return z } -// MulUint256 sets z to the product x*y, where y is a uint256, and returns z -func (z *Int) MulUint256(x *Int, y *uint256.Uint) *Int { - z.abs.Mul(x.abs, y) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg +// Abs returns the absolute value of z. +func (z *Int) Abs() *uint256.Uint { + if z.Sign() >= 0 { + return &z.value } - return z + + var absValue uint256.Uint + absValue.Sub(uint0, &z.value).Neg(&z.value) + + return &absValue } -// Div sets z to the quotient x/y for y != 0 and returns z. +// Div performs integer division z = x / y and returns z. +// If y == 0, it panics with a "division by zero" error. +// +// This function handles signed division using two's complement representation: +// 1. Determine the sign of the quotient based on the signs of x and y. +// 2. Perform unsigned division on the absolute values. +// 3. Adjust the result's sign if necessary. +// +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Let x = -6 (11111010 in two's complement) and y = 3 (00000011) +// +// Step 2: Determine signs +// +// x: negative (MSB is 1) +// y: positive (MSB is 0) +// +// Step 3: Calculate absolute values +// +// |x| = 6: 11111010 -> 00000110 +// NOT: 00000101 +// +1: 00000110 +// +// |y| = 3: 00000011 (already positive) +// +// Step 4: Unsigned division +// +// 6 / 3 = 2: 00000010 +// +// Step 5: Adjust sign (x and y have different signs) +// +// -2: 00000010 -> 11111110 +// NOT: 11111101 +// +1: 11111110 +// +// Note: This implementation rounds towards zero, as is standard in Go. func (z *Int) Div(x, y *Int) *Int { - z.initiateAbs() - - if y.abs.IsZero() { - panic("division by zero") + // Step 1: Check for division by zero + if y.IsZero() { + panic(divisionByZeroError) } - z.abs.Div(x.abs, y.abs) - z.neg = (x.neg != y.neg) && !z.abs.IsZero() // 0 has no sign + // Step 2, 3: Calculate the absolute values of x and y + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() - return z -} + // Step 4: Perform unsigned division on the absolute values + z.value.Div(xAbs, yAbs) -// DivUint256 sets z to the quotient x/y, where y is a uint256, and returns z -// If y == 0, z is set to 0 -func (z *Int) DivUint256(x *Int, y *uint256.Uint) *Int { - z.abs.Div(x.abs, y) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg + // Step 5: Adjust the sign of the result + // if x and y have different signs, the result must be negative + if xSign != ySign { + z.value.Neg(&z.value) } + return z } -// Quo sets z to the quotient x/y for y != 0 and returns z. -// If y == 0, a division-by-zero run-time panic occurs. -// OBS: differs from mempooler int256, we need to panic manually if y == 0 -// Quo implements truncated division (like Go); see QuoRem for more details. +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Let x = -7 (11111001 in two's complement) and y = 3 (00000011) +// +// Step 2: Determine signs +// +// x: negative (MSB is 1) +// y: positive (MSB is 0) +// +// Step 3: Calculate absolute values +// +// |x| = 7: 11111001 -> 00000111 +// NOT: 00000110 +// +1: 00000111 +// +// |y| = 3: 00000011 (already positive) +// +// Step 4: Unsigned division +// +// 7 / 3 = 2: 00000010 +// +// Step 5: Adjust sign (x and y have different signs) +// +// -2: 00000010 -> 11111110 +// NOT: 11111101 +// +1: 11111110 +// +// Final result: -2 (11111110 in two's complement) +// +// Note: This implementation rounds towards zero, as is standard in Go. func (z *Int) Quo(x, y *Int) *Int { + // Step 1: Check for division by zero if y.IsZero() { - panic("division by zero") + panic(divisionByZeroError) } - z.initiateAbs() + // Step 2, 3: Calculate the absolute values of x and y + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() + + // perform unsigned division on the absolute values + z.value.Div(xAbs, yAbs) + + // Step 5: Adjust the sign of the result + // if x and y have different signs, the result must be negative + if xSign != ySign { + z.value.Neg(&z.value) + } - z.abs = z.abs.Div(x.abs, y.abs) - z.neg = !(z.abs.IsZero()) && x.neg != y.neg // 0 has no sign return z } // Rem sets z to the remainder x%y for y != 0 and returns z. -// If y == 0, a division-by-zero run-time panic occurs. -// OBS: differs from mempooler int256, we need to panic manually if y == 0 -// Rem implements truncated modulus (like Go); see QuoRem for more details. +// +// The function performs the following steps: +// 1. Check for division by zero +// 2. Determine the signs of x and y +// 3. Calculate the absolute values of x and y +// 4. Perform unsigned division and get the remainder +// 5. Adjust the sign of the remainder +// +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Let x = -7 (11111001 in two's complement) and y = 3 (00000011) +// +// Step 2: Determine signs +// +// x: negative (MSB is 1) +// y: positive (MSB is 0) +// +// Step 3: Calculate absolute values +// +// |x| = 7: 11111001 -> 00000111 +// NOT: 00000110 +// +1: 00000111 +// +// |y| = 3: 00000011 (already positive) +// +// Step 4: Unsigned division +// +// 7 / 3 = 2 remainder 1 +// q = 2: 00000010 (not used in result) +// r = 1: 00000001 +// +// Step 5: Adjust sign of remainder (x is negative) +// +// -1: 00000001 -> 11111111 +// NOT: 11111110 +// +1: 11111111 +// +// Final result: -1 (11111111 in two's complement) +// +// Note: The sign of the remainder is always the same as the sign of the dividend (x). func (z *Int) Rem(x, y *Int) *Int { + // Step 1: Check for division by zero if y.IsZero() { - panic("division by zero") + panic(divisionByZeroError) } - z.initiateAbs() + // Step 2, 3 + xAbs, xSign := x.Abs(), x.Sign() + yAbs := y.Abs() - z.abs.Mod(x.abs, y.abs) - z.neg = z.abs.Sign() > 0 && x.neg // 0 has no sign + // Step 4: Perform unsigned division and get the remainder + var q, r uint256.Uint + q.DivMod(xAbs, yAbs, &r) + + // Step 5: Adjust the sign of the remainder + if xSign < 0 { + r.Neg(&r) + } + + z.value.Set(&r) return z } // Mod sets z to the modulus x%y for y != 0 and returns z. -// If y == 0, z is set to 0 (OBS: differs from the big.Int) +// The result (z) has the same sign as the divisor y. func (z *Int) Mod(x, y *Int) *Int { - if x.neg { - z.abs.Div(x.abs, y.abs) - z.abs.Add(z.abs, one) - z.abs.Mul(z.abs, y.abs) - z.abs.Sub(z.abs, x.abs) - z.abs.Mod(z.abs, y.abs) - } else { - z.abs.Mod(x.abs, y.abs) + return z.ModE(x, y) +} + +// DivE performs Euclidean division of x by y, setting z to the quotient and returning z. +// If y == 0, it panics with a "division by zero" error. +// +// Euclidean division satisfies the following properties: +// 1. The remainder is always non-negative: 0 <= x mod y < |y| +// 2. It follows the identity: x = y * (x div y) + (x mod y) +func (z *Int) DivE(x, y *Int) *Int { + if y.IsZero() { + panic(divisionByZeroError) } - z.neg = false + + // Compute the truncated division quotient + z.Quo(x, y) + + // Compute the remainder + r := new(Int).Rem(x, y) + + // If the remainder is negative, adjust the quotient + if r.Sign() < 0 { + if y.Sign() > 0 { + z.Sub(z, NewInt(1)) + } else { + z.Add(z, NewInt(1)) + } + } + return z } + +// ModE computes the Euclidean modulus of x by y, setting z to the result and returning z. +// If y == 0, it panics with a "division by zero" error. +// +// The Euclidean modulus is always non-negative and satisfies: +// +// 0 <= x mod y < |y| +// +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Case 1: Let x = -7 (11111001 in two's complement) and y = 3 (00000011) +// +// Step 1: Compute remainder (using Rem) +// +// Result of Rem: -1 (11111111 in two's complement) +// +// Step 2: Adjust sign (result is negative, y is positive) +// +// -1 + 3 = 2 +// 11111111 + 00000011 = 00000010 +// +// Final result: 2 (00000010) +// +// Case 2: Let x = -7 (11111001 in two's complement) and y = -3 (11111101 in two's complement) +// +// Step 1: Compute remainder (using Rem) +// +// Result of Rem: -1 (11111111 in two's complement) +// +// Step 2: Adjust sign (result is negative, y is negative) +// +// No adjustment needed +// +// Final result: -1 (11111111 in two's complement) +// +// Note: This implementation ensures that the result always has the same sign as y, +// which is different from the Rem operation. +func (z *Int) ModE(x, y *Int) *Int { + if y.IsZero() { + panic(divisionByZeroError) + } + + // Perform T-division to get the remainder + z.Rem(x, y) + + // Adjust the remainder if necessary + if z.Sign() >= 0 { + return z + } + if y.Sign() > 0 { + return z.Add(z, y) + } + + return z.Sub(z, y) +} + +// Sets z to the sum x + y, where z and x are uint256s and y is an int256. +// +// If the y is positive, it adds y.value to x. otherwise, it subtracts y.Abs() from x. +func AddDelta(z, x *uint256.Uint, y *Int) { + if y.Sign() >= 0 { + z.Add(x, &y.value) + } else { + z.Sub(x, y.Abs()) + } +} + +// Sets z to the sum x + y, where z and x are uint256s and y is an int256. +// +// This function returns true if the addition overflows, false otherwise. +func AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool { + var overflow bool + if y.Sign() >= 0 { + _, overflow = z.AddOverflow(x, &y.value) + } else { + var absY uint256.Uint + absY.Sub(uint0, &y.value) // absY = -y.value + _, overflow = z.SubOverflow(x, &absY) + } + + return overflow +} diff --git a/examples/gno.land/p/demo/int256/arithmetic_test.gno b/examples/gno.land/p/demo/int256/arithmetic_test.gno index 4cfa306890a..0b55552aca4 100644 --- a/examples/gno.land/p/demo/int256/arithmetic_test.gno +++ b/examples/gno.land/p/demo/int256/arithmetic_test.gno @@ -6,6 +6,36 @@ import ( "gno.land/p/demo/uint256" ) +const ( + // 2^255 - 1 + MAX_INT256 = "57896044618658097711785492504343953926634992332820282019728792003956564819967" + // -(2^255 - 1) + MINUS_MAX_INT256 = "-57896044618658097711785492504343953926634992332820282019728792003956564819967" + + // 2^255 - 1 + MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + MAX_UINT256_MINUS_1 = "115792089237316195423570985008687907853269984665640564039457584007913129639934" + + MINUS_MAX_UINT256 = "-115792089237316195423570985008687907853269984665640564039457584007913129639935" + MINUS_MAX_UINT256_PLUS_1 = "-115792089237316195423570985008687907853269984665640564039457584007913129639934" + + TWO_POW_128 = "340282366920938463463374607431768211456" + MINUS_TWO_POW_128 = "-340282366920938463463374607431768211456" + MINUS_TWO_POW_128_MINUS_1 = "-340282366920938463463374607431768211457" + TWO_POW_128_MINUS_1 = "340282366920938463463374607431768211455" + + TWO_POW_129_MINUS_1 = "680564733841876926926749214863536422911" + + TWO_POW_254 = "28948022309329048855892746252171976963317496166410141009864396001978282409984" + MINUS_TWO_POW_254 = "-28948022309329048855892746252171976963317496166410141009864396001978282409984" + HALF_MAX_INT256 = "28948022309329048855892746252171976963317496166410141009864396001978282409983" + MINUS_HALF_MAX_INT256 = "-28948022309329048855892746252171976963317496166410141009864396001978282409983" + + TWO_POW_255 = "57896044618658097711785492504343953926634992332820282019728792003956564819968" + MIN_INT256 = "-57896044618658097711785492504343953926634992332820282019728792003956564819968" + MIN_INT256_MINUS_1 = "-57896044618658097711785492504343953926634992332820282019728792003956564819969" +) + func TestAdd(t *testing.T) { tests := []struct { x, y, want string @@ -23,7 +53,10 @@ func TestAdd(t *testing.T) { {"-1", "3", "2"}, {"3", "-1", "2"}, // OVERFLOW - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "0"}, + {MAX_UINT256, "1", "0"}, + {MAX_INT256, "1", MIN_INT256}, + {MIN_INT256, "-1", MAX_INT256}, + {MAX_INT256, MAX_INT256, "-2"}, } for _, tc := range tests { @@ -49,7 +82,7 @@ func TestAdd(t *testing.T) { got.Add(x, y) if got.Neq(want) { - t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -64,10 +97,10 @@ func TestAddUint256(t *testing.T) { {"1", "2", "3"}, {"-1", "1", "0"}, {"-1", "3", "2"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "1"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639934", "-1"}, + {MINUS_MAX_UINT256_PLUS_1, MAX_UINT256, "1"}, + {MINUS_MAX_UINT256, MAX_UINT256_MINUS_1, "-1"}, // OVERFLOW - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, + {MINUS_MAX_UINT256, MAX_UINT256, "0"}, } for _, tc := range tests { @@ -93,7 +126,7 @@ func TestAddUint256(t *testing.T) { got.AddUint256(x, y) if got.Neq(want) { - t.Errorf("AddUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("AddUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -109,7 +142,7 @@ func TestAddDelta(t *testing.T) { {"1", "2", "3", "5"}, {"5", "10", "-3", "7"}, // underflow - {"1", "2", "-3", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + {"1", "2", "-3", MAX_UINT256}, } for _, tc := range tests { @@ -140,7 +173,7 @@ func TestAddDelta(t *testing.T) { AddDelta(z, x, y) if z.Neq(want) { - t.Errorf("AddDelta(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, z.ToString(), want.ToString()) + t.Errorf("AddDelta(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, z.String(), want.String()) } } } @@ -190,9 +223,11 @@ func TestSub(t *testing.T) { {"-1", "1", "-2"}, {"1", "-1", "2"}, {"-1", "-1", "0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {x: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", y: "1", want: "0"}, + {MINUS_MAX_UINT256, MINUS_MAX_UINT256, "0"}, + {MINUS_MAX_UINT256, "0", MINUS_MAX_UINT256}, + {MAX_INT256, MIN_INT256, "-1"}, + {MIN_INT256, MIN_INT256, "0"}, + {MAX_INT256, MAX_INT256, "0"}, } for _, tc := range tests { @@ -218,7 +253,7 @@ func TestSub(t *testing.T) { got.Sub(x, y) if got.Neq(want) { - t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -234,9 +269,9 @@ func TestSubUint256(t *testing.T) { {"-1", "1", "-2"}, {"-1", "3", "-4"}, // underflow - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "-0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "-1"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "3", "-2"}, + {MINUS_MAX_UINT256, "1", "0"}, + {MINUS_MAX_UINT256, "2", "-1"}, + {MINUS_MAX_UINT256, "3", "-2"}, } for _, tc := range tests { @@ -262,7 +297,7 @@ func TestSubUint256(t *testing.T) { got.SubUint256(x, y) if got.Neq(want) { - t.Errorf("SubUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("SubUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -276,6 +311,12 @@ func TestMul(t *testing.T) { {"5", "-3", "-15"}, {"0", "3", "0"}, {"3", "0", "0"}, + {"-5", "-3", "15"}, + {MAX_UINT256, "1", MAX_UINT256}, + {MAX_INT256, "2", "-2"}, + {TWO_POW_254, "2", MIN_INT256}, + {MINUS_TWO_POW_254, "2", MIN_INT256}, + {MAX_INT256, "1", MAX_INT256}, } for _, tc := range tests { @@ -301,51 +342,7 @@ func TestMul(t *testing.T) { got.Mul(x, y) if got.Neq(want) { - t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestMulUint256(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"1", "0", "0"}, - {"1", "1", "1"}, - {"1", "2", "2"}, - {"-1", "1", "-1"}, - {"-1", "3", "-3"}, - {"3", "4", "12"}, - {"-3", "4", "-12"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.MulUint256(x, y) - - if got.Neq(want) { - t.Errorf("MulUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -364,7 +361,10 @@ func TestDiv(t *testing.T) { {"-10", "3", "-3"}, {"7", "3", "2"}, {"-7", "3", "-2"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, // Max uint256 / 2 + // the maximum value of a positive number in int256 is less than the maximum value of a uint256 + {MAX_INT256, "2", HALF_MAX_INT256}, + {MINUS_MAX_INT256, "2", MINUS_HALF_MAX_INT256}, + {MAX_INT256, "-1", MINUS_MAX_INT256}, } for _, tt := range tests { @@ -372,11 +372,8 @@ func TestDiv(t *testing.T) { x := MustFromDecimal(tt.x) y := MustFromDecimal(tt.y) result := Zero().Div(x, y) - if result.ToString() != tt.expected { - t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.ToString(), tt.expected) - } - if result.abs.IsZero() && result.neg { - t.Errorf("Div(%s, %s) resulted in negative zero", tt.x, tt.y) + if result.String() != tt.expected { + t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.String(), tt.expected) } }) } @@ -393,21 +390,19 @@ func TestDiv(t *testing.T) { }) } -func TestDivUint256(t *testing.T) { +func TestQuo(t *testing.T) { tests := []struct { x, y, want string }{ {"0", "1", "0"}, - {"1", "0", "0"}, - {"1", "1", "1"}, - {"1", "2", "0"}, - {"-1", "1", "-1"}, - {"-1", "3", "0"}, - {"4", "3", "1"}, - {"25", "5", "5"}, - {"25", "4", "6"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + {"0", "-1", "0"}, + {"10", "1", "10"}, + {"10", "-1", "-10"}, + {"-10", "1", "-10"}, + {"-10", "-1", "10"}, + {"10", "-3", "-3"}, + {"-10", "3", "-3"}, + {"10", "3", "3"}, } for _, tc := range tests { @@ -417,7 +412,7 @@ func TestDivUint256(t *testing.T) { continue } - y, err := uint256.FromDecimal(tc.y) + y, err := FromDecimal(tc.y) if err != nil { t.Error(err) continue @@ -430,26 +425,28 @@ func TestDivUint256(t *testing.T) { } got := New() - got.DivUint256(x, y) + got.Quo(x, y) if got.Neq(want) { - t.Errorf("DivUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Quo(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestQuo(t *testing.T) { +func TestRem(t *testing.T) { tests := []struct { x, y, want string }{ {"0", "1", "0"}, {"0", "-1", "0"}, - {"10", "1", "10"}, - {"10", "-1", "-10"}, - {"-10", "1", "-10"}, - {"-10", "-1", "10"}, - {"10", "-3", "-3"}, - {"10", "3", "3"}, + {"10", "1", "0"}, + {"10", "-1", "0"}, + {"-10", "1", "0"}, + {"-10", "-1", "0"}, + {"10", "3", "1"}, + {"10", "-3", "1"}, + {"-10", "3", "-1"}, + {"-10", "-3", "-1"}, } for _, tc := range tests { @@ -472,15 +469,15 @@ func TestQuo(t *testing.T) { } got := New() - got.Quo(x, y) + got.Rem(x, y) if got.Neq(want) { - t.Errorf("Quo(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Rem(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestRem(t *testing.T) { +func TestMod(t *testing.T) { tests := []struct { x, y, want string }{ @@ -492,8 +489,8 @@ func TestRem(t *testing.T) { {"-10", "-1", "0"}, {"10", "3", "1"}, {"10", "-3", "1"}, - {"-10", "3", "-1"}, - {"-10", "-3", "-1"}, + {"-10", "3", "2"}, + {"-10", "-3", "2"}, } for _, tc := range tests { @@ -516,33 +513,51 @@ func TestRem(t *testing.T) { } got := New() - got.Rem(x, y) + got.Mod(x, y) if got.Neq(want) { - t.Errorf("Rem(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestMod(t *testing.T) { +func TestModeOverflow(t *testing.T) { tests := []struct { x, y, want string }{ - {"0", "1", "0"}, - {"0", "-1", "0"}, - {"10", "0", "0"}, - {"10", "1", "0"}, - {"10", "-1", "0"}, - {"-10", "0", "0"}, - {"-10", "1", "0"}, - {"-10", "-1", "0"}, - {"10", "3", "1"}, - {"10", "-3", "1"}, - {"-10", "3", "2"}, - {"-10", "-3", "2"}, + {MIN_INT256, "2", "0"}, // MIN_INT256 % 2 = 0 + {MAX_INT256, "2", "1"}, // MAX_INT256 % 2 = 1 + {MIN_INT256, "-1", "0"}, // MIN_INT256 % -1 = 0 + {MAX_INT256, "-1", "0"}, // MAX_INT256 % -1 = 0 + } + + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want := MustFromDecimal(tt.want) + got := New().Mod(x, y) + if got.Neq(want) { + t.Errorf("Mod(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) + } + } +} + +func TestModPanic(t *testing.T) { + tests := []struct { + x, y string + }{ + {"10", "0"}, + {"10", "-0"}, + {"-10", "0"}, + {"-10", "-0"}, } for _, tc := range tests { + defer func() { + if r := recover(); r == nil { + t.Errorf("Mod(%s, %s) did not panic", tc.x, tc.y) + } + }() x, err := FromDecimal(tc.x) if err != nil { t.Error(err) @@ -555,17 +570,105 @@ func TestMod(t *testing.T) { continue } - want, err := FromDecimal(tc.want) + result := New().Mod(x, y) + t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, result.String(), "0") + } +} + +func TestDivE(t *testing.T) { + testCases := []struct { + x, y int64 + want int64 + }{ + {8, 3, 2}, + {8, -3, -2}, + {-8, 3, -3}, + {-8, -3, 3}, + {1, 2, 0}, + {1, -2, 0}, + {-1, 2, -1}, + {-1, -2, 1}, + {0, 1, 0}, + {0, -1, 0}, + } + + for _, tc := range testCases { + x := NewInt(tc.x) + y := NewInt(tc.y) + want := NewInt(tc.want) + got := new(Int).DivE(x, y) + if got.Cmp(want) != 0 { + t.Errorf("DivE(%v, %v) = %v, want %v", tc.x, tc.y, got, want) + } + } +} + +func TestDivEByZero(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("DivE did not panic on division by zero") + } + }() + + x := NewInt(1) + y := NewInt(0) + new(Int).DivE(x, y) +} + +func TestModEByZero(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("ModE did not panic on division by zero") + } + }() + + x := NewInt(1) + y := NewInt(0) + new(Int).ModE(x, y) +} + +func TestLargeNumbers(t *testing.T) { + x, _ := new(Int).SetString("123456789012345678901234567890") + y, _ := new(Int).SetString("987654321098765432109876543210") + + // Expected results (calculated separately) + expectedQ, _ := new(Int).SetString("0") + expectedR, _ := new(Int).SetString("123456789012345678901234567890") + + gotQ := new(Int).DivE(x, y) + gotR := new(Int).ModE(x, y) + + if gotQ.Cmp(expectedQ) != 0 { + t.Errorf("DivE with large numbers: got %v, want %v", gotQ, expectedQ) + } + + if gotR.Cmp(expectedR) != 0 { + t.Errorf("ModE with large numbers: got %v, want %v", gotR, expectedR) + } +} + +func TestAbs(t *testing.T) { + tests := []struct { + x, want string + }{ + {"0", "0"}, + {"1", "1"}, + {"-1", "1"}, + {"-2", "2"}, + {"-100000000000", "100000000000"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) if err != nil { t.Error(err) continue } - got := New() - got.Mod(x, y) + got := x.Abs() - if got.Neq(want) { - t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + if got.String() != tc.want { + t.Errorf("Abs(%s) = %v, want %v", tc.x, got.String(), tc.want) } } } diff --git a/examples/gno.land/p/demo/int256/bitwise.gno b/examples/gno.land/p/demo/int256/bitwise.gno index c0d0f65f78f..1a1fe2e9720 100644 --- a/examples/gno.land/p/demo/int256/bitwise.gno +++ b/examples/gno.land/p/demo/int256/bitwise.gno @@ -1,94 +1,54 @@ package int256 -import ( - "gno.land/p/demo/uint256" -) - -// Or sets z = x | y and returns z. -func (z *Int) Or(x, y *Int) *Int { - if x.neg == y.neg { - if x.neg { - // (-x) | (-y) == ^(x-1) | ^(y-1) == ^((x-1) & (y-1)) == -(((x-1) & (y-1)) + 1) - x1 := new(uint256.Uint).Sub(x.abs, one) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.And(x1, y1), one) - z.neg = true // z cannot be zero if x and y are negative - return z - } - - // x | y == x | y - z.abs = z.abs.Or(x.abs, y.abs) - z.neg = false - return z - } - - // x.neg != y.neg - if x.neg { - x, y = y, x // | is symmetric - } - - // x | (-y) == x | ^(y-1) == ^((y-1) &^ x) == -(^((y-1) &^ x) + 1) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.AndNot(y1, x.abs), one) - z.neg = true // z cannot be zero if one of x or y is negative - +// Not sets z to the bitwise NOT of x and returns z. +// +// The bitwise NOT operation flips each bit of the operand. +func (z *Int) Not(x *Int) *Int { + z.value.Not(&x.value) return z } -// And sets z = x & y and returns z. +// And sets z to the bitwise AND of x and y and returns z. +// +// The bitwise AND operation results in a value that has a bit set +// only if both corresponding bits of the operands are set. func (z *Int) And(x, y *Int) *Int { - if x.neg == y.neg { - if x.neg { - // (-x) & (-y) == ^(x-1) & ^(y-1) == ^((x-1) | (y-1)) == -(((x-1) | (y-1)) + 1) - x1 := new(uint256.Uint).Sub(x.abs, one) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.Or(x1, y1), one) - z.neg = true // z cannot be zero if x and y are negative - return z - } - - // x & y == x & y - z.abs = z.abs.And(x.abs, y.abs) - z.neg = false - return z - } + z.value.And(&x.value, &y.value) + return z +} - // x.neg != y.neg - // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1192-1202;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 - if x.neg { - x, y = y, x // & is symmetric - } +// Or sets z to the bitwise OR of x and y and returns z. +// +// The bitwise OR operation results in a value that has a bit set +// if at least one of the corresponding bits of the operands is set. +func (z *Int) Or(x, y *Int) *Int { + z.value.Or(&x.value, &y.value) + return z +} - // x & (-y) == x & ^(y-1) == x &^ (y-1) - y1 := new(uint256.Uint).Sub(y.abs, uint256.One()) - z.abs = z.abs.AndNot(x.abs, y1) - z.neg = false +// Xor sets z to the bitwise XOR of x and y and returns z. +// +// The bitwise XOR operation results in a value that has a bit set +// only if the corresponding bits of the operands are different. +func (z *Int) Xor(x, y *Int) *Int { + z.value.Xor(&x.value, &y.value) return z } -// Rsh sets z = x >> n and returns z. -// OBS: Different from original implementation it was using math.Big +// Rsh sets z to the result of right-shifting x by n bits and returns z. +// +// Right shift operation moves all bits in the operand to the right by the specified number of positions. +// Bits shifted out on the right are discarded, and zeros are shifted in on the left. func (z *Int) Rsh(x *Int, n uint) *Int { - if !x.neg { - z.abs.Rsh(x.abs, n) - z.neg = x.neg - return z - } - - // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1118-1126;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 - t := NewInt(0).Sub(FromUint256(x.abs), NewInt(1)) - t = t.Rsh(t, n) - - _tmp := t.Add(t, NewInt(1)) - z.abs = _tmp.Abs() - z.neg = true - + z.value.Rsh(&x.value, n) return z } -// Lsh sets z = x << n and returns z. +// Lsh sets z to the result of left-shifting x by n bits and returns z. +// +// Left shift operation moves all bits in the operand to the left by the specified number of positions. +// Bits shifted out on the left are discarded, and zeros are shifted in on the right. func (z *Int) Lsh(x *Int, n uint) *Int { - z.abs.Lsh(x.abs, n) - z.neg = x.neg + z.value.Lsh(&x.value, n) return z } diff --git a/examples/gno.land/p/demo/int256/bitwise_test.gno b/examples/gno.land/p/demo/int256/bitwise_test.gno index 8dc16cd17ac..fc7b9bb578f 100644 --- a/examples/gno.land/p/demo/int256/bitwise_test.gno +++ b/examples/gno.land/p/demo/int256/bitwise_test.gno @@ -2,198 +2,157 @@ package int256 import ( "testing" - - "gno.land/p/demo/uint256" ) -func TestOr(t *testing.T) { +func TestBitwise_And(t *testing.T) { tests := []struct { - name string - x, y, want Int + x, y, want string }{ - { - name: "all zeroes", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, - { - name: "mixed", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, - { - name: "one operand all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, + {"5", "1", "1"}, // 0101 & 0001 = 0001 + {"-1", "1", "1"}, // 1111 & 0001 = 0001 + {"-5", "3", "3"}, // 1111...1011 & 0000...0011 = 0000...0011 + {MAX_UINT256, MAX_UINT256, MAX_UINT256}, + {TWO_POW_128, TWO_POW_128_MINUS_1, "0"}, // 2^128 & (2^128 - 1) = 0 + {TWO_POW_128, MAX_UINT256, TWO_POW_128}, // 2^128 & MAX_INT256 + {MAX_UINT256, TWO_POW_128, TWO_POW_128}, // MAX_INT256 & 2^128 } for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := New() - got.Or(&tc.x, &tc.y) - - if got.Neq(&tc.want) { - t.Errorf("Or(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) - } - }) + x, _ := FromDecimal(tc.x) + y, _ := FromDecimal(tc.y) + want, _ := FromDecimal(tc.want) + + got := new(Int).And(x, y) + + if got.Neq(want) { + t.Errorf("And(%s, %s) = %s, want %s", x.String(), y.String(), got.String(), want.String()) + } } } -func TestAnd(t *testing.T) { +func TestBitwise_Or(t *testing.T) { tests := []struct { - name string - x, y, want Int + x, y, want string }{ - { - name: "all zeroes", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, - { - name: "mixed", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "mixed 2", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "mixed 3", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "one operand zero", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "one operand all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false}, - }, + {"5", "1", "5"}, // 0101 | 0001 = 0101 + {"-1", "1", "-1"}, // 1111 | 0001 = 1111 + {"-5", "3", "-5"}, // 1111...1011 | 0000...0011 = 1111...1011 + {TWO_POW_128, TWO_POW_128_MINUS_1, TWO_POW_129_MINUS_1}, + {TWO_POW_128, MAX_UINT256, MAX_UINT256}, + {"0", TWO_POW_128, TWO_POW_128}, // 0 | 2^128 = 2^128 + {MAX_UINT256, TWO_POW_128, MAX_UINT256}, // MAX_INT256 | 2^128 = MAX_INT256 } for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := New() - got.And(&tc.x, &tc.y) - - if got.Neq(&tc.want) { - t.Errorf("And(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) - } - }) + x, _ := FromDecimal(tc.x) + y, _ := FromDecimal(tc.y) + want, _ := FromDecimal(tc.want) + + got := new(Int).Or(x, y) + + if got.Neq(want) { + t.Errorf( + "Or(%s, %s) = %s, want %s", + x.String(), y.String(), got.String(), want.String(), + ) + } } } -func TestRsh(t *testing.T) { +func TestBitwise_Not(t *testing.T) { tests := []struct { - x string - n uint - want string + x, want string }{ - {"1024", 0, "1024"}, - {"1024", 1, "512"}, - {"1024", 2, "256"}, - {"1024", 10, "1"}, - {"1024", 11, "0"}, - {"18446744073709551615", 0, "18446744073709551615"}, - {"18446744073709551615", 1, "9223372036854775807"}, - {"18446744073709551615", 62, "3"}, - {"18446744073709551615", 63, "1"}, - {"18446744073709551615", 64, "0"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 0, "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 1, "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 128, "340282366920938463463374607431768211455"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 255, "1"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 256, "0"}, - {"-1024", 0, "-1024"}, - {"-1024", 1, "-512"}, - {"-1024", 2, "-256"}, - {"-1024", 10, "-1"}, - {"-1024", 10, "-1"}, - {"-9223372036854775808", 0, "-9223372036854775808"}, - {"-9223372036854775808", 1, "-4611686018427387904"}, - {"-9223372036854775808", 62, "-2"}, - {"-9223372036854775808", 63, "-1"}, - {"-9223372036854775808", 64, "-1"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 0, "-57896044618658097711785492504343953926634992332820282019728792003956564819968"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 1, "-28948022309329048855892746252171976963317496166410141009864396001978282409984"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 253, "-4"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 254, "-2"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 255, "-1"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 256, "-1"}, + {"5", "-6"}, // 0101 -> 1111...1010 + {"-1", "0"}, // 1111...1111 -> 0000...0000 + {TWO_POW_128, MINUS_TWO_POW_128_MINUS_1}, // NOT 2^128 + {TWO_POW_255, MIN_INT256_MINUS_1}, // NOT 2^255 } for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue + x, _ := FromDecimal(tc.x) + want, _ := FromDecimal(tc.want) + + got := new(Int).Not(x) + + if got.Neq(want) { + t.Errorf("Not(%s) = %s, want %s", x.String(), got.String(), want.String()) } + } +} + +func TestBitwise_Xor(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"5", "1", "4"}, // 0101 ^ 0001 = 0100 + {"-1", "1", "-2"}, // 1111...1111 ^ 0000...0001 = 1111...1110 + {"-5", "3", "-8"}, // 1111...1011 ^ 0000...0011 = 1111...1000 + {TWO_POW_128, TWO_POW_128, "0"}, // 2^128 ^ 2^128 = 0 + {MAX_UINT256, TWO_POW_128, MINUS_TWO_POW_128_MINUS_1}, // MAX_INT256 ^ 2^128 + {TWO_POW_255, MAX_UINT256, MIN_INT256_MINUS_1}, // 2^255 ^ MAX_INT256 + } - got := New() - got.Rsh(x, tc.n) + for _, tt := range tests { + x, _ := FromDecimal(tt.x) + y, _ := FromDecimal(tt.y) + want, _ := FromDecimal(tt.want) - if got.ToString() != tc.want { - t.Errorf("Rsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) + got := new(Int).Xor(x, y) + + if got.Neq(want) { + t.Errorf("Xor(%s, %s) = %s, want %s", x.String(), y.String(), got.String(), want.String()) } } } -func TestLsh(t *testing.T) { +func TestBitwise_Rsh(t *testing.T) { tests := []struct { x string n uint want string }{ - {"1", 0, "1"}, - {"1", 1, "2"}, - {"1", 2, "4"}, - {"2", 0, "2"}, - {"2", 1, "4"}, - {"2", 2, "8"}, - {"-2", 0, "-2"}, - {"-4", 0, "-4"}, - {"-8", 0, "-8"}, + {"5", 1, "2"}, // 0101 >> 1 = 0010 + {"42", 3, "5"}, // 00101010 >> 3 = 00000101 + {TWO_POW_128, 128, "1"}, + {MAX_UINT256, 255, "1"}, + {TWO_POW_255, 254, "2"}, + {MINUS_TWO_POW_128, 128, TWO_POW_128_MINUS_1}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue + for _, tt := range tests { + x, _ := FromDecimal(tt.x) + want, _ := FromDecimal(tt.want) + + got := new(Int).Rsh(x, tt.n) + + if got.Neq(want) { + t.Errorf("Rsh(%s, %d) = %s, want %s", x.String(), tt.n, got.String(), want.String()) } + } +} + +func TestBitwise_Lsh(t *testing.T) { + tests := []struct { + x string + n uint + want string + }{ + {"5", 2, "20"}, // 0101 << 2 = 10100 + {"42", 5, "1344"}, // 00101010 << 5 = 10101000000 + {"1", 128, TWO_POW_128}, // 1 << 128 = 2^128 + {"2", 254, TWO_POW_255}, + {"1", 255, MIN_INT256}, // 1 << 255 = MIN_INT256 (overflow) + } + + for _, tt := range tests { + x, _ := FromDecimal(tt.x) + want, _ := FromDecimal(tt.want) - got := New() - got.Lsh(x, tc.n) + got := new(Int).Lsh(x, tt.n) - if got.ToString() != tc.want { - t.Errorf("Lsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) + if got.Neq(want) { + t.Errorf("Lsh(%s, %d) = %s, want %s", x.String(), tt.n, got.String(), want.String()) } } } diff --git a/examples/gno.land/p/demo/int256/cmp.gno b/examples/gno.land/p/demo/int256/cmp.gno index 426dfd76485..c91a25568e9 100644 --- a/examples/gno.land/p/demo/int256/cmp.gno +++ b/examples/gno.land/p/demo/int256/cmp.gno @@ -1,86 +1,59 @@ package int256 -// Eq returns true if z == x func (z *Int) Eq(x *Int) bool { - return (z.neg == x.neg) && z.abs.Eq(x.abs) + return z.value.Eq(&x.value) } -// Neq returns true if z != x func (z *Int) Neq(x *Int) bool { return !z.Eq(x) } -// Cmp compares x and y and returns: +// Cmp compares z and x and returns: // -// -1 if x < y -// 0 if x == y -// +1 if x > y -func (z *Int) Cmp(x *Int) (r int) { - // x cmp y == x cmp y - // x cmp (-y) == x - // (-x) cmp y == y - // (-x) cmp (-y) == -(x cmp y) - switch { - case z == x: - // nothing to do - case z.neg == x.neg: - r = z.abs.Cmp(x.abs) - if z.neg { - r = -r - } - case z.neg: - r = -1 - default: - r = 1 +// - 1 if z > x +// - 0 if z == x +// - -1 if z < x +func (z *Int) Cmp(x *Int) int { + zSign, xSign := z.Sign(), x.Sign() + + if zSign == xSign { + return z.value.Cmp(&x.value) } - return + + if zSign == 0 { + return -xSign + } + + return zSign } // IsZero returns true if z == 0 func (z *Int) IsZero() bool { - return z.abs.IsZero() + return z.value.IsZero() } // IsNeg returns true if z < 0 func (z *Int) IsNeg() bool { - return z.neg + return z.Sign() < 0 } -// Lt returns true if z < x func (z *Int) Lt(x *Int) bool { - if z.neg { - if x.neg { - return z.abs.Gt(x.abs) - } else { - return true - } - } else { - if x.neg { - return false - } else { - return z.abs.Lt(x.abs) - } - } + return z.Cmp(x) < 0 } -// Gt returns true if z > x func (z *Int) Gt(x *Int) bool { - if z.neg { - if x.neg { - return z.abs.Lt(x.abs) - } else { - return false - } - } else { - if x.neg { - return true - } else { - return z.abs.Gt(x.abs) - } - } + return z.Cmp(x) > 0 +} + +func (z *Int) Le(x *Int) bool { + return z.Cmp(x) <= 0 +} + +func (z *Int) Ge(x *Int) bool { + return z.Cmp(x) >= 0 } // Clone creates a new Int identical to z func (z *Int) Clone() *Int { - return &Int{z.abs.Clone(), z.neg} + return New().FromUint256(&z.value) } diff --git a/examples/gno.land/p/demo/int256/cmp_test.gno b/examples/gno.land/p/demo/int256/cmp_test.gno index 81b9231babe..c1c6559de3c 100644 --- a/examples/gno.land/p/demo/int256/cmp_test.gno +++ b/examples/gno.land/p/demo/int256/cmp_test.gno @@ -85,7 +85,7 @@ func TestCmp(t *testing.T) { {"-1", "0", -1}, {"0", "-1", 1}, {"1", "1", 0}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", 1}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", -1}, } for _, tc := range tests { @@ -140,7 +140,7 @@ func TestIsNeg(t *testing.T) { want bool }{ {"0", false}, - {"-0", true}, // TODO: should this be false? + {"-0", false}, {"1", false}, {"-1", true}, {"10", false}, @@ -173,7 +173,6 @@ func TestLt(t *testing.T) { {"0", "-1", false}, {"1", "1", false}, {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, } for _, tc := range tests { @@ -208,7 +207,6 @@ func TestGt(t *testing.T) { {"0", "-1", true}, {"1", "1", false}, {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, } for _, tc := range tests { @@ -232,21 +230,19 @@ func TestGt(t *testing.T) { } func TestClone(t *testing.T) { - tests := []struct { - x string - }{ - {"0"}, - {"-0"}, - {"1"}, - {"-1"}, - {"10"}, - {"-10"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + tests := []string{ + "0", + "-0", + "1", + "-1", + "10", + "-10", + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "-115792089237316195423570985008687907853269984665640564039457584007913129639935", } - for _, tc := range tests { - x, err := FromDecimal(tc.x) + for _, xStr := range tests { + x, err := FromDecimal(xStr) if err != nil { t.Error(err) continue @@ -254,8 +250,8 @@ func TestClone(t *testing.T) { y := x.Clone() - if x.Cmp(y) != 0 { - t.Errorf("Clone(%s) = %v, want %v", tc.x, y, x) + if x.Neq(y) { + t.Errorf("cloned value is not equal to original value") } } } diff --git a/examples/gno.land/p/demo/int256/conversion.gno b/examples/gno.land/p/demo/int256/conversion.gno index 9e264e7e46b..c8829ea754b 100644 --- a/examples/gno.land/p/demo/int256/conversion.gno +++ b/examples/gno.land/p/demo/int256/conversion.gno @@ -1,87 +1,107 @@ package int256 -import "gno.land/p/demo/uint256" +import ( + "math" -// SetInt64 sets z to x and returns z. -func (z *Int) SetInt64(x int64) *Int { - z.initiateAbs() + "gno.land/p/demo/uint256" +) - neg := false - if x < 0 { - neg = true - x = -x - } - if z.abs == nil { - panic("abs is nil") +// SetInt64 sets the Int to the value of the provided int64. +// +// This method allows for easy conversion from standard Go integer types +// to Int, correctly handling both positive and negative values. +func (z *Int) SetInt64(v int64) *Int { + if v >= 0 { + z.value.SetUint64(uint64(v)) + } else { + z.value.SetUint64(uint64(-v)).Neg(&z.value) } - z.abs = z.abs.SetUint64(uint64(x)) - z.neg = neg return z } -// SetUint64 sets z to x and returns z. -func (z *Int) SetUint64(x uint64) *Int { - z.initiateAbs() - - if z.abs == nil { - panic("abs is nil") - } - z.abs = z.abs.SetUint64(x) - z.neg = false +// SetUint64 sets the Int to the value of the provided uint64. +func (z *Int) SetUint64(v uint64) *Int { + z.value.SetUint64(v) return z } // Uint64 returns the lower 64-bits of z func (z *Int) Uint64() uint64 { - return z.abs.Uint64() + if z.Sign() < 0 { + panic("cannot convert negative int256 to uint64") + } + if z.value.Gt(uint256.NewUint(0).SetUint64(math.MaxUint64)) { + panic("overflow: int256 does not fit in uint64 type") + } + return z.value.Uint64() } // Int64 returns the lower 64-bits of z func (z *Int) Int64() int64 { - _abs := z.abs.Clone() - - if z.neg { - return -int64(_abs.Uint64()) + if z.Sign() >= 0 { + if z.value.BitLen() > 64 { + panic("overflow: int256 does not fit in int64 type") + } + return int64(z.value.Uint64()) + } + var temp uint256.Uint + temp.Sub(uint256.NewUint(0), &z.value) // temp = -z.value + if temp.BitLen() > 64 { + panic("overflow: int256 does not fit in int64 type") } - return int64(_abs.Uint64()) + return -int64(temp.Uint64()) } // Neg sets z to -x and returns z.) func (z *Int) Neg(x *Int) *Int { - z.abs.Set(x.abs) - if z.abs.IsZero() { - z.neg = false + if x.IsZero() { + z.value.Clear() } else { - z.neg = !x.neg + z.value.Neg(&x.value) } return z } // Set sets z to x and returns z. func (z *Int) Set(x *Int) *Int { - z.abs.Set(x.abs) - z.neg = x.neg + z.value.Set(&x.value) return z } // SetFromUint256 converts a uint256.Uint to Int and sets the value to z. func (z *Int) SetUint256(x *uint256.Uint) *Int { - z.abs.Set(x) - z.neg = false + z.value.Set(x) return z } -// OBS, differs from original mempooler int256 -// ToString returns the decimal representation of z. -func (z *Int) ToString() string { - if z == nil { - panic("int256: nil pointer to ToString()") +// ToString returns a string representation of z in base 10. +// The string is prefixed with a minus sign if z is negative. +func (z *Int) String() string { + if z.value.IsZero() { + return "0" } - - t := z.abs.Dec() - if z.neg { - return "-" + t + sign := z.Sign() + var temp uint256.Uint + if sign >= 0 { + temp.Set(&z.value) + } else { + // temp = -z.value + temp.Sub(uint256.NewUint(0), &z.value) + } + s := temp.Dec() + if sign < 0 { + return "-" + s } + return s +} - return t +// NilToZero returns the Int if it's not nil, or a new zero-valued Int otherwise. +// +// This method is useful for safely handling potentially nil Int pointers, +// ensuring that operations always have a valid Int to work with. +func (z *Int) NilToZero() *Int { + if z == nil { + return Zero() + } + return z } diff --git a/examples/gno.land/p/demo/int256/conversion_test.gno b/examples/gno.land/p/demo/int256/conversion_test.gno index b085a77a15a..44e59fe79de 100644 --- a/examples/gno.land/p/demo/int256/conversion_test.gno +++ b/examples/gno.land/p/demo/int256/conversion_test.gno @@ -8,43 +8,20 @@ import ( func TestSetInt64(t *testing.T) { tests := []struct { - x int64 - want string + v int64 + expect int }{ - {0, "0"}, - {1, "1"}, - {-1, "-1"}, - {9223372036854775807, "9223372036854775807"}, - {-9223372036854775808, "-9223372036854775808"}, + {0, 0}, + {1, 1}, + {-1, -1}, + {9223372036854775807, 1}, // overflow (max int64) + {-9223372036854775808, -1}, // underflow (min int64) } - for _, tc := range tests { - var z Int - z.SetInt64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetInt64(%d) = %s, want %s", tc.x, got, tc.want) - } - } -} - -func TestSetUint64(t *testing.T) { - tests := []struct { - x uint64 - want string - }{ - {0, "0"}, - {1, "1"}, - } - - for _, tc := range tests { - var z Int - z.SetUint64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetUint64(%d) = %s, want %s", tc.x, got, tc.want) + for _, tt := range tests { + z := New().SetInt64(tt.v) + if z.Sign() != tt.expect { + t.Errorf("SetInt64(%d) = %d, want %d", tt.v, z.Sign(), tt.expect) } } } @@ -59,24 +36,39 @@ func TestUint64(t *testing.T) { {"9223372036854775807", 9223372036854775807}, {"9223372036854775808", 9223372036854775808}, {"18446744073709551615", 18446744073709551615}, - {"18446744073709551616", 0}, - {"18446744073709551617", 1}, - {"-1", 1}, - {"-18446744073709551615", 18446744073709551615}, - {"-18446744073709551616", 0}, - {"-18446744073709551617", 1}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) got := z.Uint64() - if got != tc.want { - t.Errorf("Uint64(%s) = %d, want %d", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("Uint64(%s) = %d, want %d", tt.x, got, tt.want) } } } +func TestUint64_Panic(t *testing.T) { + tests := []struct { + x string + }{ + {"-1"}, + {"18446744073709551616"}, + {"18446744073709551617"}, + } + + for _, tt := range tests { + defer func() { + if r := recover(); r == nil { + t.Errorf("Uint64(%s) did not panic", tt.x) + } + }() + + z := MustFromDecimal(tt.x) + z.Uint64() + } +} + func TestInt64(t *testing.T) { tests := []struct { x string @@ -85,22 +77,40 @@ func TestInt64(t *testing.T) { {"0", 0}, {"1", 1}, {"9223372036854775807", 9223372036854775807}, - {"18446744073709551616", 0}, - {"18446744073709551617", 1}, {"-1", -1}, {"-9223372036854775808", -9223372036854775808}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) got := z.Int64() - if got != tc.want { - t.Errorf("Uint64(%s) = %d, want %d", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("Uint64(%s) = %d, want %d", tt.x, got, tt.want) } } } +func TestInt64_Panic(t *testing.T) { + tests := []struct { + x string + }{ + {"18446744073709551616"}, + {"18446744073709551617"}, + } + + for _, tt := range tests { + defer func() { + if r := recover(); r == nil { + t.Errorf("Int64(%s) did not panic", tt.x) + } + }() + + z := MustFromDecimal(tt.x) + z.Int64() + } +} + func TestNeg(t *testing.T) { tests := []struct { x string @@ -113,13 +123,13 @@ func TestNeg(t *testing.T) { {"-18446744073709551615", "18446744073709551615"}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) z.Neg(z) - got := z.ToString() - if got != tc.want { - t.Errorf("Neg(%s) = %s, want %s", tc.x, got, tc.want) + got := z.String() + if got != tt.want { + t.Errorf("Neg(%s) = %s, want %s", tt.x, got, tt.want) } } } @@ -136,13 +146,13 @@ func TestSet(t *testing.T) { {"-18446744073709551615", "-18446744073709551615"}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) z.Set(z) - got := z.ToString() - if got != tc.want { - t.Errorf("Set(%s) = %s, want %s", tc.x, got, tc.want) + got := z.String() + if got != tt.want { + t.Errorf("Set(%s) = %s, want %s", tt.x, got, tt.want) } } } @@ -158,77 +168,54 @@ func TestSetUint256(t *testing.T) { {"18446744073709551615", "18446744073709551615"}, } - for _, tc := range tests { + for _, tt := range tests { got := New() - z := uint256.MustFromDecimal(tc.x) + z := uint256.MustFromDecimal(tt.x) got.SetUint256(z) - if got.ToString() != tc.want { - t.Errorf("SetUint256(%s) = %s, want %s", tc.x, got.ToString(), tc.want) + if got.String() != tt.want { + t.Errorf("SetUint256(%s) = %s, want %s", tt.x, got.String(), tt.want) } } } -func TestToString(t *testing.T) { +func TestString(t *testing.T) { tests := []struct { - name string - setup func() *Int + input string expected string }{ - { - name: "Zero from subtraction", - setup: func() *Int { - minusThree := MustFromDecimal("-3") - three := MustFromDecimal("3") - return Zero().Add(minusThree, three) - }, - expected: "0", - }, - { - name: "Zero from right shift", - setup: func() *Int { - return Zero().Rsh(One(), 1234) - }, - expected: "0", - }, - { - name: "Positive number", - setup: func() *Int { - return MustFromDecimal("42") - }, - expected: "42", - }, - { - name: "Negative number", - setup: func() *Int { - return MustFromDecimal("-42") - }, - expected: "-42", - }, - { - name: "Large positive number", - setup: func() *Int { - return MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") - }, - expected: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - { - name: "Large negative number", - setup: func() *Int { - return MustFromDecimal("-115792089237316195423570985008687907853269984665640564039457584007913129639935") - }, - expected: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, + {"0", "0"}, + {"1", "1"}, + {"-1", "-1"}, + {"123456789", "123456789"}, + {"-123456789", "-123456789"}, + {"18446744073709551615", "18446744073709551615"}, // max uint64 + {"-18446744073709551615", "-18446744073709551615"}, + {TWO_POW_128_MINUS_1, TWO_POW_128_MINUS_1}, + {MINUS_TWO_POW_128, MINUS_TWO_POW_128}, + {MIN_INT256, MIN_INT256}, + {MAX_INT256, MAX_INT256}, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - z := tt.setup() - result := z.ToString() - if result != tt.expected { - t.Errorf("ToString() = %s, want %s", result, tt.expected) - } - }) + x, err := FromDecimal(tt.input) + if err != nil { + t.Errorf("Failed to parse input (%s): %v", tt.input, err) + continue + } + + output := x.String() + + if output != tt.expected { + t.Errorf("String(%s) = %s, want %s", tt.input, output, tt.expected) + } + } +} + +func TestNilToZero(t *testing.T) { + z := New().NilToZero() + if z.Sign() != 0 { + t.Errorf("NilToZero() = %d, want %d", z.Sign(), 0) } } diff --git a/examples/gno.land/p/demo/int256/doc.gno b/examples/gno.land/p/demo/int256/doc.gno new file mode 100644 index 00000000000..ec7d2d3bf9a --- /dev/null +++ b/examples/gno.land/p/demo/int256/doc.gno @@ -0,0 +1,73 @@ +// The int256 package provides a 256-bit signed interger type for gno, +// supporting arithmetic operations and bitwise manipulation. +// +// It designed for applications that require high-precision arithmetic +// beyond the standard 64-bit range. +// +// ## Features +// +// - 256-bit Signed Integers: Support for large integer ranging from -2^255 to 2^255-1. +// - Two's Complement Representation: Efficient storage and computation using two's complement. +// - Arithmetic Operations: Add, Sub, Mul, Div, Mod, Inc, Dec, etc. +// - Bitwise Operations: And, Or, Xor, Not, etc. +// - Comparison Operations: Cmp, Eq, Lt, Gt, etc. +// - Conversion Functions: Int to Uint, Uint to Int, etc. +// - String Parsing and Formatting: Convert to and from decimal string representation. +// +// ## Notes +// +// - Some methods may panic when encountering invalid inputs or overflows. +// - The `int256.Int` type can interact with `uint256.Uint` from the `p/demo/uint256` package. +// - Unlike `math/big.Int`, the `int256.Int` type has fixed size (256-bit) and does not support +// arbitrary precision arithmetic. +// +// # Division and modulus operations +// +// This package provides three different division and modulus operations: +// +// - Div and Rem: Truncated division (T-division) +// - Quo and Mod: Floored division (F-division) +// - DivE and ModE: Euclidean division (E-division) +// +// Truncated division (Div, Rem) is the most common implementation in modern processors +// and programming languages. It rounds quotients towards zero and the remainder +// always has the same sign as the dividend. +// +// Floored division (Quo, Mod) always rounds quotients towards negative infinity. +// This ensures that the modulus is always non-negative for a positive divisor, +// which can be useful in certain algorithms. +// +// Euclidean division (DivE, ModE) ensures that the remainder is always non-negative, +// regardless of the signs of the dividend and divisor. This has several mathematical +// advantages: +// +// 1. It satisfies the unique division with remainder theorem. +// 2. It preserves division and modulus properties for negative divisors. +// 3. It allows for optimizations in divisions by powers of two. +// +// [+] Currently, ModE and Mod are shared the same implementation. +// +// ## Performance considerations: +// +// - For most operations, the performance difference between these division types is negligible. +// - Euclidean division may require an extra comparison and potentially an addition, +// which could impact performance in extremely performance-critical scenarios. +// - For divisions by powers of two, Euclidean division can be optimized to use +// bitwise operations, potentially offering better performance. +// +// ## Usage guidelines: +// +// - Use Div and Rem for general-purpose division that matches most common expectations. +// - Use Quo and Mod when you need a non-negative remainder for positive divisors, +// or when implementing algorithms that assume floored division. +// - Use DivE and ModE when you need the mathematical properties of Euclidean division, +// or when working with algorithms that specifically require it. +// +// Note: When working with negative numbers, be aware of the differences in behavior +// between these division types, especially at the boundaries of integer ranges. +// +// ## References +// +// Daan Leijen, “Division and Modulus for Computer Scientists”: +// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf +package int256 diff --git a/examples/gno.land/p/demo/int256/int256.gno b/examples/gno.land/p/demo/int256/int256.gno index caccd17d531..dd3064ae946 100644 --- a/examples/gno.land/p/demo/int256/int256.gno +++ b/examples/gno.land/p/demo/int256/int256.gno @@ -1,64 +1,87 @@ -// This package provides a 256-bit signed integer type, Int, and associated functions. package int256 import ( + "errors" + "gno.land/p/demo/uint256" ) -var one = uint256.NewUint(1) +var ( + int1 = NewInt(1) + uint0 = uint256.NewUint(0) + uint1 = uint256.NewUint(1) +) type Int struct { - abs *uint256.Uint - neg bool + value uint256.Uint } -// Zero returns a new Int set to 0. -func Zero() *Int { - return NewInt(0) +// New creates and returns a new Int initialized to zero. +func New() *Int { + return &Int{} } -// One returns a new Int set to 1. -func One() *Int { - return NewInt(1) +// NewInt allocates and returns a new Int set to the value of the provided int64. +func NewInt(x int64) *Int { + return New().SetInt64(x) } -// Sign returns: +// Zero returns a new Int initialized to 0. // -// -1 if x < 0 -// 0 if x == 0 -// +1 if x > 0 -func (z *Int) Sign() int { - z.initiateAbs() +// This function is useful for creating a starting point for calculations or +// when an explicit zero value is needed. +func Zero() *Int { return &Int{} } - if z.abs.IsZero() { - return 0 - } - if z.neg { - return -1 - } - return 1 -} - -// New returns a new Int set to 0. -func New() *Int { +// One returns a new Int initialized to one. +// +// This function is convenient for operations that require a unit value, +// such as incrementing or serving as an identity element in multiplication. +func One() *Int { return &Int{ - abs: new(uint256.Uint), + value: *uint256.NewUint(1), } } -// NewInt allocates and returns a new Int set to x. -func NewInt(x int64) *Int { - return New().SetInt64(x) +// Sign determines the sign of the Int. +// +// It returns -1 for negative numbers, 0 for zero, and +1 for positive numbers. +func (z *Int) Sign() int { + if z == nil || z.IsZero() { + return 0 + } + // Right shift the value by 255 bits to check the sign bit. + // In two's complement representation, the most significant bit (MSB) is the sign bit. + // If the MSB is 0, the number is positive; if it is 1, the number is negative. + // + // Example: + // Original value: 1 0 1 0 ... 0 1 (256 bits) + // After Rsh 255: 0 0 0 0 ... 0 1 (1 bit) + // + // This approach is highly efficient as it avoids the need for comparisons + // or arithmetic operations on the full 256-bit number. Instead it reduces + // the problem to checking a single bit. + // + // Additionally, this method will work correctly for all values, + // including the minimum possible negative number (which in two's complement + // doesn't have a positive counterpart in the same bit range). + var temp uint256.Uint + if temp.Rsh(&z.value, 255).IsZero() { + return 1 + } + return -1 } -// FromDecimal returns a new Int from a decimal string. -// Returns a new Int and an error if the string is not a valid decimal. +// FromDecimal creates a new Int from a decimal string representation. +// It handles both positive and negative values. +// +// This function is useful for parsing user input or reading numeric data +// from text-based formats. func FromDecimal(s string) (*Int, error) { - return new(Int).SetString(s) + return New().SetString(s) } -// MustFromDecimal returns a new Int from a decimal string. -// Panics if the string is not a valid decimal. +// MustFromDecimal is similar to FromDecimal but panics if the input string +// is not a valid decimal representation. func MustFromDecimal(s string) *Int { z, err := FromDecimal(s) if err != nil { @@ -67,60 +90,40 @@ func MustFromDecimal(s string) *Int { return z } -// SetString sets s to the value of z and returns z and a boolean indicating success. +// SetString sets the Int to the value represented by the input string. +// This method supports decimal string representations of integers and handles +// both positive and negative values. func (z *Int) SetString(s string) (*Int, error) { - neg := false - // Remove max one leading + - if len(s) > 0 && s[0] == '+' { - neg = false - s = s[1:] + if len(s) == 0 { + return nil, errors.New("cannot set int256 from empty string") } - if len(s) > 0 && s[0] == '-' { - neg = true + // Check for negative sign + neg := s[0] == '-' + if neg || s[0] == '+' { s = s[1:] } - var ( - abs *uint256.Uint - err error - ) - abs, err = uint256.FromDecimal(s) + + // Convert string to uint256 + temp, err := uint256.FromDecimal(s) if err != nil { return nil, err } - return &Int{ - abs, - neg, - }, nil -} - -// FromUint256 is a convenience-constructor from uint256.Uint. -// Returns a new Int and whether overflow occurred. -// OBS: If u is `nil`, this method returns `nil, false` -func FromUint256(x *uint256.Uint) *Int { - if x == nil { - return nil + // If negative, negate the uint256 value + if neg { + temp.Neg(temp) } - z := Zero() - z.SetUint256(x) - return z + z.value.Set(temp) + return z, nil } -// OBS, differs from original mempooler int256 -// NilToZero sets z to 0 and return it if it's nil, otherwise it returns z -func (z *Int) NilToZero() *Int { - if z == nil { - return NewInt(0) - } +// FromUint256 sets the Int to the value of the provided Uint256. +// +// This method allows for conversion from unsigned 256-bit integers +// to signed integers. +func (z *Int) FromUint256(v *uint256.Uint) *Int { + z.value.Set(v) return z } - -// initiateAbs sets default value for `z` or `z.abs` value if is nil -// OBS: differs from mempooler int256. It checks not only `z.abs` but also `z` -func (z *Int) initiateAbs() { - if z == nil || z.abs == nil { - z.abs = new(uint256.Uint) - } -} diff --git a/examples/gno.land/p/demo/int256/int256_test.gno b/examples/gno.land/p/demo/int256/int256_test.gno index 7c8181d1bec..9fbe22bf072 100644 --- a/examples/gno.land/p/demo/int256/int256_test.gno +++ b/examples/gno.land/p/demo/int256/int256_test.gno @@ -1,7 +1,153 @@ -// ported from github.com/mempooler/int256 package int256 -import "testing" +import ( + "testing" + + "gno.land/p/demo/uint256" +) + +func TestInitializers(t *testing.T) { + tests := []struct { + name string + fn func() *Int + wantSign int + wantStr string + }{ + {"Zero", Zero, 0, "0"}, + {"New", New, 0, "0"}, + {"One", One, 1, "1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := tt.fn() + if z.Sign() != tt.wantSign { + t.Errorf("%s() = %d, want %d", tt.name, z.Sign(), tt.wantSign) + } + if z.String() != tt.wantStr { + t.Errorf("%s() = %s, want %s", tt.name, z.String(), tt.wantStr) + } + }) + } +} + +func TestNewInt(t *testing.T) { + tests := []struct { + input int64 + expected int + }{ + {0, 0}, + {1, 1}, + {-1, -1}, + {9223372036854775807, 1}, // max int64 + {-9223372036854775808, -1}, // min int64 + } + + for _, tt := range tests { + z := NewInt(tt.input) + if z.Sign() != tt.expected { + t.Errorf("NewInt(%d) = %d, want %d", tt.input, z.Sign(), tt.expected) + } + } +} + +func TestFromDecimal(t *testing.T) { + tests := []struct { + input string + expected int + isError bool + }{ + {"0", 0, false}, + {"1", 1, false}, + {"-1", -1, false}, + {"123456789", 1, false}, + {"-123456789", -1, false}, + {"invalid", 0, true}, + } + + for _, tt := range tests { + z, err := FromDecimal(tt.input) + if tt.isError { + if err == nil { + t.Errorf("FromDecimal(%s) expected error, but got nil", tt.input) + } + } else { + if err != nil { + t.Errorf("FromDecimal(%s) unexpected error: %v", tt.input, err) + } else if z.Sign() != tt.expected { + t.Errorf("FromDecimal(%s) sign is incorrect. Expected: %d, Actual: %d", tt.input, tt.expected, z.Sign()) + } + } + } +} + +func TestMustFromDecimal(t *testing.T) { + tests := []struct { + input string + expected int + shouldPanic bool + }{ + {"0", 0, false}, + {"1", 1, false}, + {"-1", -1, false}, + {"123", 1, false}, + {"invalid", 0, true}, + } + + for _, tt := range tests { + if tt.shouldPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MustFromDecimal(%q) expected panic, but got nil", tt.input) + } + }() + } + + z := MustFromDecimal(tt.input) + if !tt.shouldPanic && z.Sign() != tt.expected { + t.Errorf("MustFromDecimal(%q) sign is incorrect. Expected: %d, Actual: %d", tt.input, tt.expected, z.Sign()) + } + } +} + +func TestSetUint64(t *testing.T) { + tests := []uint64{ + 0, + 1, + 18446744073709551615, // max uint64 + } + + for _, tt := range tests { + z := New().SetUint64(tt) + if z.Sign() < 0 { + t.Errorf("SetUint64(%d) result is negative", tt) + } + if tt == 0 && z.Sign() != 0 { + t.Errorf("SetUint64(0) result is not zero") + } + if tt > 0 && z.Sign() != 1 { + t.Errorf("SetUint64(%d) result is not positive", tt) + } + } +} + +func TestFromUint256(t *testing.T) { + tests := []struct { + input *uint256.Uint + expected int + }{ + {uint256.NewUint(0), 0}, + {uint256.NewUint(1), 1}, + {uint256.NewUint(18446744073709551615), 1}, + } + + for _, tt := range tests { + z := New().FromUint256(tt.input) + if z.Sign() != tt.expected { + t.Errorf("FromUint256(%v) = %d, want %d", tt.input, z.Sign(), tt.expected) + } + } +} func TestSign(t *testing.T) { tests := []struct { @@ -9,15 +155,59 @@ func TestSign(t *testing.T) { want int }{ {"0", 0}, + {"-0", 0}, + {"+0", 0}, {"1", 1}, {"-1", -1}, + {"9223372036854775807", 1}, + {"-9223372036854775808", -1}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) got := z.Sign() - if got != tc.want { - t.Errorf("Sign(%s) = %d, want %d", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("Sign(%s) = %d, want %d", tt.x, got, tt.want) + } + } +} + +func BenchmarkSign(b *testing.B) { + z := New() + for i := 0; i < b.N; i++ { + z.SetUint64(uint64(i)) + z.Sign() + } +} + +func TestSetAndToString(t *testing.T) { + tests := []struct { + input string + expected int + isError bool + }{ + {"0", 0, false}, + {"1", 1, false}, + {"-1", -1, false}, + {"123456789", 1, false}, + {"-123456789", -1, false}, + {"invalid", 0, true}, + } + + for _, tt := range tests { + z, err := New().SetString(tt.input) + if tt.isError { + if err == nil { + t.Errorf("SetString(%s) expected error, but got nil", tt.input) + } + } else { + if err != nil { + t.Errorf("SetString(%s) unexpected error: %v", tt.input, err) + } else if z.Sign() != tt.expected { + t.Errorf("SetString(%s) sign is incorrect. Expected: %d, Actual: %d", tt.input, tt.expected, z.Sign()) + } else if z.String() != tt.input { + t.Errorf("SetString(%s) string representation is incorrect. Expected: %s, Actual: %s", tt.input, tt.input, z.String()) + } } } } diff --git a/examples/gno.land/p/demo/uint256/arithmetic_test.gno b/examples/gno.land/p/demo/uint256/arithmetic_test.gno index 079d89fa794..addd33db997 100644 --- a/examples/gno.land/p/demo/uint256/arithmetic_test.gno +++ b/examples/gno.land/p/demo/uint256/arithmetic_test.gno @@ -26,7 +26,7 @@ func TestAdd(t *testing.T) { got := new(Uint).Add(x, y) if got.Neq(want) { - t.Errorf("Add(%s, %s) = %v, want %v", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("Add(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } } } @@ -56,7 +56,7 @@ func TestAddOverflow(t *testing.T) { if got.Cmp(want) != 0 || overflow != tt.overflow { t.Errorf("AddOverflow(%s, %s) = (%s, %v), want (%s, %v)", - tt.x, tt.y, got.ToString(), overflow, tt.want, tt.overflow) + tt.x, tt.y, got.String(), overflow, tt.want, tt.overflow) } } } @@ -81,7 +81,7 @@ func TestSub(t *testing.T) { if got.Neq(want) { t.Errorf( "Sub(%s, %s) = %v, want %v", - tc.x, tc.y, got.ToString(), want.ToString(), + tc.x, tc.y, got.String(), want.String(), ) } } @@ -112,7 +112,7 @@ func TestSubOverflow(t *testing.T) { if got.Cmp(want) != 0 || overflow != tc.overflow { t.Errorf( "SubOverflow(%s, %s) = (%s, %v), want (%s, %v)", - tc.x, tc.y, got.ToString(), overflow, tc.want, tc.overflow, + tc.x, tc.y, got.String(), overflow, tc.want, tc.overflow, ) } } @@ -133,7 +133,7 @@ func TestMul(t *testing.T) { got := new(Uint).Mul(x, y) if got.Neq(want) { - t.Errorf("Mul(%s, %s) = %v, want %v", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("Mul(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } } } @@ -165,7 +165,7 @@ func TestMulOverflow(t *testing.T) { if gotZ.Neq(wantZ) { t.Errorf( "MulOverflow(%s, %s) = %s, want %s", - tt.x, tt.y, gotZ.ToString(), wantZ.ToString(), + tt.x, tt.y, gotZ.String(), wantZ.String(), ) } if gotOver != tt.wantOver { @@ -192,7 +192,7 @@ func TestDiv(t *testing.T) { got := new(Uint).Div(x, y) if got.Neq(want) { - t.Errorf("Div(%s, %s) = %v, want %v", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("Div(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } } } @@ -217,7 +217,7 @@ func TestMod(t *testing.T) { got := new(Uint).Mod(x, y) if got.Neq(want) { - t.Errorf("Mod(%s, %s) = %v, want %v", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("Mod(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } } } @@ -252,7 +252,7 @@ func TestMulMod(t *testing.T) { if got.Neq(want) { t.Errorf( "MulMod(%s, %s, %s) = %s, want %s", - tt.x, tt.y, tt.m, got.ToString(), want.ToString(), + tt.x, tt.y, tt.m, got.String(), want.String(), ) } } @@ -318,7 +318,7 @@ func TestNeg(t *testing.T) { got := new(Uint).Neg(x) if got.Neq(want) { - t.Errorf("Neg(%s) = %v, want %v", tt.x, got.ToString(), want.ToString()) + t.Errorf("Neg(%s) = %v, want %v", tt.x, got.String(), want.String()) } } } @@ -346,7 +346,7 @@ func TestExp(t *testing.T) { if got.Neq(want) { t.Errorf( "Exp(%s, %s) = %v, want %v", - tt.x, tt.y, got.ToString(), want.ToString(), + tt.x, tt.y, got.String(), want.String(), ) } } @@ -384,7 +384,7 @@ func TestExp_LargeExponent(t *testing.T) { if result.Neq(expected) { t.Errorf( "Test %s failed. Expected %s, got %s", - tt.name, expected.ToString(), result.ToString(), + tt.name, expected.String(), result.String(), ) } }) diff --git a/examples/gno.land/p/demo/uint256/bitwise_test.gno b/examples/gno.land/p/demo/uint256/bitwise_test.gno index 3561629fd94..45118af0b0f 100644 --- a/examples/gno.land/p/demo/uint256/bitwise_test.gno +++ b/examples/gno.land/p/demo/uint256/bitwise_test.gno @@ -43,7 +43,7 @@ func TestOr(t *testing.T) { if *res != tt.want { t.Errorf( "Or(%s, %s) = %s, want %s", - tt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(), + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), ) } }) @@ -102,7 +102,7 @@ func TestAnd(t *testing.T) { if *res != tt.want { t.Errorf( "And(%s, %s) = %s, want %s", - tt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(), + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), ) } }) @@ -138,7 +138,7 @@ func TestNot(t *testing.T) { if *res != tt.want { t.Errorf( "Not(%s) = %s, want %s", - tt.x.ToString(), res.ToString(), (tt.want).ToString(), + tt.x.String(), res.String(), (tt.want).String(), ) } }) @@ -197,7 +197,7 @@ func TestAndNot(t *testing.T) { if *res != tt.want { t.Errorf( "AndNot(%s, %s) = %s, want %s", - tt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(), + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), ) } }) @@ -256,7 +256,7 @@ func TestXor(t *testing.T) { if *res != tt.want { t.Errorf( "Xor(%s, %s) = %s, want %s", - tt.x.ToString(), tt.y.ToString(), res.ToString(), (tt.want).ToString(), + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), ) } }) @@ -311,7 +311,7 @@ func TestLsh(t *testing.T) { got := new(Uint).Lsh(x, tt.y) if got.Neq(want) { - t.Errorf("Lsh(%s, %d) = %s, want %s", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("Lsh(%s, %d) = %s, want %s", tt.x, tt.y, got.String(), want.String()) } } } @@ -357,7 +357,7 @@ func TestRsh(t *testing.T) { got := new(Uint).Rsh(x, tt.y) if got.Neq(want) { - t.Errorf("Rsh(%s, %d) = %s, want %s", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("Rsh(%s, %d) = %s, want %s", tt.x, tt.y, got.String(), want.String()) } } } @@ -417,7 +417,7 @@ func TestSRsh(t *testing.T) { got := new(Uint).SRsh(x, tt.y) if !got.Eq(want) { - t.Errorf("SRsh(%s, %d) = %s, want %s", tt.x, tt.y, got.ToString(), want.ToString()) + t.Errorf("SRsh(%s, %d) = %s, want %s", tt.x, tt.y, got.String(), want.String()) } } } diff --git a/examples/gno.land/p/demo/uint256/cmp_test.gno b/examples/gno.land/p/demo/uint256/cmp_test.gno index 51c9e70d9a7..05243290271 100644 --- a/examples/gno.land/p/demo/uint256/cmp_test.gno +++ b/examples/gno.land/p/demo/uint256/cmp_test.gno @@ -29,7 +29,7 @@ func TestSign(t *testing.T) { } for _, tt := range tests { - t.Run(tt.input.ToString(), func(t *testing.T) { + t.Run(tt.input.String(), func(t *testing.T) { result := tt.input.Sign() if result != tt.expected { t.Errorf("Sign() = %d; want %d", result, tt.expected) diff --git a/examples/gno.land/p/demo/uint256/conversion.gno b/examples/gno.land/p/demo/uint256/conversion.gno index 4ef90602ab3..c2f228f314c 100644 --- a/examples/gno.land/p/demo/uint256/conversion.gno +++ b/examples/gno.land/p/demo/uint256/conversion.gno @@ -130,7 +130,7 @@ func (z *Uint) scanScientificFromString(src string) error { // ToString returns the decimal string representation of z. It returns an empty string if z is nil. // OBS: doesn't exist from holiman's uint256 -func (z *Uint) ToString() string { +func (z *Uint) String() string { if z == nil { return "" } diff --git a/examples/gno.land/p/demo/uint256/conversion_test.gno b/examples/gno.land/p/demo/uint256/conversion_test.gno index 0ea20158be4..3942a102511 100644 --- a/examples/gno.land/p/demo/uint256/conversion_test.gno +++ b/examples/gno.land/p/demo/uint256/conversion_test.gno @@ -169,7 +169,7 @@ func TestSetBytes(t *testing.T) { z.SetBytes(test.input) expected := MustFromDecimal(test.expected) if z.Cmp(expected) != 0 { - t.Errorf("SetBytes(%x) = %s, expected %s", test.input, z.ToString(), test.expected) + t.Errorf("SetBytes(%x) = %s, expected %s", test.input, z.String(), test.expected) } } } diff --git a/examples/gno.land/p/demo/uint256/uint256_test.gno b/examples/gno.land/p/demo/uint256/uint256_test.gno index 0089af15c66..ae8129b6e27 100644 --- a/examples/gno.land/p/demo/uint256/uint256_test.gno +++ b/examples/gno.land/p/demo/uint256/uint256_test.gno @@ -7,8 +7,8 @@ import ( func TestSetAllOne(t *testing.T) { z := Zero() z.SetAllOne() - if z.ToString() != twoPow256Sub1 { - t.Errorf("Expected all ones, got %s", z.ToString()) + if z.String() != twoPow256Sub1 { + t.Errorf("Expected all ones, got %s", z.String()) } } @@ -120,8 +120,8 @@ func TestClone(t *testing.T) { for _, tt := range tests { z, _ := FromHex(tt.input) result := z.Clone() - if result.ToString() != tt.expected { - t.Errorf("Test %s failed. Expected %s, got %s", tt.input, tt.expected, result.ToString()) + if result.String() != tt.expected { + t.Errorf("Test %s failed. Expected %s, got %s", tt.input, tt.expected, result.String()) } } } From dc65f912bb7a3c10790e9f0911813baf7a24c0af Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Thu, 21 Nov 2024 01:03:14 -0800 Subject: [PATCH 244/345] feat(gnovm): sync code AssignStmt - ValueDecl (#3017) This PR aims at fixing this issue [1958](https://github.com/gnolang/gno/issues/1958)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: hieu.ha Co-authored-by: Mikael VALLENET --- gnovm/pkg/gnolang/preprocess.go | 407 +++++++++++++++++--------------- gnovm/tests/files/assign25c.gno | 15 ++ gnovm/tests/files/assign25d.gno | 15 ++ gnovm/tests/files/assign32.gno | 22 ++ gnovm/tests/files/assign33.gno | 13 + gnovm/tests/files/assign34.gno | 14 ++ gnovm/tests/files/assign35.gno | 14 ++ gnovm/tests/files/assign36.gno | 19 ++ gnovm/tests/files/var22c.gno | 15 ++ 9 files changed, 342 insertions(+), 192 deletions(-) create mode 100644 gnovm/tests/files/assign25c.gno create mode 100644 gnovm/tests/files/assign25d.gno create mode 100644 gnovm/tests/files/assign32.gno create mode 100644 gnovm/tests/files/assign33.gno create mode 100644 gnovm/tests/files/assign34.gno create mode 100644 gnovm/tests/files/assign35.gno create mode 100644 gnovm/tests/files/assign36.gno create mode 100644 gnovm/tests/files/var22c.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 1b85d83296d..b7c22e0b9f6 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -1975,60 +1975,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { convertIfConst(store, last, rx) } - if len(n.Lhs) > len(n.Rhs) { - switch cx := n.Rhs[0].(type) { - case *CallExpr: - // Call case: a, b := x(...) - ift := evalStaticTypeOf(store, last, cx.Func) - cft := getGnoFuncTypeOf(store, ift) - for i, lx := range n.Lhs { - ln := lx.(*NameExpr).Name - rf := cft.Results[i] - // re-definition - last.Define(ln, anyValue(rf.Type)) - } - case *TypeAssertExpr: - lhs0 := n.Lhs[0].(*NameExpr).Name - lhs1 := n.Lhs[1].(*NameExpr).Name - tt := evalStaticType(store, last, cx.Type) - // re-definitions - last.Define(lhs0, anyValue(tt)) - last.Define(lhs1, anyValue(BoolType)) - case *IndexExpr: - lhs0 := n.Lhs[0].(*NameExpr).Name - lhs1 := n.Lhs[1].(*NameExpr).Name - - var mt *MapType - dt := evalStaticTypeOf(store, last, cx.X) - mt, ok := baseOf(dt).(*MapType) - if !ok { - panic(fmt.Sprintf("invalid index expression on %T", dt)) - } - // re-definitions - last.Define(lhs0, anyValue(mt.Value)) - last.Define(lhs1, anyValue(BoolType)) - default: - panic("should not happen") - } - } else { - // General case: a, b := x, y - for i, lx := range n.Lhs { - ln := lx.(*NameExpr).Name - rx := n.Rhs[i] - rt := evalStaticTypeOf(store, last, rx) - // re-definition - if rt == nil { - // e.g. (interface{})(nil), becomes ConstExpr(undefined). - // last.Define(ln, undefined) complains, since redefinition. - } else { - last.Define(ln, anyValue(rt)) - } - // if rhs is untyped - if isUntyped(rt) { - checkOrConvertType(store, last, &n.Rhs[i], nil, false) - } - } + nameExprs := make(NameExprs, len(n.Lhs)) + for i := range len(n.Lhs) { + nameExprs[i] = *n.Lhs[i].(*NameExpr) } + + defineOrDecl(store, last, false, nameExprs, nil, n.Rhs) } else { // ASSIGN, or assignment operation (+=, -=, <<=, etc.) // NOTE: Keep in sync with DEFINE above. if len(n.Lhs) > len(n.Rhs) { @@ -2329,147 +2281,9 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // the implementation differ from // runDeclaration(), as this uses OpStaticTypeOf. } - numNames := len(n.NameExprs) - sts := make([]Type, numNames) // static types - tvs := make([]TypedValue, numNames) - - if numNames > 1 && len(n.Values) == 1 { - // Special cases if one of the following: - // - `var a, b, c T = f()` - // - `var a, b = n.(T)` - // - `var a, b = n[i], where n is a map` - - var tuple *tupleType - valueExpr := n.Values[0] - valueType := evalStaticTypeOfRaw(store, last, valueExpr) - - switch expr := valueExpr.(type) { - case *CallExpr: - tuple = valueType.(*tupleType) - case *TypeAssertExpr, *IndexExpr: - tuple = &tupleType{Elts: []Type{valueType, BoolType}} - if ex, ok := expr.(*TypeAssertExpr); ok { - ex.HasOK = true - break - } - expr.(*IndexExpr).HasOK = true - default: - panic(fmt.Sprintf("unexpected ValueDecl value expression type %T", expr)) - } - if rLen := len(tuple.Elts); rLen != numNames { - panic( - fmt.Sprintf( - "assignment mismatch: %d variable(s) but %s returns %d value(s)", - numNames, - valueExpr.String(), - rLen, - ), - ) - } + defineOrDecl(store, last, n.Const, n.NameExprs, n.Type, n.Values) - if n.Type != nil { - // only a single type can be specified. - nt := evalStaticType(store, last, n.Type) - // TODO check tt and nt compat. - for i := 0; i < numNames; i++ { - sts[i] = nt - tvs[i] = anyValue(nt) - } - } else { - // set types as return types. - for i := 0; i < numNames; i++ { - et := tuple.Elts[i] - sts[i] = et - tvs[i] = anyValue(et) - } - } - } else if len(n.Values) != 0 && numNames != len(n.Values) { - panic(fmt.Sprintf("assignment mismatch: %d variable(s) but %d value(s)", numNames, len(n.Values))) - } else { // general case - for _, v := range n.Values { - if cx, ok := v.(*CallExpr); ok { - tt, ok := evalStaticTypeOfRaw(store, last, cx).(*tupleType) - if ok && len(tt.Elts) != 1 { - panic(fmt.Sprintf("multiple-value %s (value of type %s) in single-value context", cx.Func.String(), tt.Elts)) - } - } - } - // evaluate types and convert consts. - if n.Type != nil { - // only a single type can be specified. - nt := evalStaticType(store, last, n.Type) - for i := 0; i < numNames; i++ { - sts[i] = nt - } - // convert if const to nt. - for i := range n.Values { - checkOrConvertType(store, last, &n.Values[i], nt, false) - } - } else if n.Const { - // derive static type from values. - for i, vx := range n.Values { - vt := evalStaticTypeOf(store, last, vx) - sts[i] = vt - } - } else { // T is nil, n not const - // convert n.Value to default type. - for i, vx := range n.Values { - if cx, ok := vx.(*ConstExpr); ok { - convertConst(store, last, cx, nil) - // convertIfConst(store, last, vx) - } else { - checkOrConvertType(store, last, &vx, nil, false) - } - vt := evalStaticTypeOf(store, last, vx) - sts[i] = vt - } - } - // evaluate typed value for static definition. - for i := range n.NameExprs { - // consider value if specified. - if len(n.Values) > 0 { - vx := n.Values[i] - if cx, ok := vx.(*ConstExpr); ok && - !cx.TypedValue.IsUndefined() { - if n.Const { - // const _ = : static block should contain value - tvs[i] = cx.TypedValue - } else { - // var _ = : static block should NOT contain value - tvs[i] = anyValue(cx.TypedValue.T) - } - continue - } - } - // for var decls of non-const expr. - st := sts[i] - tvs[i] = anyValue(st) - } - } - // define. - if fn, ok := last.(*FileNode); ok { - pn := fn.GetParentNode(nil).(*PackageNode) - for i := 0; i < numNames; i++ { - nx := &n.NameExprs[i] - if nx.Name == blankIdentifier { - nx.Path = NewValuePathBlock(0, 0, blankIdentifier) - } else { - pn.Define2(n.Const, nx.Name, sts[i], tvs[i]) - nx.Path = last.GetPathForName(nil, nx.Name) - } - } - } else { - for i := 0; i < numNames; i++ { - nx := &n.NameExprs[i] - if nx.Name == blankIdentifier { - nx.Path = NewValuePathBlock(0, 0, blankIdentifier) - } else { - last.Define2(n.Const, nx.Name, sts[i], tvs[i]) - nx.Path = last.GetPathForName(nil, nx.Name) - } - } - } // TODO make note of constance in static block for // future use, or consider "const paths". set as // preprocessed. @@ -2540,6 +2354,215 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { return nn } +// defineOrDecl merges the code logic from op define (:=) and declare (var/const). +func defineOrDecl( + store Store, + bn BlockNode, + isConst bool, + nameExprs []NameExpr, + typeExpr Expr, + valueExprs []Expr, +) { + numNames := len(nameExprs) + numVals := len(valueExprs) + + if numVals > 1 && numNames != numVals { + panic(fmt.Sprintf("assignment mismatch: %d variable(s) but %d value(s)", numNames, numVals)) + } + + sts := make([]Type, numNames) // static types + tvs := make([]TypedValue, numNames) + + if numNames > 1 && len(valueExprs) == 1 { + parseMultipleAssignFromOneExpr(sts, tvs, store, bn, nameExprs, typeExpr, valueExprs[0]) + } else { + parseAssignFromExprList(sts, tvs, store, bn, isConst, nameExprs, typeExpr, valueExprs) + } + + node := skipFile(bn) + + for i := 0; i < len(sts); i++ { + nx := nameExprs[i] + if nx.Name == blankIdentifier { + nx.Path = NewValuePathBlock(0, 0, blankIdentifier) + } else { + node.Define2(isConst, nx.Name, sts[i], tvs[i]) + nx.Path = bn.GetPathForName(nil, nx.Name) + } + } +} + +// parseAssignFromExprList parses assignment to multiple variables from a list of expressions. +// This function will alter the value of sts, tvs. +func parseAssignFromExprList( + sts []Type, + tvs []TypedValue, + store Store, + bn BlockNode, + isConst bool, + nameExprs []NameExpr, + typeExpr Expr, + valueExprs []Expr, +) { + numNames := len(nameExprs) + + // Ensure that function only return 1 value. + for _, v := range valueExprs { + if cx, ok := v.(*CallExpr); ok { + tt, ok := evalStaticTypeOfRaw(store, bn, cx).(*tupleType) + if ok && len(tt.Elts) != 1 { + panic(fmt.Sprintf("multiple-value %s (value of type %s) in single-value context", cx.Func.String(), tt.Elts)) + } + } + } + + // Evaluate types and convert consts. + if typeExpr != nil { + // Only a single type can be specified. + nt := evalStaticType(store, bn, typeExpr) + for i := 0; i < numNames; i++ { + sts[i] = nt + } + // Convert if const to nt. + for i := range valueExprs { + checkOrConvertType(store, bn, &valueExprs[i], nt, false) + } + } else if isConst { + // Derive static type from values. + for i, vx := range valueExprs { + vt := evalStaticTypeOf(store, bn, vx) + sts[i] = vt + } + } else { // T is nil, n not const => same as AssignStmt DEFINE + // Convert n.Value to default type. + for i, vx := range valueExprs { + if cx, ok := vx.(*ConstExpr); ok { + convertConst(store, bn, cx, nil) + // convertIfConst(store, last, vx) + } else { + checkOrConvertType(store, bn, &vx, nil, false) + } + vt := evalStaticTypeOf(store, bn, vx) + sts[i] = vt + } + } + + // Evaluate typed value for static definition. + for i := range nameExprs { + // Consider value if specified. + if len(valueExprs) > 0 { + vx := valueExprs[i] + if cx, ok := vx.(*ConstExpr); ok && + !cx.TypedValue.IsUndefined() { + if isConst { + // const _ = : static block should contain value + tvs[i] = cx.TypedValue + } else { + // var _ = : static block should NOT contain value + tvs[i] = anyValue(cx.TypedValue.T) + } + continue + } + } + // For var decls of non-const expr. + st := sts[i] + tvs[i] = anyValue(st) + } +} + +// parseMultipleAssignFromOneExpr parses assignment to multiple variables from a single expression. +// This function will alter the value of sts, tvs. +// Declare: +// - var a, b, c T = f() +// - var a, b = n.(T) +// - var a, b = n[i], where n is a map +// Assign: +// - a, b, c := f() +// - a, b := n.(T) +// - a, b := n[i], where n is a map +func parseMultipleAssignFromOneExpr( + sts []Type, + tvs []TypedValue, + store Store, + bn BlockNode, + nameExprs []NameExpr, + typeExpr Expr, + valueExpr Expr, +) { + var tuple *tupleType + numNames := len(nameExprs) + switch expr := valueExpr.(type) { + case *CallExpr: + // Call case: + // var a, b, c T = f() + // a, b, c := f() + valueType := evalStaticTypeOfRaw(store, bn, valueExpr) + tuple = valueType.(*tupleType) + case *TypeAssertExpr: + // Type assert case: + // var a, b = n.(T) + // a, b := n.(T) + tt := evalStaticType(store, bn, expr.Type) + tuple = &tupleType{Elts: []Type{tt, BoolType}} + expr.HasOK = true + case *IndexExpr: + // Map index case: + // var a, b = n[i], where n is a map + // a, b := n[i], where n is a map + var mt *MapType + dt := evalStaticTypeOf(store, bn, expr.X) + mt, ok := baseOf(dt).(*MapType) + if !ok { + panic(fmt.Sprintf("invalid index expression on %T", dt)) + } + tuple = &tupleType{Elts: []Type{mt.Value, BoolType}} + expr.HasOK = true + default: + panic(fmt.Sprintf("unexpected value expression type %T", expr)) + } + + if numValues := len(tuple.Elts); numValues != numNames { + panic( + fmt.Sprintf( + "assignment mismatch: %d variable(s) but %s returns %d value(s)", + numNames, + valueExpr.String(), + numValues, + ), + ) + } + + var st Type = nil + if typeExpr != nil { + // Only a single type can be specified. + st = evalStaticType(store, bn, typeExpr) + } + + for i := 0; i < numNames; i++ { + if st != nil { + tt := tuple.Elts[i] + + if checkAssignableTo(tt, st, false) != nil { + panic( + fmt.Sprintf( + "cannot use %v (value of type %s) as %s value in assignment", + valueExpr.String(), + tt.String(), + st.String(), + ), + ) + } + + sts[i] = st + } else { + // Set types as return types. + sts[i] = tuple.Elts[i] + } + + tvs[i] = anyValue(sts[i]) + } +} + // Identifies NameExprTypeHeapDefines. // Also finds GotoLoopStmts, XXX but probably remove, not needed. func findGotoLoopDefines(ctx BlockNode, bn BlockNode) { diff --git a/gnovm/tests/files/assign25c.gno b/gnovm/tests/files/assign25c.gno new file mode 100644 index 00000000000..6fe9415787b --- /dev/null +++ b/gnovm/tests/files/assign25c.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func f() (a, b int) { + return 1, 2 +} + +func main() { + x, y, z := 1, f() + fmt.Println(x, y, z) +} + +// Error: +// main/files/assign25c.gno:10:2: assignment mismatch: 3 variable(s) but 2 value(s) diff --git a/gnovm/tests/files/assign25d.gno b/gnovm/tests/files/assign25d.gno new file mode 100644 index 00000000000..793369c3d78 --- /dev/null +++ b/gnovm/tests/files/assign25d.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func f() (a, b int) { + return 1, 2 +} + +func main() { + x, y, z := f(), 1, 2, 3 + fmt.Println(x, y, z) +} + +// Error: +// main/files/assign25d.gno:10:2: assignment mismatch: 3 variable(s) but 4 value(s) diff --git a/gnovm/tests/files/assign32.gno b/gnovm/tests/files/assign32.gno new file mode 100644 index 00000000000..094b08cc713 --- /dev/null +++ b/gnovm/tests/files/assign32.gno @@ -0,0 +1,22 @@ +package main + +func foo() int { + return 2 +} + +func main() { + var mp map[string]int = map[string]int{"idx": 4} + var sl []int = []int{4, 5, 6} + arr := [1]int{7} + var num interface{} = 5 + + a, b, c, d, e, f, g := int(1), foo(), 3, mp["idx"], num.(int), sl[2], arr[0] + println(a, b, c, d, e, f, g) + + var h, i, j, k, l, m, n int = int(1), foo(), 3, mp["idx"], num.(int), sl[2], arr[0] + println(h, i, j, k, l, m, n) +} + +// Output: +// 1 2 3 4 5 6 7 +// 1 2 3 4 5 6 7 diff --git a/gnovm/tests/files/assign33.gno b/gnovm/tests/files/assign33.gno new file mode 100644 index 00000000000..29b6be8d1f8 --- /dev/null +++ b/gnovm/tests/files/assign33.gno @@ -0,0 +1,13 @@ +package main + +func foo() (int, bool) { + return 1, true +} + +func main() { + var a, b int = foo() + println(a, b) +} + +// Error: +// main/files/assign33.gno:8:6: cannot use foo() (value of type bool) as int value in assignment diff --git a/gnovm/tests/files/assign34.gno b/gnovm/tests/files/assign34.gno new file mode 100644 index 00000000000..a289c602028 --- /dev/null +++ b/gnovm/tests/files/assign34.gno @@ -0,0 +1,14 @@ +package main + +func foo() (int, bool) { + return 1, true +} + +func main() { + var a, b = foo() + println(a, b) +} + +// Output: +// 1 true + diff --git a/gnovm/tests/files/assign35.gno b/gnovm/tests/files/assign35.gno new file mode 100644 index 00000000000..53f7eb367f5 --- /dev/null +++ b/gnovm/tests/files/assign35.gno @@ -0,0 +1,14 @@ +package main + +func foo() (int, bool) { + return 1, true +} + +func main() { + a, b := 2, foo() + + println(a, b) +} + +// Error: +// main/files/assign35.gno:8:2: multiple-value foo (value of type [int bool]) in single-value context diff --git a/gnovm/tests/files/assign36.gno b/gnovm/tests/files/assign36.gno new file mode 100644 index 00000000000..963a4480b8d --- /dev/null +++ b/gnovm/tests/files/assign36.gno @@ -0,0 +1,19 @@ +package main + +import "fmt" + +func f() (int) { + return 2 +} + +func main() { + var a, b, c = 1, f(), 3 + fmt.Println(a, b, c) + + x, y, z := 1, f(), 3 + fmt.Println(x, y, z) +} + +// Output: +// 1 2 3 +// 1 2 3 diff --git a/gnovm/tests/files/var22c.gno b/gnovm/tests/files/var22c.gno new file mode 100644 index 00000000000..0723dfa279e --- /dev/null +++ b/gnovm/tests/files/var22c.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func f() (a, b int) { + return 1, 2 +} + +func main() { + var x, y, z = 1, f() + fmt.Println(x, y, z) +} + +// Error: +// main/files/var22c.gno:10:6: missing init expr for z \ No newline at end of file From 549da06d0203d6da4ef60164a877b9eefb579f3f Mon Sep 17 00:00:00 2001 From: Sergio Maria Matone Date: Thu, 21 Nov 2024 15:38:57 +0100 Subject: [PATCH 245/345] chore(otel): Open Telemetry metrics fixed and provided with demo example (#3038) * Redefine and cleanup Open Telemetry metrics types and usage * Rehauled demo example adding a minimal sample of RCP + Validator Node --- gno.land/pkg/sdk/vm/handler.go | 30 -- misc/telemetry/README.md | 35 ++- misc/telemetry/docker-compose.yml | 91 ++++-- misc/telemetry/gnoland/Dockerfile | 13 - misc/telemetry/gnoland/setup.sh | 19 -- .../dashboards}/dashboards.yaml | 2 +- .../dashboards/gno-otel-dashboards.json} | 275 +++++++++--------- .../datasources}/datasources.yaml | 0 misc/telemetry/supernova.Dockerfile | 12 - tm2/pkg/telemetry/config/config.go | 4 +- tm2/pkg/telemetry/metrics/metrics.go | 62 ++-- 11 files changed, 262 insertions(+), 281 deletions(-) delete mode 100644 misc/telemetry/gnoland/Dockerfile delete mode 100644 misc/telemetry/gnoland/setup.sh rename misc/telemetry/grafana/{ => provisioning/dashboards}/dashboards.yaml (66%) rename misc/telemetry/grafana/{gno-dashboards.json => provisioning/dashboards/gno-otel-dashboards.json} (81%) rename misc/telemetry/grafana/{ => provisioning/datasources}/datasources.yaml (100%) delete mode 100644 misc/telemetry/supernova.Dockerfile diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index 7b26265f35d..c484e07e887 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -1,17 +1,12 @@ package vm import ( - "context" "fmt" "strings" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/telemetry" - "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" ) type vmHandler struct { @@ -107,34 +102,9 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQ secondPart(req.Path), req.Path))) } - // Log the telemetry - logQueryTelemetry(path, res.IsErr()) - return res } -// logQueryTelemetry logs the relevant VM query telemetry -func logQueryTelemetry(path string, isErr bool) { - if !telemetry.MetricsEnabled() { - return - } - - metrics.VMQueryCalls.Add( - context.Background(), - 1, - metric.WithAttributes( - attribute.KeyValue{ - Key: "path", - Value: attribute.StringValue(path), - }, - ), - ) - - if isErr { - metrics.VMQueryErrors.Add(context.Background(), 1) - } -} - // queryPackage fetch a package's files. func (vh vmHandler) queryPackage(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { res.Data = []byte(fmt.Sprintf("TODO: parse parts get or make fileset...")) diff --git a/misc/telemetry/README.md b/misc/telemetry/README.md index 41628cc5f51..e762bd1d630 100644 --- a/misc/telemetry/README.md +++ b/misc/telemetry/README.md @@ -1,4 +1,4 @@ -## Overview +# Open Telemetry overview The purpose of this Telemetry documentation is to showcase the different node metrics exposed by the Gno node through OpenTelemetry, without having to do extraneous setup. @@ -8,9 +8,21 @@ The containerized setup is the following: - Grafana dashboard - Prometheus - OpenTelemetry collector (separate service that needs to run) -- Single Gnoland node, with 1s block times and configured telemetry (enabled) +- 1 RPC Gnoland node, with 1s block times and configured telemetry (enabled) +- 1 Validator Gnoland node, with 1s block times and configured telemetry (enabled) - Supernova process that simulates load periodically (generates network traffic) +## Metrics type + +Metrics collected are defined within codebase at `tm2/pkg/telemetry/metrics/metrics.go`. +They are collected by the OTEL collector who forwards them to Prometheus. + +They are of three different types which can be used in Grafana adding different ypt of suffixes to the metrics name : + +- Histogram ("_sum", "_count", "_bucket"): Collect variations of values along time +- Gauge: Measure a single value at the time it is read +- Counter ("_total"): A value that accumulates over time + ## Starting the containers ### Step 1: Spinning up Docker @@ -18,7 +30,7 @@ The containerized setup is the following: Make sure you have Docker installed and running on your system. After that, within the `misc/telemetry` folder run the following command: -```shell +```bash make up ``` @@ -26,21 +38,14 @@ This will build out the required Docker images for this simulation, and start th ### Step 2: Open Grafana -When you've verified that the `telemetry` containers are up and running, head on over to http://localhost:3000 to open +When you've verified that the `telemetry` containers are up and running, head on over to to open the Grafana dashboard. -Default login details: - -``` -username: admin -password: admin -``` - -After you've logged in (you can skip setting a new password), on the left hand side, click on -`Dashboards -> Gno -> Gno Node Metrics`: +After you've logged in, on the left hand side, click on +`Dashboards -> Gno -> Gno Open Telemetry Metrics`: ![Grafana](assets/grafana-1.jpeg) -This will open up the predefined Gno Metrics dashboards (added for ease of use) : +This will open up the predefined Gno Metrics dashboards (added for ease of use): ![Metrics Dashboard](assets/grafana-2.jpeg) Periodically, these metrics will be updated as the `supernova` process is simulating network traffic. @@ -53,4 +58,4 @@ To stop the cluster, you can run: make down ``` -which will stop the Docker containers. Additionally, you can delete the Docker volumes with `make clean`. \ No newline at end of file +which will stop the Docker containers. Additionally, you can delete the Docker volumes with `make clean`. diff --git a/misc/telemetry/docker-compose.yml b/misc/telemetry/docker-compose.yml index 91c2ea3471d..89c7a924e08 100644 --- a/misc/telemetry/docker-compose.yml +++ b/misc/telemetry/docker-compose.yml @@ -9,6 +9,7 @@ services: - ./collector/collector.yaml:/etc/otelcol-contrib/config.yaml networks: - gnoland-net + prometheus: image: prom/prometheus:latest command: @@ -21,34 +22,90 @@ services: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml networks: - gnoland-net + grafana: - image: grafana/grafana-enterprise + image: grafana/grafana + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin volumes: - grafana_data:/var/lib/grafana - - ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml - - ./grafana/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml - - ./grafana/gno-dashboards.json:/var/lib/grafana/dashboards/gno-dashboards.json + - ./grafana/provisioning:/etc/grafana/provisioning ports: - "3000:3000" networks: - gnoland-net - gnoland: - build: - context: ./gnoland - dockerfile: Dockerfile - ports: - - "26657:26657" + + gnoland-val: + image: ghcr.io/gnolang/gno/gnoland:master networks: - gnoland-net + volumes: + # Shared Volume + - gnoland-shared:/gnoroot/shared-data + entrypoint: + - sh + - -c + # Recreate gno genesis from git :( + - | + gnoland secrets init + rm -f /gnoroot/shared-data/node_p2p.id + apk add git make go linux-headers + git clone https://github.com/gnolang/gno.git --single-branch gnoland-src + GOPATH='/usr/' make -C gnoland-src/contribs/gnogenesis/ + gnogenesis generate + gnogenesis validator add -name val000 -address $(gnoland secrets get validator_key.address -raw) -pub-key $(gnoland secrets get validator_key.pub_key -raw) + gnogenesis balances add -balance-sheet /gnoroot/gno.land/genesis/genesis_balances.txt + gnogenesis txs add packages /gnoroot/examples/gno.land + gnoland config init + gnoland config set consensus.timeout_commit 1s + gnoland config set moniker val000 + gnoland config set telemetry.enabled true + gnoland config set telemetry.exporter_endpoint collector:4317 + gnoland config set telemetry.service_instance_id val0 + gnoland secrets get node_id.id -raw > /gnoroot/shared-data/node_p2p.id + cp /gnoroot/genesis.json /gnoroot/shared-data/genesis.json + gnoland start + healthcheck: + test: ["CMD-SHELL", "test -f /gnoroot/shared-data/node_p2p.id || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 60s + + gnoland-rpc: + image: ghcr.io/gnolang/gno/gnoland:master + networks: + - gnoland-net + volumes: + # Shared Volume + - gnoland-shared:/gnoroot/shared-data + entrypoint: + - sh + - -c + - | + gnoland secrets init + gnoland config init + gnoland config set consensus.timeout_commit 1s + gnoland config set moniker rpc0 + gnoland config set rpc.laddr tcp://0.0.0.0:26657 + gnoland config set telemetry.enabled true + gnoland config set telemetry.service_instance_id rpc000 + gnoland config set telemetry.exporter_endpoint collector:4317 + gnoland config set p2p.persistent_peers "$(cat /gnoroot/shared-data/node_p2p.id)@gnoland-val:26656" + gnoland start -genesis /gnoroot/shared-data/genesis.json + depends_on: + gnoland-val: + condition: service_healthy + restart: true + supernova: - build: - dockerfile: supernova.Dockerfile - args: - supernova_version: v1.2.1 + image: ghcr.io/gnolang/supernova:1.3.1 command: > - -sub-accounts 10 -transactions 200 -url http://gnoland:26657 + -sub-accounts 10 -transactions 100 -url http://gnoland-rpc:26657 -mnemonic "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" - restart: always + -mode PACKAGE_DEPLOYMENT + restart: unless-stopped networks: - gnoland-net @@ -61,5 +118,5 @@ volumes: driver: local grafana_data: driver: local - gnoland: + gnoland-shared: driver: local diff --git a/misc/telemetry/gnoland/Dockerfile b/misc/telemetry/gnoland/Dockerfile deleted file mode 100644 index c8a89e1a634..00000000000 --- a/misc/telemetry/gnoland/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# Use the existing gno image as the base image -FROM ghcr.io/gnolang/gno/gnoland:master AS base - -# Copy the setup script into the container -COPY ./setup.sh . - -# Make the script executable -RUN chmod +x ./setup.sh - -# Run the setup -ENTRYPOINT ["sh"] - -CMD ["./setup.sh"] \ No newline at end of file diff --git a/misc/telemetry/gnoland/setup.sh b/misc/telemetry/gnoland/setup.sh deleted file mode 100644 index 12cc418ac67..00000000000 --- a/misc/telemetry/gnoland/setup.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Initialize the node config -gnoland config init --config-path /gnoroot/gnoland-data/config/config.toml - -# Set the block time to 1s -gnoland config set --config-path /gnoroot/gnoland-data/config/config.toml consensus.timeout_commit 1s - -# Set the listen address -gnoland config set --config-path /gnoroot/gnoland-data/config/config.toml rpc.laddr tcp://0.0.0.0:26657 - -# Enable the metrics -gnoland config set --config-path /gnoroot/gnoland-data/config/config.toml telemetry.enabled true - -# Set the metrics exporter endpoint -gnoland config set --config-path /gnoroot/gnoland-data/config/config.toml telemetry.exporter_endpoint collector:4317 - -# Start the Gnoland node (lazy will init the genesis.json and secrets) -gnoland start --lazy \ No newline at end of file diff --git a/misc/telemetry/grafana/dashboards.yaml b/misc/telemetry/grafana/provisioning/dashboards/dashboards.yaml similarity index 66% rename from misc/telemetry/grafana/dashboards.yaml rename to misc/telemetry/grafana/provisioning/dashboards/dashboards.yaml index 6a70278b8a1..694ea8c2803 100644 --- a/misc/telemetry/grafana/dashboards.yaml +++ b/misc/telemetry/grafana/provisioning/dashboards/dashboards.yaml @@ -5,4 +5,4 @@ providers: folder: Gno type: file options: - path: /var/lib/grafana/dashboards \ No newline at end of file + path: /etc/grafana/provisioning/dashboards diff --git a/misc/telemetry/grafana/gno-dashboards.json b/misc/telemetry/grafana/provisioning/dashboards/gno-otel-dashboards.json similarity index 81% rename from misc/telemetry/grafana/gno-dashboards.json rename to misc/telemetry/grafana/provisioning/dashboards/gno-otel-dashboards.json index 58373bfb1ee..55880699b7f 100644 --- a/misc/telemetry/grafana/gno-dashboards.json +++ b/misc/telemetry/grafana/provisioning/dashboards/gno-otel-dashboards.json @@ -19,7 +19,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 2, "links": [], "panels": [ { @@ -37,13 +36,14 @@ }, { "datasource": { + "default": true, "type": "prometheus", "uid": "prometheus" }, "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "mappings": [], "thresholds": { @@ -64,24 +64,25 @@ "x": 0, "y": 1 }, - "id": 24, + "id": 26, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ - "lastNotNull" + "mean" ], "fields": "", "values": false }, - "showPercentChange": false, + "showPercentChange": true, "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -89,14 +90,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "vm_query_errors_counter", + "expr": "rate(vm_gas_used_hist_sum{exported_instance=~\"${node}\"}[$__rate_interval])/rate(vm_gas_used_hist_count{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, - "legendFormat": "__auto", + "legendFormat": "{{operation}}", "range": true, "refId": "A" } ], - "title": "Total Number of VM Query Errors", + "title": "Average Gas Used by VM execution - Node: ${node}", "type": "stat" }, { @@ -107,7 +108,7 @@ "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "mappings": [], "thresholds": { @@ -128,76 +129,13 @@ "x": 12, "y": 1 }, - "id": 25, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "10.4.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "vm_query_calls_counter", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Total Number of VM Query Calls", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 9 - }, - "id": 26, + "id": 27, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -205,11 +143,11 @@ "fields": "", "values": false }, - "showPercentChange": false, + "showPercentChange": true, "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -217,25 +155,26 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(vm_gas_used_hist_sum[10m])/rate(vm_gas_used_hist_count[10m])", + "expr": "rate(vm_cpu_cycles_hist_sum{exported_instance=~\"${node}\"}[$__rate_interval])/rate(vm_cpu_cycles_hist_count{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, - "legendFormat": "__auto", + "legendFormat": "{{operation}}", "range": true, "refId": "A" } ], - "title": "Average Gas Used by VM execution [10min]", + "title": "Average CPU Cycles in VM execution - Node: ${node}", "type": "stat" }, { "datasource": { + "default": true, "type": "prometheus", "uid": "prometheus" }, "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "mappings": [], "thresholds": { @@ -253,27 +192,28 @@ "gridPos": { "h": 8, "w": 12, - "x": 12, + "x": 6, "y": 9 }, - "id": 27, + "id": 24, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ - "mean" + "lastNotNull" ], "fields": "", "values": false }, - "showPercentChange": false, + "showPercentChange": true, "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -281,14 +221,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(vm_cpu_cycles_hist_sum[10m])/rate(vm_cpu_cycles_hist_count[10m])", + "expr": "vm_exec_msg_counter_total{exported_instance=~\"${node}\"}", "instant": false, - "legendFormat": "__auto", + "legendFormat": "{{operation}}", "range": true, "refId": "A" } ], - "title": "Average CPU Cycles in VM execution [10min]", + "title": "Total Number of VM Exec Msg - Node: ${node}", "type": "stat" }, { @@ -337,6 +277,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -348,7 +289,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -356,7 +297,7 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(num_mempool_txs_hist_sum[10m])/rate(num_mempool_txs_count[10m])", + "expr": "sum(rate(num_mempool_txs_hist_sum[$__rate_interval]))/sum(rate(num_mempool_txs_hist_count[$__rate_interval]))", "instant": false, "legendFormat": "__auto", "range": true, @@ -401,6 +342,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -412,7 +354,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -420,7 +362,7 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(num_cached_txs_hist_sum[10m])/rate(num_cached_txs_count[10m])", + "expr": "sum(rate(num_cached_txs_hist_sum[$__rate_interval]))/sum(rate(num_cached_txs_hist_count[$__rate_interval]))", "instant": false, "legendFormat": "__auto", "range": true, @@ -476,9 +418,10 @@ "id": 12, "options": { "colorMode": "value", - "graphMode": "area", + "graphMode": "none", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -490,7 +433,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -498,14 +441,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(inbound_peers_hist_sum[10m])/rate(inbound_peers_hist_count[10m])", + "expr": "avg_over_time(inbound_peers_gauge{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Average Inbound Peer Count", + "title": "Average Inbound Peer Count - Node: ${node}", "type": "stat" }, { @@ -542,9 +485,10 @@ "id": 13, "options": { "colorMode": "value", - "graphMode": "area", + "graphMode": "none", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -556,7 +500,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -564,14 +508,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(outbound_peers_hist_sum[10m])/rate(outbound_peers_hist_count[10m])", + "expr": "avg_over_time(outbound_peers_gauge{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Average Outbound Peer Count", + "title": "Average Outbound Peer Count - Node: ${node}", "type": "stat" }, { @@ -608,9 +552,10 @@ "id": 14, "options": { "colorMode": "value", - "graphMode": "area", + "graphMode": "none", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -622,7 +567,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -630,14 +575,14 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(dialing_peers_hist_sum[10m])/rate(dialing_peers_hist_count[10m])", + "expr": "avg_over_time(dialing_peers_gauge{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Average Dialing Peer Count", + "title": "Average Dialing Peer Count - Node: ${node}", "type": "stat" }, { @@ -687,6 +632,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -698,7 +644,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -706,7 +652,7 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(validator_count_hist_sum[10m])/rate(validator_count_hist_count[10m])", + "expr": "rate(validator_count_hist_sum{exported_instance=~\"${node}\"}[$__rate_interval])/rate(validator_count_hist_count{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, "legendFormat": "__auto", "range": true, @@ -752,6 +698,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -763,7 +710,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -771,7 +718,7 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "rate(validator_vp_hist_sum[10m])/rate(validator_vp_hist_count[10m])", + "expr": "rate(validator_vp_hist_sum{exported_instance=~\"${node}\"}[$__rate_interval])/rate(validator_vp_hist_count{exported_instance=~\"${node}\"}[$__rate_interval])", "instant": false, "legendFormat": "__auto", "range": true, @@ -825,6 +772,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -836,7 +784,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -845,7 +793,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "rate(build_block_hist_milliseconds_sum[10m])/rate(build_block_hist_milliseconds_count[10m])", + "expr": "rate(build_block_hist_milliseconds_sum[$__rate_interval])/rate(build_block_hist_milliseconds_count[$__rate_interval])", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, @@ -855,7 +803,7 @@ "useBackend": false } ], - "title": "Average Block Build Time [10min]", + "title": "Average Block Build Time", "type": "stat" }, { @@ -897,6 +845,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -908,7 +857,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -917,7 +866,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "rate(block_interval_hist_seconds_sum[10m])/rate(block_interval_hist_seconds_count[10m])", + "expr": "sum(rate(block_interval_hist_seconds_sum[$__rate_interval]))/sum(rate(block_interval_hist_seconds_count[$__rate_interval]))", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, @@ -927,7 +876,7 @@ "useBackend": false } ], - "title": "Average Block Interval [10min]", + "title": "Average Block Interval", "type": "stat" }, { @@ -991,7 +940,7 @@ "reverse": false } }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -1010,7 +959,7 @@ "useBackend": false } ], - "title": "Average Block Tx Count [10min]", + "title": "Average Block Tx Count", "type": "heatmap" }, { @@ -1049,6 +998,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -1060,7 +1010,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -1069,7 +1019,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "rate(block_size_hist_B_sum[10m])/rate(block_size_hist_B_count[10m])", + "expr": "sum(rate(block_size_hist_B_sum[$__rate_interval]))/sum(rate(block_size_hist_B_count[$__rate_interval]))", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, @@ -1079,7 +1029,7 @@ "useBackend": false } ], - "title": "Average Block Size [10min]", + "title": "Average Block Size", "type": "stat" }, { @@ -1136,6 +1086,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -1147,7 +1098,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -1156,7 +1107,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "rate(broadcast_tx_hist_milliseconds_sum[10m])/rate(broadcast_tx_hist_milliseconds_count[10m])", + "expr": "sum(rate(broadcast_tx_hist_milliseconds_sum[$__rate_interval]))/sum(rate(broadcast_tx_hist_milliseconds_count[$__rate_interval]))", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, @@ -1166,7 +1117,7 @@ "useBackend": false } ], - "title": "Average Transaction Broadcast Duration [10min]", + "title": "Average Transaction Broadcast Duration", "type": "stat" }, { @@ -1227,6 +1178,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -1238,7 +1190,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -1247,7 +1199,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "rate(http_request_time_hist_milliseconds_sum[10m])/rate(http_request_time_hist_milliseconds_count[10m])", + "expr": "rate(http_request_time_hist_milliseconds_sum[$__rate_interval])/rate(http_request_time_hist_milliseconds_count[$__rate_interval])", "fullMetaSearch": false, "hide": false, "includeNullMetadata": true, @@ -1258,7 +1210,7 @@ "useBackend": false } ], - "title": "Average HTTP Request Round Trip Time [10min]", + "title": "Average HTTP Request Round Trip Time", "type": "stat" }, { @@ -1306,6 +1258,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "mean" @@ -1317,7 +1270,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.2", + "pluginVersion": "11.2.0", "targets": [ { "datasource": { @@ -1326,7 +1279,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "rate(ws_request_time_hist_milliseconds_sum[10m])/rate(ws_request_time_hist_milliseconds_count[10m])", + "expr": "rate(ws_request_time_hist_milliseconds_sum[$__rate_interval])/rate(ws_request_time_hist_milliseconds_count[$__rate_interval])", "fullMetaSearch": false, "hide": false, "includeNullMetadata": true, @@ -1337,7 +1290,7 @@ "useBackend": false } ], - "title": "Average WS Request Round Trip Time [10min]", + "title": "Average WS Request Round Trip Time", "type": "stat" } ], @@ -1345,16 +1298,72 @@ "schemaVersion": 39, "tags": [], "templating": { - "list": [] + "list": [ + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(exported_instance)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "node", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(exported_instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "m_addpkg", + "value": "m_addpkg" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(vm_gas_used_hist_sum,operation)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "operation", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(vm_gas_used_hist_sum,operation)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] }, "time": { - "from": "now-6h", + "from": "now-8h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gno Node Metrics", + "title": "Gno Open Telemetry Metrics", "uid": "bdl7d5yogxjb4b", "version": 1, "weekStart": "" -} \ No newline at end of file +} diff --git a/misc/telemetry/grafana/datasources.yaml b/misc/telemetry/grafana/provisioning/datasources/datasources.yaml similarity index 100% rename from misc/telemetry/grafana/datasources.yaml rename to misc/telemetry/grafana/provisioning/datasources/datasources.yaml diff --git a/misc/telemetry/supernova.Dockerfile b/misc/telemetry/supernova.Dockerfile deleted file mode 100644 index 67ccbda8047..00000000000 --- a/misc/telemetry/supernova.Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.22-alpine - -ARG supernova_version=latest - -RUN go install github.com/gnolang/supernova/cmd@$supernova_version && mv /go/bin/cmd /go/bin/supernova -RUN export SUPERNOVA_PATH=$(go list -m -f "{{.Dir}}" github.com/gnolang/supernova@${supernova_version}) && \ - mkdir -p /supernova && \ - cp -r $SUPERNOVA_PATH/* /supernova - -WORKDIR /supernova - -ENTRYPOINT ["supernova"] diff --git a/tm2/pkg/telemetry/config/config.go b/tm2/pkg/telemetry/config/config.go index 47fc5666342..d11eba15016 100644 --- a/tm2/pkg/telemetry/config/config.go +++ b/tm2/pkg/telemetry/config/config.go @@ -10,8 +10,8 @@ var errEndpointNotSet = errors.New("telemetry exporter endpoint not set") type Config struct { MetricsEnabled bool `json:"enabled" toml:"enabled"` MeterName string `json:"meter_name" toml:"meter_name"` - ServiceName string `json:"service_name" toml:"service_name"` - ServiceInstanceID string `json:"service_instance_id" toml:"service_instance_id" comment:"the ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service)"` + ServiceName string `json:"service_name" toml:"service_name" comment:"in Prometheus this is transformed into the label 'exported_job'"` + ServiceInstanceID string `json:"service_instance_id" toml:"service_instance_id" comment:"the ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service), in Prometheus this is transformed into the label 'exported_instance"` ExporterEndpoint string `json:"exporter_endpoint" toml:"exporter_endpoint" comment:"the endpoint to export metrics to, like a local OpenTelemetry collector"` } diff --git a/tm2/pkg/telemetry/metrics/metrics.go b/tm2/pkg/telemetry/metrics/metrics.go index 2b04769fe0c..7a3e182e06d 100644 --- a/tm2/pkg/telemetry/metrics/metrics.go +++ b/tm2/pkg/telemetry/metrics/metrics.go @@ -19,18 +19,16 @@ const ( broadcastTxTimerKey = "broadcast_tx_hist" buildBlockTimerKey = "build_block_hist" - inboundPeersKey = "inbound_peers_hist" - outboundPeersKey = "outbound_peers_hist" - dialingPeersKey = "dialing_peers_hist" + inboundPeersKey = "inbound_peers_gauge" + outboundPeersKey = "outbound_peers_gauge" + dialingPeersKey = "dialing_peers_gauge" numMempoolTxsKey = "num_mempool_txs_hist" numCachedTxsKey = "num_cached_txs_hist" - vmQueryCallsKey = "vm_query_calls_counter" - vmQueryErrorsKey = "vm_query_errors_counter" - vmGasUsedKey = "vm_gas_used_hist" - vmCPUCyclesKey = "vm_cpu_cycles_hist" - vmExecMsgKey = "vm_exec_msg_hist" + vmExecMsgKey = "vm_exec_msg_counter" + vmGasUsedKey = "vm_gas_used_hist" + vmCPUCyclesKey = "vm_cpu_cycles_hist" validatorCountKey = "validator_count_hist" validatorVotingPowerKey = "validator_vp_hist" @@ -51,13 +49,13 @@ var ( // Networking // // InboundPeers measures the active number of inbound peers - InboundPeers metric.Int64Histogram + InboundPeers metric.Int64Gauge // OutboundPeers measures the active number of outbound peers - OutboundPeers metric.Int64Histogram + OutboundPeers metric.Int64Gauge // DialingPeers measures the active number of peers in the dialing state - DialingPeers metric.Int64Histogram + DialingPeers metric.Int64Gauge // Mempool // @@ -69,11 +67,8 @@ var ( // Runtime // - // VMQueryCalls measures the frequency of VM query calls - VMQueryCalls metric.Int64Counter - - // VMQueryErrors measures the frequency of VM query errors - VMQueryErrors metric.Int64Counter + // VMExecMsgFrequency measures the frequency of VM operations + VMExecMsgFrequency metric.Int64Counter // VMGasUsed measures the VM gas usage VMGasUsed metric.Int64Histogram @@ -81,9 +76,6 @@ var ( // VMCPUCycles measures the VM CPU cycles VMCPUCycles metric.Int64Histogram - // VMExecMsgFrequency measures the frequency of VM operations - VMExecMsgFrequency metric.Int64Counter - // Consensus // // BuildBlockTimer measures the block build duration @@ -177,26 +169,32 @@ func Init(config config.Config) error { } // Networking // - if InboundPeers, err = meter.Int64Histogram( + if InboundPeers, err = meter.Int64Gauge( inboundPeersKey, metric.WithDescription("inbound peer count"), ); err != nil { return fmt.Errorf("unable to create histogram, %w", err) } + // Initialize InboundPeers Gauge + InboundPeers.Record(ctx, 0) - if OutboundPeers, err = meter.Int64Histogram( + if OutboundPeers, err = meter.Int64Gauge( outboundPeersKey, metric.WithDescription("outbound peer count"), ); err != nil { return fmt.Errorf("unable to create histogram, %w", err) } + // Initialize OutboundPeers Gauge + OutboundPeers.Record(ctx, 0) - if DialingPeers, err = meter.Int64Histogram( + if DialingPeers, err = meter.Int64Gauge( dialingPeersKey, metric.WithDescription("dialing peer count"), ); err != nil { return fmt.Errorf("unable to create histogram, %w", err) } + // Initialize DialingPeers Gauge + DialingPeers.Record(ctx, 0) // Mempool // if NumMempoolTxs, err = meter.Int64Histogram( @@ -214,16 +212,9 @@ func Init(config config.Config) error { } // Runtime // - if VMQueryCalls, err = meter.Int64Counter( - vmQueryCallsKey, - metric.WithDescription("vm query call frequency"), - ); err != nil { - return fmt.Errorf("unable to create counter, %w", err) - } - - if VMQueryErrors, err = meter.Int64Counter( - vmQueryErrorsKey, - metric.WithDescription("vm query errors call frequency"), + if VMExecMsgFrequency, err = meter.Int64Counter( + vmExecMsgKey, + metric.WithDescription("vm msg operation call frequency"), ); err != nil { return fmt.Errorf("unable to create counter, %w", err) } @@ -242,13 +233,6 @@ func Init(config config.Config) error { return fmt.Errorf("unable to create histogram, %w", err) } - if VMExecMsgFrequency, err = meter.Int64Counter( - vmExecMsgKey, - metric.WithDescription("vm msg operation call frequency"), - ); err != nil { - return fmt.Errorf("unable to create counter, %w", err) - } - // Consensus // if ValidatorsCount, err = meter.Int64Histogram( validatorCountKey, From 4d80378640ef328cd3fa551517c984a6cf27b26b Mon Sep 17 00:00:00 2001 From: Mikael VALLENET Date: Fri, 22 Nov 2024 02:30:11 +0100 Subject: [PATCH 246/345] docs(std/testing): fix NewUserRealm reference usage (#3178) FIX: https://docs.gno.land/reference/stdlibs/std/testing/#testsetrealm FROM #### Usage ```go addr := std.Address("g1ecely4gjy0yl6s9kt409ll330q9hk2lj9ls3ec") std.TestSetRealm(std.NewUserRealm("")) // or std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) ``` TO #### Usage ```go addr := std.Address("g1ecely4gjy0yl6s9kt409ll330q9hk2lj9ls3ec") std.TestSetRealm(std.NewUserRealm(addr)) // or std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) ```
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- docs/reference/stdlibs/std/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/stdlibs/std/testing.md b/docs/reference/stdlibs/std/testing.md index e3e87ea7262..8a95ecf7827 100644 --- a/docs/reference/stdlibs/std/testing.md +++ b/docs/reference/stdlibs/std/testing.md @@ -106,7 +106,7 @@ Should be used in combination with [`NewUserRealm`](#newuserrealm) & #### Usage ```go addr := std.Address("g1ecely4gjy0yl6s9kt409ll330q9hk2lj9ls3ec") -std.TestSetRealm(std.NewUserRealm("")) +std.TestSetRealm(std.NewUserRealm(addr)) // or std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) ``` From db1d6990e0c17174668fb77698df6d4e4cf19d8c Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 22 Nov 2024 04:15:04 +0100 Subject: [PATCH 247/345] feat: r/docs/home -> r/docs (#3175) I initially considered adding a rule in gnoweb to automatically redirect to `/home`. However, I believe it makes more sense to: 1. Test having a namespace-realm to identify any inconveniences. 2. Designate `r/docs` as the documentation and `r/docs/home` as the homepage for the "docs" team. Later, we might use `r//home` to configure a DefaultRealm that enables redirection when accessing `r/`. I'm not sure yet, but let's experiment. Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/docs/{home/home.gno => docs.gno} | 2 +- examples/gno.land/r/docs/{home/home_test.gno => docs_test.gno} | 2 +- examples/gno.land/r/docs/gno.mod | 1 + examples/gno.land/r/docs/home/gno.mod | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) rename examples/gno.land/r/docs/{home/home.gno => docs.gno} (98%) rename examples/gno.land/r/docs/{home/home_test.gno => docs_test.gno} (97%) create mode 100644 examples/gno.land/r/docs/gno.mod delete mode 100644 examples/gno.land/r/docs/home/gno.mod diff --git a/examples/gno.land/r/docs/home/home.gno b/examples/gno.land/r/docs/docs.gno similarity index 98% rename from examples/gno.land/r/docs/home/home.gno rename to examples/gno.land/r/docs/docs.gno index 6e61f08c11a..f796f07bf4a 100644 --- a/examples/gno.land/r/docs/home/home.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -1,4 +1,4 @@ -package home +package docs func Render(_ string) string { return `# Gno Examples Documentation diff --git a/examples/gno.land/r/docs/home/home_test.gno b/examples/gno.land/r/docs/docs_test.gno similarity index 97% rename from examples/gno.land/r/docs/home/home_test.gno rename to examples/gno.land/r/docs/docs_test.gno index 98dc999e005..aa25332f91b 100644 --- a/examples/gno.land/r/docs/home/home_test.gno +++ b/examples/gno.land/r/docs/docs_test.gno @@ -1,4 +1,4 @@ -package home +package docs import ( "strings" diff --git a/examples/gno.land/r/docs/gno.mod b/examples/gno.land/r/docs/gno.mod new file mode 100644 index 00000000000..227ceb91124 --- /dev/null +++ b/examples/gno.land/r/docs/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs diff --git a/examples/gno.land/r/docs/home/gno.mod b/examples/gno.land/r/docs/home/gno.mod deleted file mode 100644 index b9f8d060f75..00000000000 --- a/examples/gno.land/r/docs/home/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/docs/home From 139ba0681bcd6e511ca558482984591f907d07d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Fri, 22 Nov 2024 10:50:29 +0100 Subject: [PATCH 248/345] chore: sync portal loop machine and `gnolang/gno` repo (#3173) ## Description This PR updates the outdated repo portal loop Dockerfile to align with the Dockerfile on the actual machine, that is more up to date. It also allows the portal loop to be able to pull and be loaded from state on `tx-exports`, using `make pull-exports`.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Sergio Maria Matone --- .github/workflows/portal-loop.yml | 9 +- misc/loop/.env.example | 6 + misc/loop/.gitignore | 1 + misc/loop/Makefile | 23 +++- misc/loop/README.md | 31 ++++- misc/loop/backups/snapshots/.keep | 0 misc/loop/cmd/cmd_backup.go | 24 ++-- misc/loop/cmd/cmd_serve.go | 26 ++-- misc/loop/cmd/cmd_switch.go | 24 ++-- misc/loop/cmd/snapshotter.go | 11 +- misc/loop/docker-compose.override.prod.yml | 6 + misc/loop/docker-compose.production.yml | 142 --------------------- misc/loop/docker-compose.yml | 115 +++++++++++++---- misc/loop/go.mod | 2 - misc/loop/go.sum | 6 - misc/loop/scripts/pull-gh.sh | 56 ++++++++ misc/loop/scripts/start.sh | 2 +- misc/loop/tools.go | 8 -- misc/loop/traefik/gno.yml | 6 +- misc/loop/traefik/gnofaucet.yml | 22 ---- misc/loop/traefik/gnoweb.yml | 22 ---- 21 files changed, 264 insertions(+), 278 deletions(-) create mode 100644 misc/loop/.env.example create mode 100644 misc/loop/backups/snapshots/.keep create mode 100644 misc/loop/docker-compose.override.prod.yml delete mode 100644 misc/loop/docker-compose.production.yml create mode 100755 misc/loop/scripts/pull-gh.sh delete mode 100644 misc/loop/tools.go delete mode 100644 misc/loop/traefik/gnofaucet.yml delete mode 100644 misc/loop/traefik/gnoweb.yml diff --git a/.github/workflows/portal-loop.yml b/.github/workflows/portal-loop.yml index 01135b164ac..b898a149e9d 100644 --- a/.github/workflows/portal-loop.yml +++ b/.github/workflows/portal-loop.yml @@ -57,13 +57,12 @@ jobs: - name: "Checkout" uses: actions/checkout@v4 - - name: "Setup the images" + - name: "Setup The portal loop docker compose" run: | cd misc/loop - - docker compose build - docker compose pull - docker compose up -d + echo "Making docker compose happy" + touch .env + make docker.ci - name: "Test1 - Portal loop start gnoland" run: | diff --git a/misc/loop/.env.example b/misc/loop/.env.example new file mode 100644 index 00000000000..af75eaa9fc7 --- /dev/null +++ b/misc/loop/.env.example @@ -0,0 +1,6 @@ +FAUCET_MNEMONIC= + +COUNTER_MNEMONIC= + +CAPTCHA_SITE_KEY= +CAPTCHA_SECRET_KEY= diff --git a/misc/loop/.gitignore b/misc/loop/.gitignore index 641b553abe6..bacc1a0c085 100644 --- a/misc/loop/.gitignore +++ b/misc/loop/.gitignore @@ -1,3 +1,4 @@ /portalloopd /backups /traefik/letsencrypt +.env diff --git a/misc/loop/Makefile b/misc/loop/Makefile index 3966cd42323..3ad86221ccd 100644 --- a/misc/loop/Makefile +++ b/misc/loop/Makefile @@ -1,18 +1,20 @@ -all: docker.start +PULL_GH_SCRIPT := ./scripts/pull-gh.sh + +all: docker.start.prod + +docker.start.prod: # Start the production portal loop + docker compose -f docker-compose.yml -f docker-compose.override.prod.yml up -d docker.start: # Start the portal loop docker compose up -d +docker.ci: # Start the portal loop for CI + docker compose up -d portalloopd traefik + docker.stop: # Stop the portal loop docker compose down docker rm -f $(docker ps -aq --filter "label=the-portal-loop") -docker.build: # (re)Build snapshotter image - docker compose build - -docker.pull: # Pull new images to update versions - docker compose pull - portalloopd.bash: # Get a bash command inside of the portalloopd container docker compose exec portalloopd bash @@ -20,3 +22,10 @@ switch: portalloopd.switch portalloopd.switch: # Force switch the portal loop with latest image docker compose exec portalloopd switch + +prepare-exports: + chmod +x $(PULL_GH_SCRIPT) && ./$(PULL_GH_SCRIPT) + +pull-exports: docker.stop prepare-exports + docker.start.prod + diff --git a/misc/loop/README.md b/misc/loop/README.md index ce02c83b67c..80618e1e5c7 100644 --- a/misc/loop/README.md +++ b/misc/loop/README.md @@ -1,4 +1,4 @@ -# The portal loop :infinity: +# The portal loop :infinity: ## What is it? @@ -6,17 +6,15 @@ It's a Gnoland node that aim to run with always the latest version of gno and ne For more information, see issue on github [gnolang/gno#1239](https://github.com/gnolang/gno/issues/1239) - ## How to use Start the loop with: ```sh -$ docker compose up -d +docker compose up -d # or using the Makefile - -$ make +make docker.start ``` The [`portalloopd`](./cmd/portalloopd) binary is starting inside of the docker container `portalloopd` @@ -24,7 +22,7 @@ The [`portalloopd`](./cmd/portalloopd) binary is starting inside of the docker c This script is doing: - Setup the current portal-loop in read only mode -- Pull the latest version of [ghcr.io/gnolang/gno]() +- Pull the latest version of [ghcr.io/gnolang/gno](ghcr.io/gnolang/gno) - Backup the txs using [gnolang/tx-archive](https://github.com/gnolang/tx-archive) - Start a new docker container with the backups files - Changing the proxy (traefik) to redirect to the new portal loop @@ -40,3 +38,24 @@ You can find a [Makefile](./Makefile) to help you interact with the portal loop ```bash make portalloopd.switch ``` + +### Running in production + +- Create an `.env` file adding all the entries from `.env.example` +- Setup the DNS names present in the `docker-compose.yml` file +- run using `make all` + +### Pulling in Portal Loop state `from tx-exports` + +To pull Portal Loop state from tx-exports, run the following make directive: + +```bash +make pull-exports +``` + +This will run the following steps: + +- stop any running portal loop containers -> Portal Loop will be down +- clone the `gnolang/tx-exports` repository and prepare the backup txs sheets located there as the genesis transactions + for Portal Loop +- start the portal loop containers -> Portal Loop will start back up again \ No newline at end of file diff --git a/misc/loop/backups/snapshots/.keep b/misc/loop/backups/snapshots/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/misc/loop/cmd/cmd_backup.go b/misc/loop/cmd/cmd_backup.go index f95e1e15a4a..2fa41c03ee4 100644 --- a/misc/loop/cmd/cmd_backup.go +++ b/misc/loop/cmd/cmd_backup.go @@ -12,8 +12,10 @@ import ( type backupCfg struct { rpcAddr string traefikGnoFile string - backupDir string hostPWD string + + masterBackupFile string + snapshotsDir string } func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { @@ -21,8 +23,8 @@ func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { os.Setenv("HOST_PWD", os.Getenv("PWD")) } - if os.Getenv("BACKUP_DIR") == "" { - os.Setenv("BACKUP_DIR", "./backups") + if os.Getenv("SNAPSHOTS_DIR") == "" { + os.Setenv("SNAPSHOTS_DIR", "./backups/snapshots") } if os.Getenv("RPC_URL") == "" { @@ -37,10 +39,15 @@ func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { os.Setenv("TRAEFIK_GNO_FILE", "./traefik/gno.yml") } + if os.Getenv("MASTER_BACKUP_FILE") == "" { + os.Setenv("MASTER_BACKUP_FILE", "./backups/backup.jsonl") + } + fs.StringVar(&c.rpcAddr, "rpc", os.Getenv("RPC_URL"), "tendermint rpc url") fs.StringVar(&c.traefikGnoFile, "traefik-gno-file", os.Getenv("TRAEFIK_GNO_FILE"), "traefik gno file") - fs.StringVar(&c.backupDir, "backup-dir", os.Getenv("BACKUP_DIR"), "backup directory") fs.StringVar(&c.hostPWD, "pwd", os.Getenv("HOST_PWD"), "host pwd (for docker usage)") + fs.StringVar(&c.masterBackupFile, "master-backup-file", os.Getenv("MASTER_BACKUP_FILE"), "master txs backup file path") + fs.StringVar(&c.snapshotsDir, "snapshots-dir", os.Getenv("SNAPSHOTS_DIR"), "snapshots directory") } func newBackupCmd(io commands.IO) *commands.Command { @@ -67,10 +74,11 @@ func execBackup(ctx context.Context, cfg *backupCfg) error { portalLoop := &snapshotter{} portalLoop, err = NewSnapshotter(dockerClient, config{ - backupDir: cfg.backupDir, - rpcAddr: cfg.rpcAddr, - hostPWD: cfg.hostPWD, - traefikGnoFile: cfg.traefikGnoFile, + snapshotsDir: cfg.snapshotsDir, + masterBackupFile: cfg.masterBackupFile, + rpcAddr: cfg.rpcAddr, + hostPWD: cfg.hostPWD, + traefikGnoFile: cfg.traefikGnoFile, }) if err != nil { return err diff --git a/misc/loop/cmd/cmd_serve.go b/misc/loop/cmd/cmd_serve.go index 61303041b34..f796f2268f6 100644 --- a/misc/loop/cmd/cmd_serve.go +++ b/misc/loop/cmd/cmd_serve.go @@ -16,8 +16,10 @@ import ( type serveCfg struct { rpcAddr string traefikGnoFile string - backupDir string hostPWD string + + masterBackupFile string + snapshotsDir string } func (c *serveCfg) RegisterFlags(fs *flag.FlagSet) { @@ -25,8 +27,8 @@ func (c *serveCfg) RegisterFlags(fs *flag.FlagSet) { os.Setenv("HOST_PWD", os.Getenv("PWD")) } - if os.Getenv("BACKUP_DIR") == "" { - os.Setenv("BACKUP_DIR", "./backups") + if os.Getenv("SNAPSHOTS_DIR") == "" { + os.Setenv("SNAPSHOTS_DIR", "./backups/snapshots") } if os.Getenv("RPC_URL") == "" { @@ -41,13 +43,18 @@ func (c *serveCfg) RegisterFlags(fs *flag.FlagSet) { os.Setenv("TRAEFIK_GNO_FILE", "./traefik/gno.yml") } + if os.Getenv("MASTER_BACKUP_FILE") == "" { + os.Setenv("MASTER_BACKUP_FILE", "./backups/backup.jsonl") + } + fs.StringVar(&c.rpcAddr, "rpc", os.Getenv("RPC_URL"), "tendermint rpc url") fs.StringVar(&c.traefikGnoFile, "traefik-gno-file", os.Getenv("TRAEFIK_GNO_FILE"), "traefik gno file") - fs.StringVar(&c.backupDir, "backup-dir", os.Getenv("BACKUP_DIR"), "backup directory") fs.StringVar(&c.hostPWD, "pwd", os.Getenv("HOST_PWD"), "host pwd (for docker usage)") + fs.StringVar(&c.masterBackupFile, "master-backup-file", os.Getenv("MASTER_BACKUP_FILE"), "master txs backup file path") + fs.StringVar(&c.snapshotsDir, "snapshots-dir", os.Getenv("SNAPSHOTS_DIR"), "snapshots directory") } -func newServeCmd(io commands.IO) *commands.Command { +func newServeCmd(_ commands.IO) *commands.Command { cfg := &serveCfg{} return commands.NewCommand( @@ -89,10 +96,11 @@ func execServe(ctx context.Context, cfg *serveCfg, args []string) error { // the loop for { portalLoop, err = NewSnapshotter(dockerClient, config{ - backupDir: cfg.backupDir, - rpcAddr: cfg.rpcAddr, - hostPWD: cfg.hostPWD, - traefikGnoFile: cfg.traefikGnoFile, + snapshotsDir: cfg.snapshotsDir, + masterBackupFile: cfg.masterBackupFile, + rpcAddr: cfg.rpcAddr, + hostPWD: cfg.hostPWD, + traefikGnoFile: cfg.traefikGnoFile, }) if err != nil { return err diff --git a/misc/loop/cmd/cmd_switch.go b/misc/loop/cmd/cmd_switch.go index 02f770cb61c..80487c2805d 100644 --- a/misc/loop/cmd/cmd_switch.go +++ b/misc/loop/cmd/cmd_switch.go @@ -12,8 +12,10 @@ import ( type switchCfg struct { rpcAddr string traefikGnoFile string - backupDir string hostPWD string + + masterBackupFile string + snapshotsDir string } func (c *switchCfg) RegisterFlags(fs *flag.FlagSet) { @@ -21,8 +23,8 @@ func (c *switchCfg) RegisterFlags(fs *flag.FlagSet) { os.Setenv("HOST_PWD", os.Getenv("PWD")) } - if os.Getenv("BACKUP_DIR") == "" { - os.Setenv("BACKUP_DIR", "./backups") + if os.Getenv("SNAPSHOTS_DIR") == "" { + os.Setenv("SNAPSHOTS_DIR", "./backups/snapshots") } if os.Getenv("RPC_URL") == "" { @@ -37,10 +39,15 @@ func (c *switchCfg) RegisterFlags(fs *flag.FlagSet) { os.Setenv("TRAEFIK_GNO_FILE", "./traefik/gno.yml") } + if os.Getenv("MASTER_BACKUP_FILE") == "" { + os.Setenv("MASTER_BACKUP_FILE", "./backups/backup.jsonl") + } + fs.StringVar(&c.rpcAddr, "rpc", os.Getenv("RPC_URL"), "tendermint rpc url") fs.StringVar(&c.traefikGnoFile, "traefik-gno-file", os.Getenv("TRAEFIK_GNO_FILE"), "traefik gno file") - fs.StringVar(&c.backupDir, "backup-dir", os.Getenv("BACKUP_DIR"), "backup directory") fs.StringVar(&c.hostPWD, "pwd", os.Getenv("HOST_PWD"), "host pwd (for docker usage)") + fs.StringVar(&c.masterBackupFile, "master-backup-file", os.Getenv("MASTER_BACKUP_FILE"), "master txs backup file path") + fs.StringVar(&c.snapshotsDir, "snapshots-dir", os.Getenv("SNAPSHOTS_DIR"), "snapshots directory") } func newSwitchCmd(io commands.IO) *commands.Command { @@ -67,10 +74,11 @@ func execSwitch(ctx context.Context, cfg *switchCfg) error { portalLoop := &snapshotter{} portalLoop, err = NewSnapshotter(dockerClient, config{ - backupDir: cfg.backupDir, - rpcAddr: cfg.rpcAddr, - hostPWD: cfg.hostPWD, - traefikGnoFile: cfg.traefikGnoFile, + snapshotsDir: cfg.snapshotsDir, + masterBackupFile: cfg.masterBackupFile, + rpcAddr: cfg.rpcAddr, + hostPWD: cfg.hostPWD, + traefikGnoFile: cfg.traefikGnoFile, }) if err != nil { return err diff --git a/misc/loop/cmd/snapshotter.go b/misc/loop/cmd/snapshotter.go index 06562e2c5c5..2dda5d568d9 100644 --- a/misc/loop/cmd/snapshotter.go +++ b/misc/loop/cmd/snapshotter.go @@ -42,19 +42,22 @@ type snapshotter struct { type config struct { rpcAddr string traefikGnoFile string - backupDir string - hostPWD string + + snapshotsDir string + masterBackupFile string + + hostPWD string } func NewSnapshotter(dockerClient *client.Client, cfg config) (*snapshotter, error) { timenow := time.Now() now := fmt.Sprintf("%s_%v", timenow.Format("2006-01-02_"), timenow.UnixNano()) - backupFile, err := filepath.Abs(cfg.backupDir + "/backup.jsonl") + backupFile, err := filepath.Abs(cfg.masterBackupFile) if err != nil { return nil, err } - instanceBackupFile, err := filepath.Abs(fmt.Sprintf("%s/backup_%s.jsonl", cfg.backupDir, now)) + instanceBackupFile, err := filepath.Abs(fmt.Sprintf("%s/backup_%s.jsonl", cfg.snapshotsDir, now)) if err != nil { return nil, err } diff --git a/misc/loop/docker-compose.override.prod.yml b/misc/loop/docker-compose.override.prod.yml new file mode 100644 index 00000000000..cdc38dc3a73 --- /dev/null +++ b/misc/loop/docker-compose.override.prod.yml @@ -0,0 +1,6 @@ +services: + + portalloopd: + image: ghcr.io/gnolang/gno/portalloopd:latest + ports: + - 127.0.0.1:9090:9090 diff --git a/misc/loop/docker-compose.production.yml b/misc/loop/docker-compose.production.yml deleted file mode 100644 index 2afa013de64..00000000000 --- a/misc/loop/docker-compose.production.yml +++ /dev/null @@ -1,142 +0,0 @@ -version: "3" - -networks: - portal-loop: - name: portal-loop - driver: bridge - ipam: - config: - - subnet: 172.177.0.0/16 - -services: - traefik: - image: "traefik:v2.10" - restart: unless-stopped - command: - - "--api.insecure=true" - - "--providers.file=true" - - "--providers.file.watch=true" - - "--providers.file.directory=/etc/traefik/configs" - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--entrypoints.web.address=:80" - - "--entrypoints.rpc.address=:26657" - - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - - "--entrypoints.web.http.redirections.entrypoint.permanent=true" - - "--entryPoints.web.forwardedHeaders.insecure" - - "--entrypoints.traefik.address=:8080" - - - "--entrypoints.websecure.address=:443" - # - "--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory" - - "--certificatesresolvers.le.acme.tlschallenge=true" - - "--certificatesresolvers.le.acme.email=dev@gno.land" - - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json" - networks: - - portal-loop - ports: - - "80:80" - - "443:443" - - "26657:26657" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - ./traefik:/etc/traefik/configs - - ./traefik/letsencrypt:/letsencrypt - - gnoweb: - image: ghcr.io/gnolang/gno/gnoweb:master - restart: unless-stopped - env_file: ".env" - entrypoint: - - gnoweb - - --bind=0.0.0.0:8888 - - --remote=traefik:26657 - - --faucet-url=https://faucet-api.gno.land - - --captcha-site=$CAPTCHA_SITE_KEY - - --with-analytics - - --help-chainid=portal-loop - - --help-remote=https://rpc.gno.land:443 - networks: - - portal-loop - labels: - com.centurylinklabs.watchtower.enable: "true" - traefik.enable: "true" - traefik.http.routers.gnoweb.entrypoints: "web,websecure" - traefik.http.routers.gnoweb.rule: "Host(`gno.land`) || Host(`www.gno.land`)" - traefik.http.routers.gnoweb.tls: "true" - traefik.http.routers.gnoweb.tls.certresolver: "le" - - gnofaucet: - image: ghcr.io/gnolang/gno/gnofaucet-slim - networks: - - portal-loop - command: - - "serve" - - "--listen-address=0.0.0.0:5050" - - "--chain-id=portal-loop" - - "--is-behind-proxy=true" - - "--mnemonic=${FAUCET_MNEMONIC}" - - "--num-accounts=1" - - "--remote=http://traefik:26657" - - "--captcha-secret=${CAPTCHA_SECRET_KEY}" - env_file: ".env" - # environment: - # from .env - # - RECAPTCHA_SECRET_KEY - labels: - com.centurylinklabs.watchtower.enable: "true" - traefik.enable: "true" - traefik.http.routers.gnofaucet-api.entrypoints: "websecure" - traefik.http.routers.gnofaucet-api.rule: "Host(`faucet-api.gno.land`)" - traefik.http.routers.gnofaucet-api.tls: "true" - traefik.http.routers.gnofaucet-api.tls.certresolver: "le" - traefik.http.middlewares.gnofaucet-ratelimit.ratelimit.average: "6" - traefik.http.middlewares.gnofaucet-ratelimit.ratelimit.period: "1m" - - portalloopd: - image: ghcr.io/gnolang/gno/portalloopd - restart: unless-stopped - volumes: - - ./scripts:/scripts - - ./backups:/backups - - ./traefik:/etc/traefik/configs - - "/var/run/docker.sock:/var/run/docker.sock:ro" - networks: - - portal-loop - ports: - - 127.0.0.1:9090:9090 - environment: - HOST_PWD: $PWD - BACKUP_DIR: "/backups" - RPC_URL: "http://traefik:26657" - PROM_ADDR: "0.0.0.0:9090" - TRAEFIK_GNO_FILE: "/etc/traefik/configs/gno.yml" - extra_hosts: - - host.docker.internal:host-gateway - labels: - - "com.centurylinklabs.watchtower.enable=true" - - autocounterd: - image: ghcr.io/gnolang/gno/autocounterd - restart: unless-stopped - env_file: ".env" - command: - - "start" - - "--chain-id=portal-loop" - - "--interval=15m" - - "--mnemonic=${COUNTER_MNEMONIC}" - - "--rpc=http://traefik:26657" - networks: - - portal-loop - labels: - com.centurylinklabs.watchtower.enable: "true" - - watchtower: - image: containrrr/watchtower - command: --interval 30 --http-api-metrics --label-enable - volumes: - - /var/run/docker.sock:/var/run/docker.sock - environment: - WATCHTOWER_HTTP_API_TOKEN: "mytoken" - ports: - - 127.0.0.1:8000:8080 diff --git a/misc/loop/docker-compose.yml b/misc/loop/docker-compose.yml index ed2fe7192f5..c3adc6a39ea 100644 --- a/misc/loop/docker-compose.yml +++ b/misc/loop/docker-compose.yml @@ -1,11 +1,8 @@ -version: "3" - networks: portal-loop: name: portal-loop driver: bridge ipam: - driver: default config: - subnet: 172.42.0.0/16 @@ -18,53 +15,92 @@ services: - "--providers.file=true" - "--providers.file.watch=true" - "--providers.file.directory=/etc/traefik/configs" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.rpc.address=:26657" + - "--entrypoints.websecure.address=:443" - "--entrypoints.web.address=:80" - - "--entrypoints.private.address=:26657" - - "--entrypoints.traefik.address=:8080" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entrypoints.web.http.redirections.entrypoint.permanent=true" + - "--entryPoints.web.forwardedHeaders.insecure" + - "--certificatesresolvers.le.acme.tlschallenge=true" + - "--certificatesresolvers.le.acme.email=dev@gno.land" networks: - portal-loop ports: - "80:80" - - "8080:8080" + - "443:443" - "26657:26657" volumes: - ./traefik:/etc/traefik/configs + - "/var/run/docker.sock:/var/run/docker.sock:ro" gnoweb: image: ghcr.io/gnolang/gno/gnoweb:master restart: unless-stopped - networks: - - portal-loop - ports: - - 8888:8888 + env_file: ".env" entrypoint: - gnoweb - --bind=0.0.0.0:8888 - --remote=traefik:26657 - - --faucet-url - - "http://localhost:5050" - - --help-chainid + - --faucet-url=https://faucet-api.gno.land + - --captcha-site=$CAPTCHA_SITE_KEY + - --with-analytics + - --help-chainid=portal-loop + - --help-remote=https://rpc.gno.land:443 + networks: - portal-loop - - --help-remote - - http://127.0.0.1:26657 + labels: + com.centurylinklabs.watchtower.enable: "true" + traefik.enable: "true" + traefik.http.routers.gnoweb.entrypoints: "web,websecure" + traefik.http.routers.gnoweb.rule: "Host(`gno.land`) || Host(`www.gno.land`)" + traefik.http.routers.gnoweb.tls: "true" + traefik.http.routers.gnoweb.tls.certresolver: "le" gnofaucet: - image: ghcr.io/gnolang/gno/gnofaucet-slim + image: ghcr.io/gnolang/gno/gnofaucet:master networks: - portal-loop - ports: - - 5050:5050 command: - "serve" - "--listen-address=0.0.0.0:5050" - "--chain-id=portal-loop" - # - "--is-behind-proxy=true" - - "--mnemonic=${MNEMONIC}" - # - "--num-accounts=1" + - "--is-behind-proxy=true" + - "--mnemonic=${FAUCET_MNEMONIC}" + - "--num-accounts=1" - "--remote=http://traefik:26657" - environment: - # from .env - - RECAPTCHA_SECRET_KEY + - "--captcha-secret=${CAPTCHA_SECRET_KEY}" + env_file: ".env" + labels: + com.centurylinklabs.watchtower.enable: "true" + traefik.enable: "true" + traefik.http.routers.gnofaucet-api.entrypoints: "websecure" + traefik.http.routers.gnofaucet-api.rule: "Host(`faucet-api.gno.land`)" + traefik.http.routers.gnofaucet-api.tls: "true" + traefik.http.routers.gnofaucet-api.tls.certresolver: "le" + traefik.http.middlewares.gnofaucet-ratelimit.ratelimit.average: "6" + traefik.http.middlewares.gnofaucet-ratelimit.ratelimit.period: "1m" + + tx-indexer: + image: ghcr.io/gnolang/tx-indexer:latest + networks: + - portal-loop + entrypoint: + - /tx-indexer + - start + - "-http-rate-limit=500" + - "-listen-address=0.0.0.0:8546" + - "-max-slots=2000" + - "-remote=http://traefik:26657" + labels: + traefik.enable: "true" + traefik.http.routers.tx-indexer.entrypoints: "websecure" + traefik.http.routers.tx-indexer.rule: "Host(`indexer.portal.gnoteam.com`)" + traefik.http.routers.tx-indexer.tls: "true" + traefik.http.routers.tx-indexer.tls.certresolver: "le" + traefik.http.services.tx-indexer.loadbalancer.server.port: 8546 portalloopd: build: @@ -82,9 +118,38 @@ services: - 9090:9090 environment: HOST_PWD: $PWD - BACKUP_DIR: "/backups" + SNAPSHOTS_DIR: "/backups/snapshots" + MASTER_BACKUP_FILE: "/backups/backup.jsonl" RPC_URL: "http://traefik:26657" PROM_ADDR: "0.0.0.0:9090" TRAEFIK_GNO_FILE: "/etc/traefik/configs/gno.yml" extra_hosts: - host.docker.internal:host-gateway + labels: + - "com.centurylinklabs.watchtower.enable=true" + + autocounterd: + image: ghcr.io/gnolang/gno/autocounterd:latest + restart: unless-stopped + env_file: ".env" + command: + - "start" + - "--chain-id=portal-loop" + - "--interval=15m" + - "--mnemonic=${COUNTER_MNEMONIC}" + - "--rpc=http://traefik:26657" + networks: + - portal-loop + labels: + com.centurylinklabs.watchtower.enable: "true" + + watchtower: + image: containrrr/watchtower + command: --interval 30 --http-api-metrics --label-enable + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /home/devops/.docker/config.json:/config.json + environment: + WATCHTOWER_HTTP_API_TOKEN: "mytoken" + ports: + - 127.0.0.1:8000:8080 diff --git a/misc/loop/go.mod b/misc/loop/go.mod index 9fc5bfb2d57..f1c09cd9f82 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -65,8 +65,6 @@ require ( go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.uber.org/zap/exp v0.2.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 27ed94fecae..740cc629a21 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -201,14 +201,8 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/misc/loop/scripts/pull-gh.sh b/misc/loop/scripts/pull-gh.sh new file mode 100755 index 00000000000..efbb360d551 --- /dev/null +++ b/misc/loop/scripts/pull-gh.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env sh + +TMP_DIR=temp-tx-exports + +# The master backup file will contain the ultimate txs backup +# that the portal loop use when looping (generating the genesis) +MASTER_BACKUP_FILE="backup.jsonl" + +# Clones the portal loop backups subdirectory, located in BACKUPS_REPO (tx-exports) +pullGHBackups () { + BACKUPS_REPO=https://github.com/gnolang/tx-exports.git + BACKUPS_REPO_PATH="portal-loop" + + # Clone just the root folder of the same name + git clone --depth 1 --no-checkout $BACKUPS_REPO + cd "$(basename "$BACKUPS_REPO" .git)" || exit 1 + + # Clone just the backups path in the cloned repo + git sparse-checkout set $BACKUPS_REPO_PATH + git checkout + + # Go back to the parent directory + cd .. +} + +# Create the temporary working dir +rm -rf $TMP_DIR && mkdir $TMP_DIR +cd $TMP_DIR || exit 1 + +# Pull the backup repo data +pullGHBackups + +# Combine the pulled backups into a single backup file +TXS_BACKUPS_PREFIX="backup_portal_loop_txs_" + +find . -type f -name "${TXS_BACKUPS_PREFIX}*.jsonl" | sort | xargs cat > "temp_$MASTER_BACKUP_FILE" + +BACKUPS_DIR="../backups" +TIMESTAMP=$(date +%s) + +# Check if the master backup file already exists +if [ -e "$BACKUPS_DIR/$MASTER_BACKUP_FILE" ]; then + # Back up the existing master txs file + echo "Master backup file exists, backing up..." + mv "$BACKUPS_DIR/$MASTER_BACKUP_FILE" "$BACKUPS_DIR/${MASTER_BACKUP_FILE}-legacy-$TIMESTAMP" + + echo "Renamed $MASTER_BACKUP_FILE to ${MASTER_BACKUP_FILE}-legacy-$TIMESTAMP" +fi + +# Use the GitHub state as the canonical backup +mv "temp_$MASTER_BACKUP_FILE" "$BACKUPS_DIR/$MASTER_BACKUP_FILE" +echo "Moved temp_$MASTER_BACKUP_FILE to $BACKUPS_DIR/$MASTER_BACKUP_FILE" + +# Clean up the temporary directory +cd .. +rm -rf $TMP_DIR diff --git a/misc/loop/scripts/start.sh b/misc/loop/scripts/start.sh index 76869ccb4bd..6dd57b2c041 100755 --- a/misc/loop/scripts/start.sh +++ b/misc/loop/scripts/start.sh @@ -12,7 +12,7 @@ SEEDS=${SEEDS:-""} PERSISTENT_PEERS=${PERSISTENT_PEERS:-""} echo "" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl -cat ${GENESIS_BACKUP_FILE} >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +cat "${GENESIS_BACKUP_FILE}" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl # Initialize the secrets gnoland secrets init diff --git a/misc/loop/tools.go b/misc/loop/tools.go deleted file mode 100644 index 789ea2949f0..00000000000 --- a/misc/loop/tools.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build tools - -package tools - -import ( - _ "github.com/gnolang/gno/gno.land/cmd/gnoland" - _ "github.com/gnolang/tx-archive/cmd" -) diff --git a/misc/loop/traefik/gno.yml b/misc/loop/traefik/gno.yml index 7e65930889d..09d7e0d0815 100644 --- a/misc/loop/traefik/gno.yml +++ b/misc/loop/traefik/gno.yml @@ -11,7 +11,7 @@ http: gno-portal-loop-local: service: gno-portal-loop rule: "PathPrefix(`/`)" - entrypoints: ["private"] + entrypoints: [ "rpc" ] middlewares: [] gno-portal-loop: @@ -19,11 +19,11 @@ http: tls: certResolver: le rule: "Host(`rpc.gno.land`) || Host(`rpc.portal.gnoteam.com`)" - entrypoints: ["web", "websecure"] + entrypoints: [ "web", "websecure" ] middlewares: [] services: gno-portal-loop: loadBalancer: servers: - - url: "http://172.42.0.2:26657" + - url: "http://172.42.0.4:26657" diff --git a/misc/loop/traefik/gnofaucet.yml b/misc/loop/traefik/gnofaucet.yml deleted file mode 100644 index 25dd092a504..00000000000 --- a/misc/loop/traefik/gnofaucet.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -http: - routers: - gnofaucet-local: - service: gnofaucet - rule: "Host(`faucet.portal.gno.local`)" - entrypoints: ["web", "websecure", "private"] - middlewares: [] - - gnofaucet: - service: gnofaucet - rule: "Host(`faucet.gno.land`) || Host(`faucet.portal.gnoteam.com`)" - tls: - certResolver: le - entrypoints: ["web", "websecure"] - middlewares: [] - - services: - gnofaucet: - loadBalancer: - servers: - - url: "http://localhost:9000" diff --git a/misc/loop/traefik/gnoweb.yml b/misc/loop/traefik/gnoweb.yml deleted file mode 100644 index 8bce0f4bfb6..00000000000 --- a/misc/loop/traefik/gnoweb.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -http: - routers: - gnoweb-local: - service: gnoweb - rule: "Host(`portal.gno.local`)" - entrypoints: ["web", "websecure", "private"] - middlewares: [] - - gnoweb: - service: gnoweb - rule: "Host(`gno.land`) || Host(`portal.gnoteam.com`)" - tls: - certResolver: le - entrypoints: ["web", "websecure"] - middlewares: [] - - services: - gnoweb: - loadBalancer: - servers: - - url: "http://localhost:8888" From b3e4aed721b5bca386dbcdd7f5823b639d9bd47f Mon Sep 17 00:00:00 2001 From: Sergio Maria Matone Date: Fri, 22 Nov 2024 11:39:33 +0100 Subject: [PATCH 249/345] chore: (portal loop): Fixing Portal loop prod config (#3181) small fixes to portal loop prod config and autocounterd build --- .github/workflows/autocounterd.yml | 1 + misc/loop/docker-compose.override.prod.yml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/autocounterd.yml b/.github/workflows/autocounterd.yml index 63799960df5..9217fe2eef2 100644 --- a/.github/workflows/autocounterd.yml +++ b/.github/workflows/autocounterd.yml @@ -7,6 +7,7 @@ on: push: paths: - misc/autocounterd + - misc/loop - .github/workflows/autocounterd.yml branches: - "master" diff --git a/misc/loop/docker-compose.override.prod.yml b/misc/loop/docker-compose.override.prod.yml index cdc38dc3a73..90b8e7c71a7 100644 --- a/misc/loop/docker-compose.override.prod.yml +++ b/misc/loop/docker-compose.override.prod.yml @@ -2,5 +2,3 @@ services: portalloopd: image: ghcr.io/gnolang/gno/portalloopd:latest - ports: - - 127.0.0.1:9090:9090 From 77e660614018f07175e8f3569cdc98be03130602 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Sat, 23 Nov 2024 00:31:21 +0100 Subject: [PATCH 250/345] fix: invoke user recover with implicit panics (#3067) Currently only explicit panic invocations are recovered in the user code. This PR covers the implicit panics that happen because of invalid operations. Associated [issue](https://github.com/gnolang/gno/issues/1148) This maintains the distinction between VM panics and user panics. Here is a list of possible runtime panics that we will cover. - [x] Out-of-Bounds Slice or Array Access - [x] Invalid Slice Indexing - [x] Division by Zero and MOD zero - [x] Type Assertion Failure - [x] Invalid Memory Allocation (bad call to make()) - [x] Out-of-Bounds String Indexing - [x] nil pointer dereference - [x] Write to a nil map Also, fixed a small bug with the builtint function `make`. It wasn't panicking when cap > len --- gnovm/pkg/gnolang/alloc.go | 7 ++ gnovm/pkg/gnolang/debugger_test.go | 2 +- gnovm/pkg/gnolang/machine.go | 18 ++++- gnovm/pkg/gnolang/op_assign.go | 12 +++- gnovm/pkg/gnolang/op_binary.go | 108 ++++++++++++++++++++++++++-- gnovm/pkg/gnolang/op_expressions.go | 4 ++ gnovm/pkg/gnolang/uverse.go | 5 ++ gnovm/pkg/gnolang/values.go | 60 ++++++++++------ gnovm/tests/files/recover12.gno | 15 ++++ gnovm/tests/files/recover13.gno | 15 ++++ gnovm/tests/files/recover14.gno | 15 ++++ gnovm/tests/files/recover15.gno | 15 ++++ gnovm/tests/files/recover16.gno | 14 ++++ gnovm/tests/files/recover17.gno | 15 ++++ gnovm/tests/files/recover18.gno | 15 ++++ gnovm/tests/files/recover19.gno | 15 ++++ 16 files changed, 307 insertions(+), 28 deletions(-) create mode 100644 gnovm/tests/files/recover12.gno create mode 100644 gnovm/tests/files/recover13.gno create mode 100644 gnovm/tests/files/recover14.gno create mode 100644 gnovm/tests/files/recover15.gno create mode 100644 gnovm/tests/files/recover16.gno create mode 100644 gnovm/tests/files/recover17.gno create mode 100644 gnovm/tests/files/recover18.gno create mode 100644 gnovm/tests/files/recover19.gno diff --git a/gnovm/pkg/gnolang/alloc.go b/gnovm/pkg/gnolang/alloc.go index df042038e43..7e942ab61b9 100644 --- a/gnovm/pkg/gnolang/alloc.go +++ b/gnovm/pkg/gnolang/alloc.go @@ -194,6 +194,9 @@ func (alloc *Allocator) NewString(s string) StringValue { } func (alloc *Allocator) NewListArray(n int) *ArrayValue { + if n < 0 { + panic(&Exception{Value: typedString("len out of range")}) + } alloc.AllocateListArray(int64(n)) return &ArrayValue{ List: make([]TypedValue, n), @@ -201,6 +204,10 @@ func (alloc *Allocator) NewListArray(n int) *ArrayValue { } func (alloc *Allocator) NewDataArray(n int) *ArrayValue { + if n < 0 { + panic(&Exception{Value: typedString("len out of range")}) + } + alloc.AllocateDataArray(int64(n)) return &ArrayValue{ Data: make([]byte, n), diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 44786257d67..63a3ee74675 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -158,7 +158,7 @@ func TestDebug(t *testing.T) { {in: "up xxx", out: `"xxx": invalid syntax`}, {in: "b 37\nc\np b\n", out: "(3 int)"}, {in: "b 27\nc\np b\n", out: `("!zero" string)`}, - {in: "b 22\nc\np t.A[3]\n", out: "Command failed: slice index out of bounds: 3 (len=3)"}, + {in: "b 22\nc\np t.A[3]\n", out: "Command failed: &{(\"slice index out of bounds: 3 (len=3)\" string) }"}, {in: "b 43\nc\nc\nc\np i\ndetach\n", out: "(1 int)"}, }) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 33bf32730c5..aac8b4f5802 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -829,7 +829,9 @@ func (m *Machine) RunFunc(fn Name) { func (m *Machine) RunMain() { defer func() { - if r := recover(); r != nil { + r := recover() + + if r != nil { switch r := r.(type) { case UnhandledPanicError: fmt.Printf("Machine.RunMain() panic: %s\nStacktrace: %s\n", @@ -1280,6 +1282,20 @@ const ( // main run loop. func (m *Machine) Run() { + defer func() { + r := recover() + + if r != nil { + switch r := r.(type) { + case *Exception: + m.Panic(r.Value) + m.Run() + default: + panic(r) + } + } + }() + for { if m.Debugger.enabled { m.Debug() diff --git a/gnovm/pkg/gnolang/op_assign.go b/gnovm/pkg/gnolang/op_assign.go index 8caacbfd1e6..114c8c589c4 100644 --- a/gnovm/pkg/gnolang/op_assign.go +++ b/gnovm/pkg/gnolang/op_assign.go @@ -131,7 +131,11 @@ func (m *Machine) doOpQuoAssign() { } } // lv /= rv - quoAssign(lv.TV, rv) + err := quoAssign(lv.TV, rv) + if err != nil { + panic(err) + } + if lv.Base != nil { m.Realm.DidUpdate(lv.Base.(Object), nil, nil) } @@ -154,7 +158,11 @@ func (m *Machine) doOpRemAssign() { } } // lv %= rv - remAssign(lv.TV, rv) + err := remAssign(lv.TV, rv) + if err != nil { + panic(err) + } + if lv.Base != nil { m.Realm.DidUpdate(lv.Base.(Object), nil, nil) } diff --git a/gnovm/pkg/gnolang/op_binary.go b/gnovm/pkg/gnolang/op_binary.go index 24123d285ad..a541a7da8b5 100644 --- a/gnovm/pkg/gnolang/op_binary.go +++ b/gnovm/pkg/gnolang/op_binary.go @@ -252,7 +252,10 @@ func (m *Machine) doOpQuo() { } // lv / rv - quoAssign(lv, rv) + err := quoAssign(lv, rv) + if err != nil { + panic(err) + } } func (m *Machine) doOpRem() { @@ -266,7 +269,10 @@ func (m *Machine) doOpRem() { } // lv % rv - remAssign(lv, rv) + err := remAssign(lv, rv) + if err != nil { + panic(err) + } } func (m *Machine) doOpShl() { @@ -845,45 +851,94 @@ func mulAssign(lv, rv *TypedValue) { } // for doOpQuo and doOpQuoAssign. -func quoAssign(lv, rv *TypedValue) { +func quoAssign(lv, rv *TypedValue) *Exception { + expt := &Exception{ + Value: typedString("division by zero"), + } + // set the result in lv. // NOTE this block is replicated in op_assign.go switch baseOf(lv.T) { case IntType: + if rv.GetInt() == 0 { + return expt + } lv.SetInt(lv.GetInt() / rv.GetInt()) case Int8Type: + if rv.GetInt8() == 0 { + return expt + } lv.SetInt8(lv.GetInt8() / rv.GetInt8()) case Int16Type: + if rv.GetInt16() == 0 { + return expt + } lv.SetInt16(lv.GetInt16() / rv.GetInt16()) case Int32Type, UntypedRuneType: + if rv.GetInt32() == 0 { + return expt + } lv.SetInt32(lv.GetInt32() / rv.GetInt32()) case Int64Type: + if rv.GetInt64() == 0 { + return expt + } lv.SetInt64(lv.GetInt64() / rv.GetInt64()) case UintType: + if rv.GetUint() == 0 { + return expt + } lv.SetUint(lv.GetUint() / rv.GetUint()) case Uint8Type: + if rv.GetUint8() == 0 { + return expt + } lv.SetUint8(lv.GetUint8() / rv.GetUint8()) case DataByteType: + if rv.GetUint8() == 0 { + return expt + } lv.SetDataByte(lv.GetDataByte() / rv.GetUint8()) case Uint16Type: + if rv.GetUint16() == 0 { + return expt + } lv.SetUint16(lv.GetUint16() / rv.GetUint16()) case Uint32Type: + if rv.GetUint32() == 0 { + return expt + } lv.SetUint32(lv.GetUint32() / rv.GetUint32()) case Uint64Type: + if rv.GetUint64() == 0 { + return expt + } lv.SetUint64(lv.GetUint64() / rv.GetUint64()) case Float32Type: // NOTE: gno doesn't fuse *+. + if rv.GetFloat32() == 0 { + return expt + } lv.SetFloat32(lv.GetFloat32() / rv.GetFloat32()) // XXX FOR DETERMINISM, PANIC IF NAN. case Float64Type: // NOTE: gno doesn't fuse *+. + if rv.GetFloat64() == 0 { + return expt + } lv.SetFloat64(lv.GetFloat64() / rv.GetFloat64()) // XXX FOR DETERMINISM, PANIC IF NAN. case BigintType, UntypedBigintType: + if rv.GetBigInt().Sign() == 0 { + return expt + } lb := lv.GetBigInt() lb = big.NewInt(0).Quo(lb, rv.GetBigInt()) lv.V = BigintValue{V: lb} case BigdecType, UntypedBigdecType: + if rv.GetBigDec().Cmp(apd.New(0, 0)) == 0 { + return expt + } lb := lv.GetBigDec() rb := rv.GetBigDec() quo := apd.New(0, 0) @@ -898,36 +953,79 @@ func quoAssign(lv, rv *TypedValue) { lv.T, )) } + + return nil } // for doOpRem and doOpRemAssign. -func remAssign(lv, rv *TypedValue) { +func remAssign(lv, rv *TypedValue) *Exception { + expt := &Exception{ + Value: typedString("division by zero"), + } + // set the result in lv. // NOTE this block is replicated in op_assign.go switch baseOf(lv.T) { case IntType: + if rv.GetInt() == 0 { + return expt + } lv.SetInt(lv.GetInt() % rv.GetInt()) case Int8Type: + if rv.GetInt8() == 0 { + return expt + } lv.SetInt8(lv.GetInt8() % rv.GetInt8()) case Int16Type: + if rv.GetInt16() == 0 { + return expt + } lv.SetInt16(lv.GetInt16() % rv.GetInt16()) case Int32Type, UntypedRuneType: + if rv.GetInt32() == 0 { + return expt + } lv.SetInt32(lv.GetInt32() % rv.GetInt32()) case Int64Type: + if rv.GetInt64() == 0 { + return expt + } lv.SetInt64(lv.GetInt64() % rv.GetInt64()) case UintType: + if rv.GetUint() == 0 { + return expt + } lv.SetUint(lv.GetUint() % rv.GetUint()) case Uint8Type: + if rv.GetUint8() == 0 { + return expt + } lv.SetUint8(lv.GetUint8() % rv.GetUint8()) case DataByteType: + if rv.GetUint8() == 0 { + return expt + } lv.SetDataByte(lv.GetDataByte() % rv.GetUint8()) case Uint16Type: + if rv.GetUint16() == 0 { + return expt + } lv.SetUint16(lv.GetUint16() % rv.GetUint16()) case Uint32Type: + if rv.GetUint32() == 0 { + return expt + } lv.SetUint32(lv.GetUint32() % rv.GetUint32()) case Uint64Type: + if rv.GetUint64() == 0 { + return expt + } lv.SetUint64(lv.GetUint64() % rv.GetUint64()) case BigintType, UntypedBigintType: + if rv.GetBigInt().Sign() == 0 { + return expt + } + lb := lv.GetBigInt() lb = big.NewInt(0).Rem(lb, rv.GetBigInt()) lv.V = BigintValue{V: lb} @@ -937,6 +1035,8 @@ func remAssign(lv, rv *TypedValue) { lv.T, )) } + + return nil } // for doOpBand and doOpBandAssign. diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index a1d677ca878..b661d693304 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -145,6 +145,10 @@ func (m *Machine) doOpStar() { xv := m.PopValue() switch bt := baseOf(xv.T).(type) { case *PointerType: + if xv.V == nil { + panic(&Exception{Value: typedString("nil pointer dereference")}) + } + pv := xv.V.(PointerValue) if pv.TV.T == DataByteType { tv := TypedValue{T: bt.Elt} diff --git a/gnovm/pkg/gnolang/uverse.go b/gnovm/pkg/gnolang/uverse.go index ba66b27499f..2780e6d8034 100644 --- a/gnovm/pkg/gnolang/uverse.go +++ b/gnovm/pkg/gnolang/uverse.go @@ -838,6 +838,11 @@ func makeUverseNode() { li := lv.ConvertGetInt() cv := vargs.TV.GetPointerAtIndexInt(m.Store, 1).Deref() ci := cv.ConvertGetInt() + + if ci < li { + panic(&Exception{Value: typedString(`makeslice: cap out of range`)}) + } + if et.Kind() == Uint8Kind { arrayValue := m.Alloc.NewDataArray(ci) m.PushValue(TypedValue{ diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 68c2967811f..e4141772d98 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -436,12 +436,18 @@ func (sv *SliceValue) GetLength() int { func (sv *SliceValue) GetPointerAtIndexInt2(store Store, ii int, et Type) PointerValue { // Necessary run-time slice bounds check if ii < 0 { - panic(fmt.Sprintf( - "slice index out of bounds: %d", ii)) + excpt := &Exception{ + Value: typedString(fmt.Sprintf( + "slice index out of bounds: %d", ii)), + } + panic(excpt) } else if sv.Length <= ii { - panic(fmt.Sprintf( - "slice index out of bounds: %d (len=%d)", - ii, sv.Length)) + excpt := &Exception{ + Value: typedString(fmt.Sprintf( + "slice index out of bounds: %d (len=%d)", + ii, sv.Length)), + } + panic(excpt) } return sv.GetBase(store).GetPointerAtIndexInt2(store, sv.Offset+ii, et) } @@ -1700,6 +1706,9 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val path.Type = VPField path.Depth = 0 case 2: + if tv.V == nil { + panic(&Exception{Value: typedString("nil pointer dereference")}) + } dtv = tv.V.(PointerValue).TV isPtr = true path.Type = VPField @@ -1975,6 +1984,14 @@ func (tv *TypedValue) GetPointerAtIndex(alloc *Allocator, store Store, iv *Typed bv := &TypedValue{ // heap alloc T: Uint8Type, } + + if ii >= len(sv) { + panic(&Exception{Value: typedString(fmt.Sprintf("index out of range [%d] with length %d", ii, len(sv)))}) + } + if ii < 0 { + panic(&Exception{Value: typedString(fmt.Sprintf("invalid slice index %d (index must be non-negative)", ii))}) + } + bv.SetUint8(sv[ii]) return PointerValue{ TV: bv, @@ -1997,7 +2014,7 @@ func (tv *TypedValue) GetPointerAtIndex(alloc *Allocator, store Store, iv *Typed return sv.GetPointerAtIndexInt2(store, ii, bt.Elt) case *MapType: if tv.V == nil { - panic("uninitialized map index") + panic(&Exception{Value: typedString("uninitialized map index")}) } mv := tv.V.(*MapValue) pv := mv.GetPointerForKey(alloc, store, iv) @@ -2149,26 +2166,26 @@ func (tv *TypedValue) GetCapacity() int { func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { if low < 0 { - panic(fmt.Sprintf( + panic(&Exception{Value: typedString(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - low)) + low))}) } if high < 0 { - panic(fmt.Sprintf( + panic(&Exception{Value: typedString(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - high)) + low))}) } if low > high { - panic(fmt.Sprintf( + panic(&Exception{Value: typedString(fmt.Sprintf( "invalid slice index %d > %d", - low, high)) + low, high))}) } switch t := baseOf(tv.T).(type) { case PrimitiveType: if tv.GetLength() < high { - panic(fmt.Sprintf( + panic(&Exception{Value: typedString(fmt.Sprintf( "slice bounds out of range [%d:%d] with string length %d", - low, high, tv.GetLength())) + low, high, tv.GetLength()))}) } if t == StringType || t == UntypedStringType { return TypedValue{ @@ -2176,12 +2193,14 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { V: alloc.NewString(tv.GetString()[low:high]), } } - panic("non-string primitive type cannot be sliced") + panic(&Exception{Value: typedString(fmt.Sprintf( + "non-string primitive type cannot be sliced", + ))}) case *ArrayType: if tv.GetLength() < high { - panic(fmt.Sprintf( + panic(&Exception{Value: typedString(fmt.Sprintf( "slice bounds out of range [%d:%d] with array length %d", - low, high, tv.GetLength())) + low, high, tv.GetLength()))}) } av := tv.V.(*ArrayValue) st := alloc.NewType(&SliceType{ @@ -2199,13 +2218,14 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { } case *SliceType: if tv.GetCapacity() < high { - panic(fmt.Sprintf( + panic(&Exception{Value: typedString(fmt.Sprintf( "slice bounds out of range [%d:%d] with capacity %d", - low, high, tv.GetCapacity())) + low, high, tv.GetCapacity()))}) } if tv.V == nil { if low != 0 || high != 0 { - panic("nil slice index out of range") + panic(&Exception{Value: typedString(fmt.Sprintf( + "nil slice index out of range"))}) } return TypedValue{ T: tv.T, diff --git a/gnovm/tests/files/recover12.gno b/gnovm/tests/files/recover12.gno new file mode 100644 index 00000000000..ab16959adfb --- /dev/null +++ b/gnovm/tests/files/recover12.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + arr := []int{1, 2, 3} + _ = arr[3] // Panics because index 3 is out of bounds +} + +// Output: +// recover: slice index out of bounds: 3 (len=3) diff --git a/gnovm/tests/files/recover13.gno b/gnovm/tests/files/recover13.gno new file mode 100644 index 00000000000..d4b1df10ae5 --- /dev/null +++ b/gnovm/tests/files/recover13.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + arr := []int{1, 2, 3} + _ = arr[-1:] // Panics because of negative index +} + +// Output: +// recover: invalid slice index -1 (index must be non-negative) diff --git a/gnovm/tests/files/recover14.gno b/gnovm/tests/files/recover14.gno new file mode 100644 index 00000000000..30a34ab291a --- /dev/null +++ b/gnovm/tests/files/recover14.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + x, y := 10, 0 + _ = x / y // Panics because of division by zero +} + +// Output: +// recover: division by zero diff --git a/gnovm/tests/files/recover15.gno b/gnovm/tests/files/recover15.gno new file mode 100644 index 00000000000..74ba3ea66a2 --- /dev/null +++ b/gnovm/tests/files/recover15.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + var i interface{} = "hello" + _ = i.(int) // Panics because i holds a string, not an int +} + +// Output: +// recover: string is not of type int diff --git a/gnovm/tests/files/recover16.gno b/gnovm/tests/files/recover16.gno new file mode 100644 index 00000000000..31770f6d469 --- /dev/null +++ b/gnovm/tests/files/recover16.gno @@ -0,0 +1,14 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + _ = make([]int, -1) // Panics because of negative length +} + +// Output: +// recover: len out of range diff --git a/gnovm/tests/files/recover17.gno b/gnovm/tests/files/recover17.gno new file mode 100644 index 00000000000..575801017b3 --- /dev/null +++ b/gnovm/tests/files/recover17.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + str := "hello" + _ = str[10] // Panics because index 10 is out of bounds +} + +// Output: +// recover: index out of range [10] with length 5 diff --git a/gnovm/tests/files/recover18.gno b/gnovm/tests/files/recover18.gno new file mode 100644 index 00000000000..f717e560dd2 --- /dev/null +++ b/gnovm/tests/files/recover18.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + var m map[string]int // nil map + m["key"] = 42 // Panics when trying to assign to a nil map +} + +// Output: +// recover: uninitialized map index diff --git a/gnovm/tests/files/recover19.gno b/gnovm/tests/files/recover19.gno new file mode 100644 index 00000000000..e1f0ff4c3b1 --- /dev/null +++ b/gnovm/tests/files/recover19.gno @@ -0,0 +1,15 @@ +package main + + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + var p *int + println(*p) +} + +// Output: +// recover: nil pointer dereference From d0493dffec850c9d018e80b617b5d9c70d286c61 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Sat, 23 Nov 2024 00:32:31 +0100 Subject: [PATCH 251/345] fix: typed const conversion validation (#3117) During preprocessing, validates if typed constants are convertible. Fixes [issue](https://github.com/gnolang/gno/issues/2681) Typed constants are convertible only in a lossless way. That means that we can convert floats to integers if the fractional part of the float is 0. --- gnovm/pkg/gnolang/gno_test.go | 130 ++++++++ gnovm/pkg/gnolang/op_expressions.go | 2 +- gnovm/pkg/gnolang/preprocess.go | 2 +- gnovm/pkg/gnolang/values.go | 2 +- gnovm/pkg/gnolang/values_conversions.go | 384 +++++++++++++++++++++++- gnovm/tests/files/float1.gno | 3 +- 6 files changed, 515 insertions(+), 8 deletions(-) diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go index 3b15c018505..89458667997 100644 --- a/gnovm/pkg/gnolang/gno_test.go +++ b/gnovm/pkg/gnolang/gno_test.go @@ -129,6 +129,136 @@ func TestBuiltinIdentifiersShadowing(t *testing.T) { } } +func TestConvertTo(t *testing.T) { + t.Parallel() + + testFunc := func(source, msg string) { + defer func() { + if len(msg) == 0 { + return + } + + r := recover() + + if r == nil { + t.Fail() + } + + err := r.(*PreprocessError) + c := strings.Contains(err.Error(), msg) + if !c { + t.Fatalf(`expected "%s", got "%s"`, msg, r) + } + }() + + m := NewMachine("test", nil) + + n := MustParseFile("main.go", source) + m.RunFiles(n) + m.RunMain() + } + + type cases struct { + source string + msg string + } + + tests := []cases{ + { + `package test + +func main() { + const a int = -1 + println(uint(a)) +}`, + `test/main.go:5:13: cannot convert constant of type IntKind to UintKind`, + }, + { + `package test + +func main() { + const a int = -1 + println(uint8(a)) +}`, + `test/main.go:5:13: cannot convert constant of type IntKind to Uint8Kind`, + }, + { + `package test + +func main() { + const a int = -1 + println(uint16(a)) +}`, + `test/main.go:5:13: cannot convert constant of type IntKind to Uint16Kind`, + }, + { + `package test + +func main() { + const a int = -1 + println(uint32(a)) +}`, + `test/main.go:5:13: cannot convert constant of type IntKind to Uint32Kind`, + }, + { + `package test + +func main() { + const a int = -1 + println(uint64(a)) +}`, + `test/main.go:5:13: cannot convert constant of type IntKind to Uint64Kind`, + }, + { + `package test + +func main() { + const a float32 = 1.5 + println(int32(a)) +}`, + `test/main.go:5:13: cannot convert constant of type Float32Kind to Int32Kind`, + }, + { + `package test + +func main() { + println(int32(1.5)) +}`, + `test/main.go:4:13: cannot convert (const (1.5 bigdec)) to integer type`, + }, + { + `package test + +func main() { + const a float64 = 1.5 + println(int64(a)) +}`, + `test/main.go:5:13: cannot convert constant of type Float64Kind to Int64Kind`, + }, + { + `package test + +func main() { + println(int64(1.5)) +}`, + `test/main.go:4:13: cannot convert (const (1.5 bigdec)) to integer type`, + }, + { + `package test + + func main() { + const f = float64(1.0) + println(int64(f)) + }`, + ``, + }, + } + + for _, tc := range tests { + testFunc(tc.source, tc.msg) + } +} + // run empty main(). func TestRunEmptyMain(t *testing.T) { t.Parallel() diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index b661d693304..b614e72e945 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -800,6 +800,6 @@ func (m *Machine) doOpFuncLit() { func (m *Machine) doOpConvert() { xv := m.PopValue() t := m.PopValue().GetType() - ConvertTo(m.Alloc, m.Store, xv, t) + ConvertTo(m.Alloc, m.Store, xv, t, false) m.PushValue(*xv) } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index b7c22e0b9f6..85a846535ce 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3656,7 +3656,7 @@ func convertConst(store Store, last BlockNode, cx *ConstExpr, t Type) { setConstAttrs(cx) } else if t != nil { // e.g. a named type or uint8 type to int for indexing. - ConvertTo(nilAllocator, store, &cx.TypedValue, t) + ConvertTo(nilAllocator, store, &cx.TypedValue, t, true) setConstAttrs(cx) } } diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index e4141772d98..8e27bcbcbdb 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1207,7 +1207,7 @@ func (tv *TypedValue) SetInt(n int) { func (tv *TypedValue) ConvertGetInt() int { var store Store = nil // not used - ConvertTo(nilAllocator, store, tv, IntType) + ConvertTo(nilAllocator, store, tv, IntType, false) return tv.GetInt() } diff --git a/gnovm/pkg/gnolang/values_conversions.go b/gnovm/pkg/gnolang/values_conversions.go index 9ec3427ed8f..df93144b4e7 100644 --- a/gnovm/pkg/gnolang/values_conversions.go +++ b/gnovm/pkg/gnolang/values_conversions.go @@ -13,7 +13,7 @@ import ( // t cannot be nil or untyped or DataByteType. // the conversion is forced and overflow/underflow is ignored. // TODO: return error, and let caller also print the file and line. -func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type) { +func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bool) { if debug { if t == nil { panic("ConvertTo() requires non-nil type") @@ -47,7 +47,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type) { // both NativeType, use reflect to assert. // convert go-native to gno type (shallow). *tv = go2GnoValue2(alloc, store, tv.V.(*NativeValue).Value, false) - ConvertTo(alloc, store, tv, t) + ConvertTo(alloc, store, tv, t, isConst) return } } else { @@ -92,6 +92,17 @@ GNO_CASE: tv.T = t // simple conversion. return } + + validate := func(from Kind, to Kind, cmp func() bool) { + if isConst { + msg := fmt.Sprintf("cannot convert constant of type %s to %s\n", from, to) + if cmp != nil && cmp() { + return + } + panic(msg) + } + } + switch tvk { case IntKind: switch k { @@ -100,14 +111,20 @@ GNO_CASE: tv.T = t tv.SetInt(x) case Int8Kind: + validate(IntKind, Int8Kind, func() bool { return tv.GetInt() >= math.MinInt8 && tv.GetInt() <= math.MaxInt8 }) + x := int8(tv.GetInt()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(IntKind, Int16Kind, func() bool { return tv.GetInt() >= math.MinInt16 && tv.GetInt() <= math.MaxInt16 }) + x := int16(tv.GetInt()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(IntKind, Int32Kind, func() bool { return tv.GetInt() >= math.MinInt32 && tv.GetInt() <= math.MaxInt32 }) + x := int32(tv.GetInt()) tv.T = t tv.SetInt32(x) @@ -116,22 +133,32 @@ GNO_CASE: tv.T = t tv.SetInt64(x) case UintKind: + validate(IntKind, UintKind, func() bool { return tv.GetInt() >= 0 }) + x := uint(tv.GetInt()) tv.T = t tv.SetUint(x) case Uint8Kind: + validate(IntKind, Uint8Kind, func() bool { return tv.GetInt() >= 0 && tv.GetInt() <= math.MaxUint8 }) + x := uint8(tv.GetInt()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(IntKind, Uint16Kind, func() bool { return tv.GetInt() >= 0 && tv.GetInt() <= math.MaxUint16 }) + x := uint16(tv.GetInt()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(IntKind, Uint32Kind, func() bool { return tv.GetInt() >= 0 && tv.GetInt() <= math.MaxUint32 }) + x := uint32(tv.GetInt()) tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(IntKind, Uint64Kind, func() bool { return tv.GetInt() >= 0 }) + x := uint64(tv.GetInt()) tv.T = t tv.SetUint64(x) @@ -144,6 +171,7 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(IntKind, StringKind, nil) tv.V = alloc.NewString(string(rune(tv.GetInt()))) tv.T = t tv.ClearNum() @@ -175,22 +203,32 @@ GNO_CASE: tv.T = t tv.SetInt64(x) case UintKind: + validate(Int8Kind, UintKind, func() bool { return tv.GetInt8() >= 0 }) + x := uint(tv.GetInt8()) tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Int8Kind, Uint8Kind, func() bool { return tv.GetInt8() >= 0 }) + x := uint8(tv.GetInt8()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Int8Kind, Uint16Kind, func() bool { return tv.GetInt8() >= 0 }) + x := uint16(tv.GetInt8()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Int8Kind, Uint32Kind, func() bool { return tv.GetInt8() >= 0 }) + x := uint32(tv.GetInt8()) tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(Int8Kind, Uint64Kind, func() bool { return tv.GetInt8() >= 0 }) + x := uint64(tv.GetInt8()) tv.T = t tv.SetUint64(x) @@ -218,6 +256,8 @@ GNO_CASE: tv.T = t tv.SetInt(x) case Int8Kind: + validate(Int16Kind, Int8Kind, func() bool { return tv.GetInt16() >= math.MinInt8 && tv.GetInt16() <= math.MaxInt8 }) + x := int8(tv.GetInt16()) tv.T = t tv.SetInt8(x) @@ -234,22 +274,32 @@ GNO_CASE: tv.T = t tv.SetInt64(x) case UintKind: + validate(Int16Kind, UintKind, func() bool { return tv.GetInt16() >= 0 }) + x := uint(tv.GetInt16()) tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Int16Kind, Uint8Kind, func() bool { return tv.GetInt16() >= 0 && tv.GetInt16() <= math.MaxUint8 }) + x := uint8(tv.GetInt16()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Int16Kind, Uint16Kind, func() bool { return tv.GetInt16() >= 0 }) + x := uint16(tv.GetInt16()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Int16Kind, Uint32Kind, func() bool { return tv.GetInt16() >= 0 }) + x := uint32(tv.GetInt16()) tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(Int16Kind, Uint64Kind, func() bool { return tv.GetInt16() >= 0 }) + x := uint64(tv.GetInt16()) tv.T = t tv.SetUint64(x) @@ -262,6 +312,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Int16Kind, StringKind, nil) + tv.V = alloc.NewString(string(rune(tv.GetInt16()))) tv.T = t tv.ClearNum() @@ -277,10 +329,14 @@ GNO_CASE: tv.T = t tv.SetInt(x) case Int8Kind: + validate(Int32Kind, Int8Kind, func() bool { return tv.GetInt32() >= math.MinInt8 && tv.GetInt32() <= math.MaxInt8 }) + x := int8(tv.GetInt32()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Int32Kind, Int16Kind, func() bool { return tv.GetInt32() >= math.MinInt16 && tv.GetInt32() <= math.MaxInt16 }) + x := int16(tv.GetInt32()) tv.T = t tv.SetInt16(x) @@ -293,22 +349,32 @@ GNO_CASE: tv.T = t tv.SetInt64(x) case UintKind: + validate(Int32Kind, UintKind, func() bool { return tv.GetInt32() >= 0 }) + x := uint(tv.GetInt32()) tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Int32Kind, Uint8Kind, func() bool { return tv.GetInt32() >= 0 && tv.GetInt32() <= math.MaxUint8 }) + x := uint8(tv.GetInt32()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Int32Kind, Uint16Kind, func() bool { return tv.GetInt32() >= 0 && tv.GetInt32() <= math.MaxUint16 }) + x := uint16(tv.GetInt32()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Int32Kind, Uint32Kind, func() bool { return tv.GetInt32() >= 0 }) + x := uint32(tv.GetInt32()) tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(Int32Kind, Uint64Kind, func() bool { return tv.GetInt32() >= 0 }) + x := uint64(tv.GetInt32()) tv.T = t tv.SetUint64(x) @@ -321,6 +387,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Int32Kind, StringKind, nil) + tv.V = alloc.NewString(string(tv.GetInt32())) tv.T = t tv.ClearNum() @@ -336,14 +404,20 @@ GNO_CASE: tv.T = t tv.SetInt(x) case Int8Kind: + validate(Int64Kind, Int8Kind, func() bool { return tv.GetInt64() >= math.MinInt8 && tv.GetInt64() <= math.MaxInt8 }) + x := int8(tv.GetInt64()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Int64Kind, Int16Kind, func() bool { return tv.GetInt64() >= math.MinInt16 && tv.GetInt64() <= math.MaxInt16 }) + x := int16(tv.GetInt64()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Int64Kind, Int32Kind, func() bool { return tv.GetInt64() >= math.MinInt32 && tv.GetInt64() <= math.MaxInt32 }) + x := int32(tv.GetInt64()) tv.T = t tv.SetInt32(x) @@ -352,22 +426,32 @@ GNO_CASE: tv.T = t tv.SetInt64(x) case UintKind: + validate(Int64Kind, UintKind, func() bool { return tv.GetInt64() >= 0 && uint(tv.GetInt64()) <= math.MaxUint }) + x := uint(tv.GetInt64()) tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Int64Kind, Uint8Kind, func() bool { return tv.GetInt64() >= 0 && tv.GetInt64() <= math.MaxUint8 }) + x := uint8(tv.GetInt64()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Int64Kind, Uint16Kind, func() bool { return tv.GetInt64() >= 0 && tv.GetInt64() <= math.MaxUint16 }) + x := uint16(tv.GetInt64()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Int64Kind, Uint32Kind, func() bool { return tv.GetInt64() >= 0 && tv.GetInt64() <= math.MaxUint32 }) + x := uint32(tv.GetInt64()) tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(Int64Kind, Uint64Kind, func() bool { return tv.GetInt64() >= 0 }) + x := uint64(tv.GetInt64()) tv.T = t tv.SetUint64(x) @@ -380,6 +464,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Int64Kind, Uint64Kind, nil) + tv.V = alloc.NewString(string(rune(tv.GetInt64()))) tv.T = t tv.ClearNum() @@ -391,22 +477,32 @@ GNO_CASE: case UintKind: switch k { case IntKind: + validate(UintKind, IntKind, func() bool { return tv.GetUint() <= math.MaxInt }) + x := int(tv.GetUint()) tv.T = t tv.SetInt(x) case Int8Kind: + validate(UintKind, Int8Kind, func() bool { return tv.GetUint() <= math.MaxInt8 }) + x := int8(tv.GetUint()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(UintKind, Int16Kind, func() bool { return tv.GetUint() <= math.MaxInt16 }) + x := int16(tv.GetUint()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(UintKind, Int32Kind, func() bool { return tv.GetUint() <= math.MaxInt32 }) + x := int32(tv.GetUint()) tv.T = t tv.SetInt32(x) case Int64Kind: + validate(UintKind, Int64Kind, func() bool { return tv.GetUint() <= math.MaxInt64 }) + x := int64(tv.GetUint()) tv.T = t tv.SetInt64(x) @@ -415,14 +511,20 @@ GNO_CASE: tv.T = t tv.SetUint(x) case Uint8Kind: + validate(UintKind, Uint8Kind, func() bool { return tv.GetUint() <= math.MaxUint8 }) + x := uint8(tv.GetUint()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(UintKind, Uint16Kind, func() bool { return tv.GetUint() <= math.MaxUint16 }) + x := uint16(tv.GetUint()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(UintKind, Uint32Kind, func() bool { return tv.GetUint() <= math.MaxUint32 }) + x := uint32(tv.GetUint()) tv.T = t tv.SetUint32(x) @@ -439,6 +541,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(UintKind, StringKind, nil) + tv.V = alloc.NewString(string(rune(tv.GetUint()))) tv.T = t tv.ClearNum() @@ -454,18 +558,26 @@ GNO_CASE: tv.T = t tv.SetInt(x) case Int8Kind: + validate(Uint8Kind, Int8Kind, func() bool { return tv.GetUint8() <= math.MaxInt8 }) + x := int8(tv.GetUint8()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Uint8Kind, Int16Kind, func() bool { return int(tv.GetUint8()) <= math.MaxInt16 }) + x := int16(tv.GetUint8()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Uint8Kind, Int32Kind, func() bool { return int(tv.GetUint8()) <= math.MaxInt32 }) + x := int32(tv.GetUint8()) tv.T = t tv.SetInt32(x) case Int64Kind: + validate(Uint8Kind, Int64Kind, func() bool { return int(tv.GetUint8()) <= math.MaxInt64 }) + x := int64(tv.GetUint8()) tv.T = t tv.SetInt64(x) @@ -498,6 +610,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Uint8Kind, StringKind, nil) + tv.V = alloc.NewString(string(rune(tv.GetUint8()))) tv.T = t tv.ClearNum() @@ -513,18 +627,26 @@ GNO_CASE: tv.T = t tv.SetInt(x) case Int8Kind: + validate(Uint16Kind, Int8Kind, func() bool { return tv.GetUint16() <= math.MaxInt8 }) + x := int8(tv.GetUint16()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Uint16Kind, Int16Kind, func() bool { return tv.GetUint16() <= math.MaxInt16 }) + x := int16(tv.GetUint16()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Uint16Kind, Int32Kind, func() bool { return int(tv.GetUint16()) <= math.MaxInt32 }) + x := int32(tv.GetUint16()) tv.T = t tv.SetInt32(x) case Int64Kind: + validate(Uint16Kind, Int64Kind, func() bool { return int(tv.GetUint16()) <= math.MaxInt64 }) + x := int64(tv.GetUint16()) tv.T = t tv.SetInt64(x) @@ -533,6 +655,8 @@ GNO_CASE: tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Uint16Kind, Uint8Kind, func() bool { return int(tv.GetUint16()) <= math.MaxUint8 }) + x := uint8(tv.GetUint16()) tv.T = t tv.SetUint8(x) @@ -557,6 +681,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Uint16Kind, StringKind, nil) + tv.V = alloc.NewString(string(rune(tv.GetUint16()))) tv.T = t tv.ClearNum() @@ -568,18 +694,26 @@ GNO_CASE: case Uint32Kind: switch k { case IntKind: + validate(Uint32Kind, IntKind, func() bool { return int(tv.GetUint32()) <= math.MaxInt }) + x := int(tv.GetUint32()) tv.T = t tv.SetInt(x) case Int8Kind: + validate(Uint32Kind, Int8Kind, func() bool { return int(tv.GetUint32()) <= math.MaxInt8 }) + x := int8(tv.GetUint32()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Uint32Kind, Int16Kind, func() bool { return int(tv.GetUint32()) <= math.MaxInt16 }) + x := int16(tv.GetUint32()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Uint32Kind, Int32Kind, func() bool { return int(tv.GetUint32()) <= math.MaxInt32 }) + x := int32(tv.GetUint32()) tv.T = t tv.SetInt32(x) @@ -592,10 +726,14 @@ GNO_CASE: tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Uint32Kind, Uint8Kind, func() bool { return int(tv.GetUint32()) <= math.MaxUint8 }) + x := uint8(tv.GetUint32()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Uint32Kind, Uint16Kind, func() bool { return int(tv.GetUint32()) <= math.MaxUint16 }) + x := uint16(tv.GetUint32()) tv.T = t tv.SetUint16(x) @@ -616,6 +754,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Uint32Kind, StringKind, nil) + tv.V = alloc.NewString(string(rune(tv.GetUint32()))) tv.T = t tv.ClearNum() @@ -627,38 +767,56 @@ GNO_CASE: case Uint64Kind: switch k { case IntKind: + validate(Uint64Kind, IntKind, func() bool { return int(tv.GetUint64()) <= math.MaxInt }) + x := int(tv.GetUint64()) tv.T = t tv.SetInt(x) case Int8Kind: + validate(Uint64Kind, Int8Kind, func() bool { return int(tv.GetUint64()) <= math.MaxInt8 }) + x := int8(tv.GetUint64()) tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Uint64Kind, Int16Kind, func() bool { return int(tv.GetUint64()) <= math.MaxInt16 }) + x := int16(tv.GetUint64()) tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Uint64Kind, Int32Kind, func() bool { return int(tv.GetUint64()) <= math.MaxInt32 }) + x := int32(tv.GetUint64()) tv.T = t tv.SetInt32(x) case Int64Kind: + validate(Uint64Kind, Int64Kind, func() bool { return int(tv.GetUint64()) <= math.MaxInt64 }) + x := int64(tv.GetUint64()) tv.T = t tv.SetInt64(x) case UintKind: + validate(Uint64Kind, UintKind, func() bool { return tv.GetUint64() <= math.MaxUint }) + x := uint(tv.GetUint64()) tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Uint64Kind, Uint8Kind, func() bool { return int(tv.GetUint64()) <= math.MaxUint8 }) + x := uint8(tv.GetUint64()) tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Uint64Kind, Uint16Kind, func() bool { return int(tv.GetUint64()) <= math.MaxUint16 }) + x := uint16(tv.GetUint64()) tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Uint64Kind, Uint32Kind, func() bool { return int(tv.GetUint64()) <= math.MaxUint32 }) + x := uint32(tv.GetUint64()) tv.T = t tv.SetUint32(x) @@ -675,6 +833,8 @@ GNO_CASE: tv.T = t tv.SetFloat64(x) case StringKind: + validate(Uint64Kind, StringKind, nil) + tv.V = alloc.NewString(string(rune(tv.GetUint64()))) tv.T = t tv.ClearNum() @@ -686,42 +846,148 @@ GNO_CASE: case Float32Kind: switch k { case IntKind: + validate(Float32Kind, IntKind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt && int64(trunc) <= math.MaxInt + }) + x := int(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetInt(x) case Int8Kind: + validate(Float32Kind, Int8Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt8 && int64(trunc) <= math.MaxInt8 + }) + x := int8(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Float32Kind, Int16Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt16 && int64(trunc) <= math.MaxInt16 + }) + x := int16(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Float32Kind, Int32Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt32 && int64(trunc) <= math.MaxInt32 + }) + x := int32(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetInt32(x) case Int64Kind: + validate(Float32Kind, Int64Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + return val == trunc + }) + x := int64(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetInt64(x) case UintKind: + validate(Float32Kind, UintKind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return trunc >= 0 && trunc <= math.MaxUint + }) + x := uint(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Float32Kind, Uint8Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= 0 && int64(trunc) <= math.MaxUint8 + }) + x := uint8(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Float32Kind, Uint16Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= 0 && int64(trunc) <= math.MaxUint16 + }) + x := uint16(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Float32Kind, Uint32Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= 0 && int64(trunc) <= math.MaxUint32 + }) + x := uint32(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(Float32Kind, Uint64Kind, func() bool { + val := float64(tv.GetFloat32()) + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return trunc >= 0 && trunc <= math.MaxUint + }) + x := uint64(tv.GetFloat32()) // XXX determinism? tv.T = t tv.SetUint64(x) @@ -741,46 +1007,156 @@ GNO_CASE: case Float64Kind: switch k { case IntKind: + validate(Float64Kind, IntKind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt && int64(trunc) <= math.MaxInt + }) + x := int(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetInt(x) case Int8Kind: + validate(Float64Kind, Int8Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt8 && int64(trunc) <= math.MaxInt8 + }) + x := int8(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetInt8(x) case Int16Kind: + validate(Float64Kind, Int16Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt16 && int64(trunc) <= math.MaxInt16 + }) + x := int16(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetInt16(x) case Int32Kind: + validate(Float64Kind, Int32Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= math.MinInt32 && int64(trunc) <= math.MaxInt32 + }) + x := int32(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetInt32(x) case Int64Kind: + validate(Float64Kind, Int64Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + return val == trunc + }) + x := int64(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetInt64(x) case UintKind: + validate(Float64Kind, UintKind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return trunc >= 0 && trunc <= math.MaxUint + }) + x := uint(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetUint(x) case Uint8Kind: + validate(Float64Kind, Uint8Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= 0 && int64(trunc) <= math.MaxUint8 + }) + x := uint8(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetUint8(x) case Uint16Kind: + validate(Float64Kind, Uint16Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= 0 && int64(trunc) <= math.MaxUint16 + }) + x := uint16(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetUint16(x) case Uint32Kind: + validate(Float64Kind, Uint32Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return int64(trunc) >= 0 && int64(trunc) <= math.MaxUint32 + }) + x := uint32(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetUint32(x) case Uint64Kind: + validate(Float64Kind, Uint64Kind, func() bool { + val := tv.GetFloat64() + trunc := math.Trunc(val) + + if val != trunc { + return false + } + + return trunc >= 0 && trunc <= math.MaxUint64 + }) + x := uint64(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetUint64(x) case Float32Kind: + validate(Float64Kind, Float32Kind, func() bool { + return tv.GetFloat64() <= math.MaxFloat32 + }) + x := float32(tv.GetFloat64()) // XXX determinism? tv.T = t tv.SetFloat32(x) @@ -923,7 +1299,7 @@ func ConvertUntypedTo(tv *TypedValue, t Type) { ConvertUntypedTo(tv, gnot) // then convert to native value. // NOTE: this should only be called during preprocessing, so no alloc needed. - ConvertTo(nilAllocator, nil, tv, t) + ConvertTo(nilAllocator, nil, tv, t, false) } // special case: simple conversion if t != nil && tv.T.Kind() == t.Kind() { @@ -962,7 +1338,7 @@ func ConvertUntypedTo(tv *TypedValue, t Type) { tv.T = t return } else { - ConvertTo(nilAllocator, nil, tv, t) + ConvertTo(nilAllocator, nil, tv, t, false) } default: panic(fmt.Sprintf( diff --git a/gnovm/tests/files/float1.gno b/gnovm/tests/files/float1.gno index 9eaed64e063..95e130d816e 100644 --- a/gnovm/tests/files/float1.gno +++ b/gnovm/tests/files/float1.gno @@ -1,7 +1,8 @@ package main func main() { - x := int(float64(1.2)) + f := float64(1.2) + x := int(f) println(x) } From c6f8dd4e29df2a35c9b1d88a897b057d662d77b8 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Sun, 24 Nov 2024 03:34:30 +0100 Subject: [PATCH 252/345] fix: const conversions for 32 bits (#3186) The const conversion pr broke 32bit platform build because the comparison wasn't portable for that platform. This is a fix. --- gnovm/pkg/gnolang/values_conversions.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gnovm/pkg/gnolang/values_conversions.go b/gnovm/pkg/gnolang/values_conversions.go index df93144b4e7..baeded76c1a 100644 --- a/gnovm/pkg/gnolang/values_conversions.go +++ b/gnovm/pkg/gnolang/values_conversions.go @@ -151,7 +151,7 @@ GNO_CASE: tv.T = t tv.SetUint16(x) case Uint32Kind: - validate(IntKind, Uint32Kind, func() bool { return tv.GetInt() >= 0 && tv.GetInt() <= math.MaxUint32 }) + validate(IntKind, Uint32Kind, func() bool { return tv.GetInt() >= 0 && uint64(tv.GetInt()) <= math.MaxUint32 }) x := uint32(tv.GetInt()) tv.T = t @@ -501,7 +501,7 @@ GNO_CASE: tv.T = t tv.SetInt32(x) case Int64Kind: - validate(UintKind, Int64Kind, func() bool { return tv.GetUint() <= math.MaxInt64 }) + validate(UintKind, Int64Kind, func() bool { return uint64(tv.GetUint()) <= math.MaxInt64 }) x := int64(tv.GetUint()) tv.T = t @@ -576,7 +576,7 @@ GNO_CASE: tv.T = t tv.SetInt32(x) case Int64Kind: - validate(Uint8Kind, Int64Kind, func() bool { return int(tv.GetUint8()) <= math.MaxInt64 }) + validate(Uint8Kind, Int64Kind, func() bool { return true }) x := int64(tv.GetUint8()) tv.T = t @@ -645,7 +645,7 @@ GNO_CASE: tv.T = t tv.SetInt32(x) case Int64Kind: - validate(Uint16Kind, Int64Kind, func() bool { return int(tv.GetUint16()) <= math.MaxInt64 }) + validate(Uint16Kind, Int64Kind, func() bool { return true }) x := int64(tv.GetUint16()) tv.T = t @@ -791,7 +791,7 @@ GNO_CASE: tv.T = t tv.SetInt32(x) case Int64Kind: - validate(Uint64Kind, Int64Kind, func() bool { return int(tv.GetUint64()) <= math.MaxInt64 }) + validate(Uint64Kind, Int64Kind, func() bool { return tv.GetUint64() <= math.MaxInt64 }) x := int64(tv.GetUint64()) tv.T = t @@ -815,7 +815,7 @@ GNO_CASE: tv.T = t tv.SetUint16(x) case Uint32Kind: - validate(Uint64Kind, Uint32Kind, func() bool { return int(tv.GetUint64()) <= math.MaxUint32 }) + validate(Uint64Kind, Uint32Kind, func() bool { return tv.GetUint64() <= math.MaxUint32 }) x := uint32(tv.GetUint64()) tv.T = t From 2f162b44143ce6aebb8f17a57bc3a66ad2ad4829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 25 Nov 2024 03:58:54 +0100 Subject: [PATCH 253/345] chore: remove ancient docker integration in `misc` (#3172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR removes the ancient docker integration test contained in `misc` that fails, and a random docker compose, also in `misc`. This test package is actually never even run on `master`, because it requires a specific build flag `docker` 🤷‍♂️ Closes #3161
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- .github/workflows/misc.yml | 20 +- go.mod | 2 +- misc/docker-compose/docker-compose.yml | 25 --- misc/docker-integration/Makefile | 2 - misc/docker-integration/README.md | 5 - misc/docker-integration/integration.go | 1 - misc/docker-integration/integration_test.go | 191 -------------------- 7 files changed, 10 insertions(+), 236 deletions(-) delete mode 100644 misc/docker-compose/docker-compose.yml delete mode 100644 misc/docker-integration/Makefile delete mode 100644 misc/docker-integration/README.md delete mode 100644 misc/docker-integration/integration.go delete mode 100644 misc/docker-integration/integration_test.go diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index 859e1429983..ad2c886e2ac 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -12,17 +12,15 @@ on: jobs: main: strategy: - fail-fast: false - matrix: - # fixed list because we have some non go programs on that misc folder - program: - - autocounterd - # - devdeps - - docker-integration - - genproto - - genstd - - goscan - - loop + fail-fast: false + matrix: + # fixed list because we have some non go programs on that misc folder + program: + - autocounterd + - genproto + - genstd + - goscan + - loop name: Run Main uses: ./.github/workflows/main_template.yml with: diff --git a/go.mod b/go.mod index 24d09a87236..f73ba1926e6 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,6 @@ require ( golang.org/x/term v0.23.0 golang.org/x/tools v0.24.0 google.golang.org/protobuf v1.35.1 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -69,4 +68,5 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/misc/docker-compose/docker-compose.yml b/misc/docker-compose/docker-compose.yml deleted file mode 100644 index 470aeaf3127..00000000000 --- a/misc/docker-compose/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: "3.7" -services: - gnonode: - container_name: gnoland-node - build: - context: . - dockerfile: ../..Dockerfile - environment: - - LOG_LEVEL=4 - command: [ "gnoland", "start" ] - volumes: - - "gnonode:/opt/gno/src/gnoland-data" - networks: - - gnonode - restart: on-failure - logging: - driver: "json-file" - options: - max-file: "10" - max-size: "100m" - -networks: - gnonode: {} -volumes: - gnonode: {} diff --git a/misc/docker-integration/Makefile b/misc/docker-integration/Makefile deleted file mode 100644 index cd620d9be9f..00000000000 --- a/misc/docker-integration/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -test: - go test --tags=docker -v . diff --git a/misc/docker-integration/README.md b/misc/docker-integration/README.md deleted file mode 100644 index 21138ed033b..00000000000 --- a/misc/docker-integration/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# docker-integration - -This folder contains tests that runs integration tests inside Docker, using the CLIs. - -We encourage to keep the tests simple and focusing on cross-component testing. diff --git a/misc/docker-integration/integration.go b/misc/docker-integration/integration.go deleted file mode 100644 index 76ab1b7282d..00000000000 --- a/misc/docker-integration/integration.go +++ /dev/null @@ -1 +0,0 @@ -package integration diff --git a/misc/docker-integration/integration_test.go b/misc/docker-integration/integration_test.go deleted file mode 100644 index 973cb386e9b..00000000000 --- a/misc/docker-integration/integration_test.go +++ /dev/null @@ -1,191 +0,0 @@ -//go:build docker - -package integration - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" -) - -const ( - gnolandContainerName = "int_gnoland" - - test1Addr = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" - test1Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" - dockerWaitTimeout = 30 -) - -func TestDockerIntegration(t *testing.T) { - t.Parallel() - - tmpdir, err := os.MkdirTemp(os.TempDir(), "*-gnoland-integration") - require.NoError(t, err) - - checkDocker(t) - cleanupGnoland(t) - buildDockerImage(t) - startGnoland(t) - waitGnoland(t) - - runSuite(t, tmpdir) -} - -func runSuite(t *testing.T, tempdir string) { - t.Helper() - - // add test1 account to docker container keys with "pass" password - dockerExec(t, fmt.Sprintf( - `echo "pass\npass\n%s\n" | gnokey add -recover -insecure-password-stdin test1`, - test1Seed, - )) - // assert test1 account exists - var acc gnoland.GnoAccount - dockerExec_gnokeyQuery(t, "auth/accounts/"+test1Addr, &acc) - require.Equal(t, test1Addr, acc.Address.String(), "test1 account not found") - - // This value is chosen arbitrarily and may not be optimal. - // Feel free to update it to a more suitable amount. - minCoins := std.MustParseCoins(ugnot.ValueString(9990000000000)) - require.True(t, acc.Coins.IsAllGTE(minCoins), - "test1 account coins expected at least %s, got %s", minCoins, acc.Coins) - - // add gno.land/r/demo/tests package as tests_copy - dockerExec(t, - `echo 'pass' | gnokey maketx addpkg -insecure-password-stdin \ - -gas-fee 1000000ugnot -gas-wanted 2000000 \ - -broadcast -chainid dev \ - -pkgdir /opt/gno/src/examples/gno.land/r/demo/tests/ \ - -pkgpath gno.land/r/demo/tests_copy \ - -deposit 100000000ugnot \ - test1`, - ) - // assert gno.land/r/demo/tests_copy has been added - var qfuncs vm.FunctionSignatures - dockerExec_gnokeyQuery(t, `-data "gno.land/r/demo/tests_copy" vm/qfuncs`, &qfuncs) - require.True(t, len(qfuncs) > 0, "gno.land/r/demo/tests_copy not added") - - // broadcast a package TX - dockerExec(t, - `echo 'pass' | gnokey maketx call -insecure-password-stdin \ - -gas-fee 1000000ugnot -gas-wanted 2000000 \ - -broadcast -chainid dev \ - -pkgpath "gno.land/r/demo/tests_copy" -func "InitTestNodes" \ - test1`, - ) -} - -func checkDocker(t *testing.T) { - t.Helper() - output, err := createCommand(t, []string{"docker", "info"}).CombinedOutput() - require.NoError(t, err, "docker daemon not running: %s", string(output)) -} - -func buildDockerImage(t *testing.T) { - t.Helper() - - cmd := createCommand(t, []string{ - "docker", - "build", - "-t", "gno:integration", - filepath.Join("..", ".."), - }) - output, err := cmd.CombinedOutput() - require.NoError(t, err, string(output)) -} - -// dockerExec runs docker exec with cmd as argument -func dockerExec(t *testing.T, cmd string) []byte { - t.Helper() - - cmds := append( - []string{"docker", "exec", gnolandContainerName, "sh", "-c"}, - cmd, - ) - bz, err := createCommand(t, cmds).CombinedOutput() - require.NoError(t, err, string(bz)) - return bz -} - -// dockerExec_gnokeyQuery runs dockerExec with gnokey query prefix and parses -// the command output to out. -func dockerExec_gnokeyQuery(t *testing.T, cmd string, out any) { - t.Helper() - - output := dockerExec(t, "gnokey query "+cmd) - // parses the output of gnokey query: - // height: h - // data: { JSON } - var resp struct { - Height int64 `yaml:"height"` - Data any `yaml:"data"` - } - err := yaml.Unmarshal(output, &resp) - require.NoError(t, err) - bz, err := json.Marshal(resp.Data) - require.NoError(t, err) - err = amino.UnmarshalJSON(bz, out) - require.NoError(t, err) -} - -func createCommand(t *testing.T, args []string) *exec.Cmd { - t.Helper() - msg := strings.Join(args, " ") - t.Log(msg) - return exec.Command(args[0], args[1:]...) -} - -func startGnoland(t *testing.T) { - t.Helper() - - cmd := createCommand(t, []string{ - "docker", "run", - "-d", - "--name", gnolandContainerName, - "-w", "/opt/gno/src/gno.land", - "gno:integration", - "gnoland", - "start", - }) - output, err := cmd.CombinedOutput() - require.NoError(t, err) - require.NotEmpty(t, string(output)) // should be the hash of the container. - - // t.Cleanup(func() { cleanupGnoland(t) }) -} - -func waitGnoland(t *testing.T) { - t.Helper() - t.Log("waiting...") - for i := 0; i < dockerWaitTimeout; i++ { - output, _ := createCommand(t, - []string{"docker", "logs", gnolandContainerName}, - ).CombinedOutput() - if strings.Contains(string(output), "Committed state") { - // ok blockchain is ready - t.Log("gnoland ready") - return - } - time.Sleep(time.Second) - } - // cleanupGnoland(t) - panic("gnoland start timeout") -} - -func cleanupGnoland(t *testing.T) { - t.Helper() - createCommand(t, []string{"docker", "rm", "-f", gnolandContainerName}).Run() -} From a723673b115221f426b34ae51b794995a156933d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 25 Nov 2024 06:47:54 +0100 Subject: [PATCH 254/345] chore: revert "fix(portal-loop): hotfix revert "chore: rename r/manfred -> r/moul (#2820)" (#2865)" (#3024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 69400d468d7bf82ce359eaf7cb092ac545785b10. Revertception.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Miloš Živković --- examples/gno.land/r/demo/foo20/foo20.gno | 2 +- examples/gno.land/r/demo/foo20/foo20_test.gno | 4 +- .../gno.land/r/demo/groups/z_1_a_filetest.gno | 2 +- .../gno.land/r/demo/groups/z_2_a_filetest.gno | 2 +- examples/gno.land/r/demo/users/users.gno | 2 +- .../gno.land/r/demo/users/z_10_filetest.gno | 2 +- .../gno.land/r/demo/users/z_11_filetest.gno | 2 +- .../gno.land/r/demo/users/z_11b_filetest.gno | 2 +- .../gno.land/r/demo/users/z_2_filetest.gno | 2 +- .../gno.land/r/demo/users/z_3_filetest.gno | 2 +- .../gno.land/r/demo/users/z_4_filetest.gno | 2 +- .../gno.land/r/demo/users/z_5_filetest.gno | 2 +- .../gno.land/r/demo/users/z_6_filetest.gno | 2 +- .../gno.land/r/demo/users/z_7_filetest.gno | 2 +- .../gno.land/r/demo/users/z_7b_filetest.gno | 2 +- .../gno.land/r/demo/users/z_8_filetest.gno | 2 +- .../gno.land/r/demo/users/z_9_filetest.gno | 2 +- examples/gno.land/r/gnoland/blog/admin.gno | 2 +- .../gno.land/r/gnoland/blog/gnoblog_test.gno | 16 ++--- examples/gno.land/r/gnoland/home/home.gno | 2 +- .../r/gnoland/home/overide_filetest.gno | 2 +- examples/gno.land/r/gnoland/pages/admin.gno | 2 +- examples/gno.land/r/manfred/config/gno.mod | 1 - examples/gno.land/r/manfred/home/gno.mod | 5 -- examples/gno.land/r/manfred/home/home.gno | 57 +----------------- .../gno.land/r/{manfred => moul}/README.md | 0 .../r/{manfred => moul}/config/config.gno | 2 +- examples/gno.land/r/moul/config/gno.mod | 1 + examples/gno.land/r/moul/home/gno.mod | 6 ++ examples/gno.land/r/moul/home/home.gno | 60 +++++++++++++++++++ .../r/{manfred => moul}/home/z1_filetest.gno | 2 +- .../r/{manfred => moul}/home/z2_filetest.gno | 4 +- .../r/{manfred => moul}/present/admin.gno | 2 +- .../r/{manfred => moul}/present/gno.mod | 2 +- .../present/present_miami23.gno | 0 .../present/present_miami23_filetest.gno | 2 +- .../present/presentations.gno | 2 +- examples/gno.land/r/sys/users/verify.gno | 2 +- .../gnoland/testdata/addpkg_namespace.txtar | 2 +- gno.land/genesis/genesis_balances.txt | 3 +- gno.land/genesis/genesis_txs.jsonl | 16 ++--- gno.land/pkg/gnoweb/gnoweb_test.go | 2 +- .../integration/testdata/adduserfrom.txtar | 4 +- 43 files changed, 121 insertions(+), 114 deletions(-) delete mode 100644 examples/gno.land/r/manfred/config/gno.mod mode change 100644 => 100755 examples/gno.land/r/manfred/home/home.gno rename examples/gno.land/r/{manfred => moul}/README.md (100%) rename examples/gno.land/r/{manfred => moul}/config/config.gno (75%) create mode 100644 examples/gno.land/r/moul/config/gno.mod create mode 100644 examples/gno.land/r/moul/home/gno.mod create mode 100644 examples/gno.land/r/moul/home/home.gno rename examples/gno.land/r/{manfred => moul}/home/z1_filetest.gno (88%) rename examples/gno.land/r/{manfred => moul}/home/z2_filetest.gno (84%) rename examples/gno.land/r/{manfred => moul}/present/admin.gno (97%) rename examples/gno.land/r/{manfred => moul}/present/gno.mod (71%) rename examples/gno.land/r/{manfred => moul}/present/present_miami23.gno (100%) rename examples/gno.land/r/{manfred => moul}/present/present_miami23_filetest.gno (84%) rename examples/gno.land/r/{manfred => moul}/present/presentations.gno (86%) diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index fe099117215..31fa577c515 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -16,7 +16,7 @@ import ( var ( Token, privateLedger = grc20.NewToken("Foo", "FOO", 4) UserTeller = Token.CallerTeller() - owner = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred + owner = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @manfred ) func init() { diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index b3346296b04..b9e80fbb476 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -12,7 +12,7 @@ import ( func TestReadOnlyPublicMethods(t *testing.T) { var ( - admin = pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + admin = pusers.AddressOrName("g1manfred47kzduec920z88wfr64ylksmdcedlf5") alice = pusers.AddressOrName(testutils.TestAddress("alice")) bob = pusers.AddressOrName(testutils.TestAddress("bob")) ) @@ -60,7 +60,7 @@ func TestReadOnlyPublicMethods(t *testing.T) { func TestErrConditions(t *testing.T) { var ( - admin = pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + admin = pusers.AddressOrName("g1manfred47kzduec920z88wfr64ylksmdcedlf5") alice = pusers.AddressOrName(testutils.TestAddress("alice")) empty = pusers.AddressOrName("") ) diff --git a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno index aeff9ab7774..18799e31a67 100644 --- a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno @@ -13,7 +13,7 @@ import ( var gid groups.GroupID -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno index d1cc53d612f..7c97b01ccf5 100644 --- a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno @@ -13,7 +13,7 @@ import ( var gid groups.GroupID -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index daad2e7fc18..1f08c9ae08c 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -16,7 +16,7 @@ import ( // State var ( - admin std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul + admin std.Address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul restricted avl.Tree // Name -> true - restricted name name2User avl.Tree // Name -> *users.User diff --git a/examples/gno.land/r/demo/users/z_10_filetest.gno b/examples/gno.land/r/demo/users/z_10_filetest.gno index 078058c0703..afeecffcc42 100644 --- a/examples/gno.land/r/demo/users/z_10_filetest.gno +++ b/examples/gno.land/r/demo/users/z_10_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func init() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_11_filetest.gno b/examples/gno.land/r/demo/users/z_11_filetest.gno index 603d63f371d..27c7e9813da 100644 --- a/examples/gno.land/r/demo/users/z_11_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_11b_filetest.gno b/examples/gno.land/r/demo/users/z_11b_filetest.gno index 5e661e8f8c1..be508963911 100644 --- a/examples/gno.land/r/demo/users/z_11b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11b_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_2_filetest.gno b/examples/gno.land/r/demo/users/z_2_filetest.gno index 84b62a7e483..c1b92790f8b 100644 --- a/examples/gno.land/r/demo/users/z_2_filetest.gno +++ b/examples/gno.land/r/demo/users/z_2_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_3_filetest.gno b/examples/gno.land/r/demo/users/z_3_filetest.gno index ce34c6bba66..5402235e03d 100644 --- a/examples/gno.land/r/demo/users/z_3_filetest.gno +++ b/examples/gno.land/r/demo/users/z_3_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_4_filetest.gno b/examples/gno.land/r/demo/users/z_4_filetest.gno index 1a46d915c96..613fadf9625 100644 --- a/examples/gno.land/r/demo/users/z_4_filetest.gno +++ b/examples/gno.land/r/demo/users/z_4_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno index 2b3e1b17b5c..dcb957f2155 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_6_filetest.gno b/examples/gno.land/r/demo/users/z_6_filetest.gno index 85305fff1ad..919088088a2 100644 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ b/examples/gno.land/r/demo/users/z_6_filetest.gno @@ -6,7 +6,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() diff --git a/examples/gno.land/r/demo/users/z_7_filetest.gno b/examples/gno.land/r/demo/users/z_7_filetest.gno index 3332ab49af4..1d3c9e3a917 100644 --- a/examples/gno.land/r/demo/users/z_7_filetest.gno +++ b/examples/gno.land/r/demo/users/z_7_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_7b_filetest.gno b/examples/gno.land/r/demo/users/z_7b_filetest.gno index 60a397abe79..09c15bb135d 100644 --- a/examples/gno.land/r/demo/users/z_7b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_7b_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_8_filetest.gno b/examples/gno.land/r/demo/users/z_8_filetest.gno index 1eaa017b7d2..78fada74a71 100644 --- a/examples/gno.land/r/demo/users/z_8_filetest.gno +++ b/examples/gno.land/r/demo/users/z_8_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_9_filetest.gno b/examples/gno.land/r/demo/users/z_9_filetest.gno index 2bd9bf555dc..c73c685aebd 100644 --- a/examples/gno.land/r/demo/users/z_9_filetest.gno +++ b/examples/gno.land/r/demo/users/z_9_filetest.gno @@ -7,7 +7,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 9c94a265fca..87d465449f3 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -18,7 +18,7 @@ var ( func init() { // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" + adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul } func AdminSetAdminAddr(addr std.Address) { diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno index 15688ca4bc7..328fbe2baa4 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno @@ -7,7 +7,7 @@ import ( ) func TestPackage(t *testing.T) { - std.TestSetOrigCaller(std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")) + std.TestSetOrigCaller(std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")) author := std.GetOrigCaller() @@ -59,7 +59,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) Written by moul on 20 May 2022 -Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog ---
Comment section @@ -110,20 +110,20 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) Written by moul on 20 May 2022 -Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog ---
Comment section
comment4 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
---
comment2 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
--- @@ -152,20 +152,20 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4) Written by manfred on 20 May 2022 -Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog ---
Comment section
comment4 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
---
comment2 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
--- diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index 04c549a0d27..6a520dba394 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -17,7 +17,7 @@ import ( var ( override string - admin = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred by default + admin = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul ) func Render(_ string) string { diff --git a/examples/gno.land/r/gnoland/home/overide_filetest.gno b/examples/gno.land/r/gnoland/home/overide_filetest.gno index 4f21b90a3c2..be7e33501d6 100644 --- a/examples/gno.land/r/gnoland/home/overide_filetest.gno +++ b/examples/gno.land/r/gnoland/home/overide_filetest.gno @@ -8,7 +8,7 @@ import ( ) func main() { - std.TestSetOrigCaller("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") home.AdminSetOverride("Hello World!") println(home.Render("")) home.AdminTransferOwnership(testutils.TestAddress("newAdmin")) diff --git a/examples/gno.land/r/gnoland/pages/admin.gno b/examples/gno.land/r/gnoland/pages/admin.gno index ab447e8f604..71050f4ef57 100644 --- a/examples/gno.land/r/gnoland/pages/admin.gno +++ b/examples/gno.land/r/gnoland/pages/admin.gno @@ -15,7 +15,7 @@ var ( func init() { // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" + adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul } func AdminSetAdminAddr(addr std.Address) { diff --git a/examples/gno.land/r/manfred/config/gno.mod b/examples/gno.land/r/manfred/config/gno.mod deleted file mode 100644 index 516bf38528e..00000000000 --- a/examples/gno.land/r/manfred/config/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/manfred/config diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod index 0ef23834fb5..2efefe1824f 100644 --- a/examples/gno.land/r/manfred/home/gno.mod +++ b/examples/gno.land/r/manfred/home/gno.mod @@ -1,6 +1 @@ module gno.land/r/manfred/home - -require ( - gno.land/r/leon/hof v0.0.0-latest - gno.land/r/manfred/config v0.0.0-latest -) diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno old mode 100644 new mode 100755 index 3e29636439d..56caf30d9fd --- a/examples/gno.land/r/manfred/home/home.gno +++ b/examples/gno.land/r/manfred/home/home.gno @@ -1,60 +1,5 @@ package home -import ( - "gno.land/r/leon/hof" - "gno.land/r/manfred/config" -) - -var ( - todos []string - status string - memeImgURL string -) - -func init() { - todos = append(todos, "fill this todo list...") - status = "Online" // Initial status set to "Online" - memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" - hof.Register() -} - func Render(path string) string { - content := "# Manfred's (gn)home Dashboard\n\n" - - content += "## Meme\n" - content += "![](" + memeImgURL + ")\n\n" - - content += "## Status\n" - content += status + "\n\n" - - content += "## Personal ToDo List\n" - for _, todo := range todos { - content += "- [ ] " + todo + "\n" - } - content += "\n" - - // TODO: Implement a feature to list replies on r/boards on my posts - // TODO: Maybe integrate a calendar feature for upcoming events? - - return content -} - -func AddNewTodo(todo string) { - config.AssertIsAdmin() - todos = append(todos, todo) -} - -func DeleteTodo(todoIndex int) { - config.AssertIsAdmin() - if todoIndex >= 0 && todoIndex < len(todos) { - // Remove the todo from the list by merging slices from before and after the todo - todos = append(todos[:todoIndex], todos[todoIndex+1:]...) - } else { - panic("Invalid todo index") - } -} - -func UpdateStatus(newStatus string) { - config.AssertIsAdmin() - status = newStatus + return "Moved to r/moul" } diff --git a/examples/gno.land/r/manfred/README.md b/examples/gno.land/r/moul/README.md similarity index 100% rename from examples/gno.land/r/manfred/README.md rename to examples/gno.land/r/moul/README.md diff --git a/examples/gno.land/r/manfred/config/config.gno b/examples/gno.land/r/moul/config/config.gno similarity index 75% rename from examples/gno.land/r/manfred/config/config.gno rename to examples/gno.land/r/moul/config/config.gno index 23e90df50ff..a4f24411747 100644 --- a/examples/gno.land/r/manfred/config/config.gno +++ b/examples/gno.land/r/moul/config/config.gno @@ -2,7 +2,7 @@ package config import "std" -var addr = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +var addr = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul func Addr() std.Address { return addr diff --git a/examples/gno.land/r/moul/config/gno.mod b/examples/gno.land/r/moul/config/gno.mod new file mode 100644 index 00000000000..2029efc8fcb --- /dev/null +++ b/examples/gno.land/r/moul/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/moul/config diff --git a/examples/gno.land/r/moul/home/gno.mod b/examples/gno.land/r/moul/home/gno.mod new file mode 100644 index 00000000000..f42a2c2ced8 --- /dev/null +++ b/examples/gno.land/r/moul/home/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/moul/home + +require ( + gno.land/r/leon/hof v0.0.0-latest + gno.land/r/moul/config v0.0.0-latest +) diff --git a/examples/gno.land/r/moul/home/home.gno b/examples/gno.land/r/moul/home/home.gno new file mode 100644 index 00000000000..140e7b5e0c8 --- /dev/null +++ b/examples/gno.land/r/moul/home/home.gno @@ -0,0 +1,60 @@ +package home + +import ( + "gno.land/r/leon/hof" + "gno.land/r/moul/config" +) + +var ( + todos []string + status string + memeImgURL string +) + +func init() { + todos = append(todos, "fill this todo list...") + status = "Online" // Initial status set to "Online" + memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" + hof.Register() +} + +func Render(path string) string { + content := "# Manfred's (gn)home Dashboard\n\n" + + content += "## Meme\n" + content += "![](" + memeImgURL + ")\n\n" + + content += "## Status\n" + content += status + "\n\n" + + content += "## Personal ToDo List\n" + for _, todo := range todos { + content += "- [ ] " + todo + "\n" + } + content += "\n" + + // TODO: Implement a feature to list replies on r/boards on my posts + // TODO: Maybe integrate a calendar feature for upcoming events? + + return content +} + +func AddNewTodo(todo string) { + config.AssertIsAdmin() + todos = append(todos, todo) +} + +func DeleteTodo(todoIndex int) { + config.AssertIsAdmin() + if todoIndex >= 0 && todoIndex < len(todos) { + // Remove the todo from the list by merging slices from before and after the todo + todos = append(todos[:todoIndex], todos[todoIndex+1:]...) + } else { + panic("Invalid todo index") + } +} + +func UpdateStatus(newStatus string) { + config.AssertIsAdmin() + status = newStatus +} diff --git a/examples/gno.land/r/manfred/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno similarity index 88% rename from examples/gno.land/r/manfred/home/z1_filetest.gno rename to examples/gno.land/r/moul/home/z1_filetest.gno index 801efedb306..5203e07ada7 100644 --- a/examples/gno.land/r/manfred/home/z1_filetest.gno +++ b/examples/gno.land/r/moul/home/z1_filetest.gno @@ -1,6 +1,6 @@ package main -import "gno.land/r/manfred/home" +import "gno.land/r/moul/home" func main() { println(home.Render("")) diff --git a/examples/gno.land/r/manfred/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno similarity index 84% rename from examples/gno.land/r/manfred/home/z2_filetest.gno rename to examples/gno.land/r/moul/home/z2_filetest.gno index 316fd400867..02d08cd591e 100644 --- a/examples/gno.land/r/manfred/home/z2_filetest.gno +++ b/examples/gno.land/r/moul/home/z2_filetest.gno @@ -3,11 +3,11 @@ package main import ( "std" - "gno.land/r/manfred/home" + "gno.land/r/moul/home" ) func main() { - std.TestSetOrigCaller("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") home.AddNewTodo("aaa") home.AddNewTodo("bbb") home.AddNewTodo("ccc") diff --git a/examples/gno.land/r/manfred/present/admin.gno b/examples/gno.land/r/moul/present/admin.gno similarity index 97% rename from examples/gno.land/r/manfred/present/admin.gno rename to examples/gno.land/r/moul/present/admin.gno index 60af578b54f..ab99b1725c5 100644 --- a/examples/gno.land/r/manfred/present/admin.gno +++ b/examples/gno.land/r/moul/present/admin.gno @@ -15,7 +15,7 @@ var ( func init() { // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" + adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" } func AdminSetAdminAddr(addr std.Address) { diff --git a/examples/gno.land/r/manfred/present/gno.mod b/examples/gno.land/r/moul/present/gno.mod similarity index 71% rename from examples/gno.land/r/manfred/present/gno.mod rename to examples/gno.land/r/moul/present/gno.mod index 5d50447e0e0..3ae0bf2e64d 100644 --- a/examples/gno.land/r/manfred/present/gno.mod +++ b/examples/gno.land/r/moul/present/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/manfred/present +module gno.land/r/moul/present require ( gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/manfred/present/present_miami23.gno b/examples/gno.land/r/moul/present/present_miami23.gno similarity index 100% rename from examples/gno.land/r/manfred/present/present_miami23.gno rename to examples/gno.land/r/moul/present/present_miami23.gno diff --git a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno b/examples/gno.land/r/moul/present/present_miami23_filetest.gno similarity index 84% rename from examples/gno.land/r/manfred/present/present_miami23_filetest.gno rename to examples/gno.land/r/moul/present/present_miami23_filetest.gno index ac19d83ade4..09d332ec6e4 100644 --- a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno +++ b/examples/gno.land/r/moul/present/present_miami23_filetest.gno @@ -1,7 +1,7 @@ package main import ( - "gno.land/r/manfred/present" + "gno.land/r/moul/present" ) func main() { diff --git a/examples/gno.land/r/manfred/present/presentations.gno b/examples/gno.land/r/moul/present/presentations.gno similarity index 86% rename from examples/gno.land/r/manfred/present/presentations.gno rename to examples/gno.land/r/moul/present/presentations.gno index 8a99f502e86..c5529804751 100644 --- a/examples/gno.land/r/manfred/present/presentations.gno +++ b/examples/gno.land/r/moul/present/presentations.gno @@ -8,7 +8,7 @@ import ( var b = &blog.Blog{ Title: "Manfred's Presentations", - Prefix: "/r/manfred/present:", + Prefix: "/r/moul/present:", NoBreadcrumb: true, } diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno index 852626622e4..a836e84683d 100644 --- a/examples/gno.land/r/sys/users/verify.gno +++ b/examples/gno.land/r/sys/users/verify.gno @@ -7,7 +7,7 @@ import ( "gno.land/r/demo/users" ) -const admin = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul +const admin = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul type VerifyNameFunc func(enabled bool, address std.Address, name string) bool diff --git a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar index 5a88fd6d603..d207289e0ff 100644 --- a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar +++ b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar @@ -4,7 +4,7 @@ loadpkg gno.land/r/sys/users adduser admin adduser gui -patchpkg "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" $USER_ADDR_admin # use our custom admin +patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $USER_ADDR_admin # use our custom admin gnoland start diff --git a/gno.land/genesis/genesis_balances.txt b/gno.land/genesis/genesis_balances.txt index fa3232149c1..c372d7f9fd7 100644 --- a/gno.land/genesis/genesis_balances.txt +++ b/gno.land/genesis/genesis_balances.txt @@ -16,7 +16,8 @@ g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=1000000000000ugnot # faucet3 (devx) g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=1000000000000ugnot # faucet4 (adena) # Contributors premine & GitHub requests (closed). -g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot # @moul +g1manfred47kzduec920z88wfr64ylksmdcedlf5=10000000000ugnot # @moul +g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot # @manfred g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa=10000000000ugnot # @piux2 g15gdm49ktawvkrl88jadqpucng37yxutucuwaef=10000000000ugnot # @chadwick g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s=10000000000ugnot # @mefodica #83 diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl index fa2c9e83fbd..9027d51c0ac 100644 --- a/gno.land/genesis/genesis_txs.jsonl +++ b/gno.land/genesis/genesis_txs.jsonl @@ -1,17 +1,17 @@ -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","administrator","g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"AqCqe0cS55Ym7/BvPDoCDyPP5q8284gecVQ2PMOlq/4lJpO9Q18SOWKI15dMEBY1pT0AYyhCeTirlsM1I3Y4Cg=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","zo_oma","Love is the encryption key\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yg5/iiktruezVw5vZJwLlGwyrvw8RlqOToTRMWXkE2"},"signature":"GGp+bVL2eEvKecPqgcULSABYOSnSMnJzfIsR8ZIRER1GGX/fOiCReX4WKMrGLVROJVfbLQkDRwvhS4TLHlSoSQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","manfred","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","moul","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","piupiu","@piux2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar68lqbU2YC63fbMcYUtJhYO3/66APM/EqF7m0nUjGyz"},"signature":"pTUpP0d/XlfVe3TH1hlaoLhKadzIKG1gtQ/Ueuat72p+659RWRea58Z0mk6GgPE/EeTbhMEY45zufevBdGJVoQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","anarcher","https://twitter.com/anarcher"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":"pf5xm8oWIQIOEwSGw4icPmynLXb1P1HxKfjeh8UStU1mlIBPKa7yppeIMPpAflC0o2zjFR7Axe7CimAebm3BHg=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","anarcher","https://twitter.com/anarcher"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":"pf5xm8oWIQIOEwSGw4icPmynLXb1P1HxKfjeh8UStU1mlIBPKa7yppeIMPpAflC0o2zjFR7Axe7CimAebm3BHg=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","ideamour","\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhClx4AsDuX3DNCPxhDwWnrfd4MIZmxJE4vt47ClVvT2"},"signature":"IQe64af878k6HjLDqIJeg27GXAVF6xS+96cDe2jMlxNV6+8sOcuUctp0GiWVnYfN4tpthC6d4WhBo+VlpHqkbg=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateBoard","args":["testboard"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"vzlSxEFh5jOkaSdv3rsV91v/OJKEF2qSuoCpri1u5tRWq62T7xr3KHRCF5qFnn4aQX/yE8g8f/Y//WPOCUGhJw=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Hello World","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm \nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n## Starting the `gnoland` node node/validator.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### build gnoland.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake \n```\n\n### add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mnemonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### start gnoland validator node.\n\n```bash\n./build/gnoland\n```\n\n(This can be reset with `make reset`).\n\n### start gnoland web server (optional).\n\n```bash\ngo run ./gnoland/website\n```\n\n## Signing and broadcasting transactions.\n\n### publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 2000000 \u003e addpkg.avl.unsigned.txt\n./build/gnokey query \"auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n./build/gnokey sign test1 --txpath addpkg.avl.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 0 \u003e addpkg.avl.signed.txt\n./build/gnokey broadcast addpkg.avl.signed.txt --remote %%REMOTE%%\n```\n\n### publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 300000000 \u003e addpkg.boards.unsigned.txt\n./build/gnokey sign test1 --txpath addpkg.boards.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 1 \u003e addpkg.boards.signed.txt\n./build/gnokey broadcast addpkg.boards.signed.txt --remote %%REMOTE%%\n```\n\n### create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateBoard --args \"testboard\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createboard.unsigned.txt\n./build/gnokey sign test1 --txpath createboard.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 2 \u003e createboard.signed.txt\n./build/gnokey broadcast createboard.signed.txt --remote %%REMOTE%%\n```\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"testboard\\\")\"\n```\n\n### create a post of a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreatePost --args 1 --args \"Hello World\" --args#file \"./examples/gno.land/r/demo/boards/README.md\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createpost.unsigned.txt\n./build/gnokey sign test1 --txpath createpost.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 3 \u003e createpost.signed.txt\n./build/gnokey broadcast createpost.signed.txt --remote %%REMOTE%%\n```\n\n### create a comment to a post.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateReply --args 1 --args 1 --args \"A comment\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createcomment.unsigned.txt\n./build/gnokey sign test1 --txpath createcomment.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 4 \u003e createcomment.signed.txt\n./build/gnokey broadcast createcomment.signed.txt --remote %%REMOTE%%\n```\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard/1\"\n```\n\n### render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:testboard` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard\"\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"V43B1waFxhzheW9TfmCpjLdrC4dC1yjUGES5y3J6QsNar6hRpNz4G1thzWmWK7xXhg8u1PCIpxLxGczKQYhuPw=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n - [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n - [gno.land/r/demo/nft/nft.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"ZXfrTiHxPFQL8uSm+Tv7WXIHPMca9okhm94RAlC6YgNbB1VHQYYpoP4w+cnL3YskVzGrOZxensXa9CAZ+cNNeg=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""}} {"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} -{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/gnoweb_test.go b/gno.land/pkg/gnoweb/gnoweb_test.go index 8c8bcca48f5..99eb86ea07e 100644 --- a/gno.land/pkg/gnoweb/gnoweb_test.go +++ b/gno.land/pkg/gnoweb/gnoweb_test.go @@ -34,7 +34,7 @@ func TestRoutes(t *testing.T) { {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `input type="text" value="foo/bar"`}, {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, {"/r/demo/users:administrator", ok, "address"}, - {"/r/demo/users", ok, "manfred"}, + {"/r/demo/users", ok, "moul"}, {"/r/demo/users/users.gno", ok, "// State"}, {"/r/demo/deep/very/deep", ok, "it works!"}, {"/r/demo/deep/very/deep?arg1=val1&arg2=val2", ok, "hi ?arg1=val1&arg2=val2"}, diff --git a/gno.land/pkg/integration/testdata/adduserfrom.txtar b/gno.land/pkg/integration/testdata/adduserfrom.txtar index a23849aa604..47ec70b00e6 100644 --- a/gno.land/pkg/integration/testdata/adduserfrom.txtar +++ b/gno.land/pkg/integration/testdata/adduserfrom.txtar @@ -27,8 +27,8 @@ stdout ' "BaseAccount": {' stdout ' "address": "g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp",' stdout ' "coins": "10000000ugnot",' stdout ' "public_key": null,' -stdout ' "account_number": "58",' +stdout ' "account_number": "59",' stdout ' "sequence": "0"' stdout ' }' stdout '}' -! stderr '.+' # empty \ No newline at end of file +! stderr '.+' # empty From 30aac2a81882452146807c559ff37aa6b9e2ca5a Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Mon, 25 Nov 2024 01:45:13 -0800 Subject: [PATCH 255/345] feat(gnovm): returns error like in Golang when assignment with a function which does not return any value (#3049) This PR aims at resolving this issue: https://github.com/gnolang/gno/issues/1082 This depends on https://github.com/gnolang/gno/pull/3017 because it refactored the code to sync the logic/code between AssignStmt vs ValueDecl. Related https://github.com/gnolang/gno/pull/2695
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: hieu.ha Co-authored-by: Mikael VALLENET --- gnovm/pkg/gnolang/preprocess.go | 20 +++++++------------- gnovm/pkg/gnolang/type_check.go | 8 ++++++++ gnovm/tests/files/assign37.gno | 10 ++++++++++ gnovm/tests/files/assign37b.gno | 12 ++++++++++++ gnovm/tests/files/var34.gno | 10 ++++++++++ gnovm/tests/files/var34b.gno | 11 +++++++++++ gnovm/tests/files/var34c.gno | 10 ++++++++++ 7 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 gnovm/tests/files/assign37.gno create mode 100644 gnovm/tests/files/assign37b.gno create mode 100644 gnovm/tests/files/var34.gno create mode 100644 gnovm/tests/files/var34b.gno create mode 100644 gnovm/tests/files/var34c.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 85a846535ce..b9eb756512e 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2373,7 +2373,7 @@ func defineOrDecl( sts := make([]Type, numNames) // static types tvs := make([]TypedValue, numNames) - if numNames > 1 && len(valueExprs) == 1 { + if numVals == 1 && numNames > 1 { parseMultipleAssignFromOneExpr(sts, tvs, store, bn, nameExprs, typeExpr, valueExprs[0]) } else { parseAssignFromExprList(sts, tvs, store, bn, isConst, nameExprs, typeExpr, valueExprs) @@ -2410,7 +2410,7 @@ func parseAssignFromExprList( for _, v := range valueExprs { if cx, ok := v.(*CallExpr); ok { tt, ok := evalStaticTypeOfRaw(store, bn, cx).(*tupleType) - if ok && len(tt.Elts) != 1 { + if ok && len(tt.Elts) > 1 { panic(fmt.Sprintf("multiple-value %s (value of type %s) in single-value context", cx.Func.String(), tt.Elts)) } } @@ -3132,18 +3132,12 @@ func gnoTypeOf(store Store, t Type) Type { // but rather computes the type OF x. func evalStaticTypeOf(store Store, last BlockNode, x Expr) Type { t := evalStaticTypeOfRaw(store, last, x) - if tt, ok := t.(*tupleType); ok { - if len(tt.Elts) != 1 { - panic(fmt.Sprintf( - "evalStaticTypeOf() only supports *CallExpr with 1 result, got %s", - tt.String(), - )) - } else { - return tt.Elts[0] - } - } else { - return t + + if tt, ok := t.(*tupleType); ok && len(tt.Elts) == 1 { + return tt.Elts[0] } + + return t } // like evalStaticTypeOf() but returns the raw *tupleType for *CallExpr. diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index c62d67375ee..e786bed683f 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -1029,6 +1029,14 @@ func assertValidAssignRhs(store Store, last BlockNode, n Node) { tt = evalStaticType(store, last, exp) panic(fmt.Sprintf("%s (type) is not an expression", tt.String())) } + + // Ensures that function used in ValueDecl or AssignStmt must return at least 1 value. + if cx, ok := exp.(*CallExpr); ok { + tType, ok := tt.(*tupleType) + if ok && len(tType.Elts) == 0 { + panic(fmt.Sprintf("%s (no value) used as value", cx.Func.String())) + } + } } } diff --git a/gnovm/tests/files/assign37.gno b/gnovm/tests/files/assign37.gno new file mode 100644 index 00000000000..d96aab42070 --- /dev/null +++ b/gnovm/tests/files/assign37.gno @@ -0,0 +1,10 @@ +package main + +func f() { } + +func main() { + a := f() +} + +// Error: +// main/files/assign37.gno:6:2: f (no value) used as value diff --git a/gnovm/tests/files/assign37b.gno b/gnovm/tests/files/assign37b.gno new file mode 100644 index 00000000000..42e3dbe6e1d --- /dev/null +++ b/gnovm/tests/files/assign37b.gno @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func f() { } + +func main() { + a, b := f(), f() +} + +// Error: +// main/files/assign37b.gno:8:2: f (no value) used as value diff --git a/gnovm/tests/files/var34.gno b/gnovm/tests/files/var34.gno new file mode 100644 index 00000000000..4f1dff6183f --- /dev/null +++ b/gnovm/tests/files/var34.gno @@ -0,0 +1,10 @@ +package main + +func f() {} + +func main() { + var t = f() +} + +// Error: +// main/files/var34.gno:6:6: f (no value) used as value diff --git a/gnovm/tests/files/var34b.gno b/gnovm/tests/files/var34b.gno new file mode 100644 index 00000000000..21e8a89bb14 --- /dev/null +++ b/gnovm/tests/files/var34b.gno @@ -0,0 +1,11 @@ +package main + +func f() {} + +func main() { + var a int + a = f() +} + +// Error: +// main/files/var34b.gno:7:2: f (no value) used as value diff --git a/gnovm/tests/files/var34c.gno b/gnovm/tests/files/var34c.gno new file mode 100644 index 00000000000..18ab996eefd --- /dev/null +++ b/gnovm/tests/files/var34c.gno @@ -0,0 +1,10 @@ +package main + +func f() {} + +func main() { + var a, b int = f(), 1 +} + +// Error: +// main/files/var34c.gno:6:6: f (no value) used as value From f27b182702d278d79d8b1f95d06913a20f4f5f62 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Mon, 25 Nov 2024 11:25:50 +0100 Subject: [PATCH 256/345] fix: bit shifting const expr overflow (#3192) This provides validation for cases where bit shifting const expressions overflow. Closes #3128 --------- Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/machine.go | 44 ++--- gnovm/pkg/gnolang/op_assign.go | 4 +- gnovm/pkg/gnolang/op_binary.go | 177 ++++++++++++++++++- gnovm/pkg/gnolang/preprocess.go | 3 + gnovm/pkg/gnolang/values_conversions_test.go | 132 ++++++++++++++ 5 files changed, 332 insertions(+), 28 deletions(-) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index aac8b4f5802..e341ef8e9f1 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -67,13 +67,13 @@ type Machine struct { Debugger Debugger // Configuration - CheckTypes bool // not yet used - ReadOnly bool - MaxCycles int64 - Output io.Writer - Store Store - Context interface{} - GasMeter store.GasMeter + PreprocessorMode bool // this is used as a flag when const values are evaluated during preprocessing + ReadOnly bool + MaxCycles int64 + Output io.Writer + Store Store + Context interface{} + GasMeter store.GasMeter // PanicScope is incremented each time a panic occurs and is reset to // zero when it is recovered. PanicScope uint @@ -103,18 +103,18 @@ func NewMachine(pkgPath string, store Store) *Machine { // MachineOptions is used to pass options to [NewMachineWithOptions]. type MachineOptions struct { // Active package of the given machine; must be set before execution. - PkgPath string - CheckTypes bool // not yet used - ReadOnly bool - Debug bool - Input io.Reader // used for default debugger input only - Output io.Writer // default os.Stdout - Store Store // default NewStore(Alloc, nil, nil) - Context interface{} - Alloc *Allocator // or see MaxAllocBytes. - MaxAllocBytes int64 // or 0 for no limit. - MaxCycles int64 // or 0 for no limit. - GasMeter store.GasMeter + PkgPath string + PreprocessorMode bool + ReadOnly bool + Debug bool + Input io.Reader // used for default debugger input only + Output io.Writer // default os.Stdout + Store Store // default NewStore(Alloc, nil, nil) + Context interface{} + Alloc *Allocator // or see MaxAllocBytes. + MaxAllocBytes int64 // or 0 for no limit. + MaxCycles int64 // or 0 for no limit. + GasMeter store.GasMeter } // the machine constructor gets spammed @@ -136,7 +136,7 @@ var machinePool = sync.Pool{ // Machines initialized through this constructor must be finalized with // [Machine.Release]. func NewMachineWithOptions(opts MachineOptions) *Machine { - checkTypes := opts.CheckTypes + preprocessorMode := opts.PreprocessorMode readOnly := opts.ReadOnly maxCycles := opts.MaxCycles vmGasMeter := opts.GasMeter @@ -169,7 +169,7 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm := machinePool.Get().(*Machine) mm.Package = pv mm.Alloc = alloc - mm.CheckTypes = checkTypes + mm.PreprocessorMode = preprocessorMode mm.ReadOnly = readOnly mm.MaxCycles = maxCycles mm.Output = output @@ -2249,7 +2249,7 @@ func (m *Machine) String() string { var builder strings.Builder builder.Grow(totalLength) - builder.WriteString(fmt.Sprintf("Machine:\n CheckTypes: %v\n Op: %v\n Values: (len: %d)\n", m.CheckTypes, m.Ops[:m.NumOps], m.NumValues)) + builder.WriteString(fmt.Sprintf("Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues)) for i := m.NumValues - 1; i >= 0; i-- { builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Values[i])) diff --git a/gnovm/pkg/gnolang/op_assign.go b/gnovm/pkg/gnolang/op_assign.go index 114c8c589c4..5e841fb18fd 100644 --- a/gnovm/pkg/gnolang/op_assign.go +++ b/gnovm/pkg/gnolang/op_assign.go @@ -274,7 +274,7 @@ func (m *Machine) doOpShlAssign() { } } // lv <<= rv - shlAssign(lv.TV, rv) + shlAssign(m, lv.TV, rv) if lv.Base != nil { m.Realm.DidUpdate(lv.Base.(Object), nil, nil) } @@ -294,7 +294,7 @@ func (m *Machine) doOpShrAssign() { } } // lv >>= rv - shrAssign(lv.TV, rv) + shrAssign(m, lv.TV, rv) if lv.Base != nil { m.Realm.DidUpdate(lv.Base.(Object), nil, nil) } diff --git a/gnovm/pkg/gnolang/op_binary.go b/gnovm/pkg/gnolang/op_binary.go index a541a7da8b5..6d26fa7ce54 100644 --- a/gnovm/pkg/gnolang/op_binary.go +++ b/gnovm/pkg/gnolang/op_binary.go @@ -2,6 +2,7 @@ package gnolang import ( "fmt" + "math" "math/big" "github.com/cockroachdb/apd/v3" @@ -288,7 +289,7 @@ func (m *Machine) doOpShl() { } // lv << rv - shlAssign(lv, rv) + shlAssign(m, lv, rv) } func (m *Machine) doOpShr() { @@ -304,7 +305,7 @@ func (m *Machine) doOpShr() { } // lv >> rv - shrAssign(lv, rv) + shrAssign(m, lv, rv) } func (m *Machine) doOpBand() { @@ -1196,32 +1197,116 @@ func xorAssign(lv, rv *TypedValue) { } // for doOpShl and doOpShlAssign. -func shlAssign(lv, rv *TypedValue) { +func shlAssign(m *Machine, lv, rv *TypedValue) { rv.AssertNonNegative("runtime error: negative shift amount") + + checkOverflow := func(v func() bool) { + if m.PreprocessorMode && !v() { + panic(`constant overflows`) + } + } + // set the result in lv. // NOTE: baseOf(rv.T) is always UintType. switch baseOf(lv.T) { case IntType: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt)) != 1 + }) + lv.SetInt(lv.GetInt() << rv.GetUint()) case Int8Type: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt8())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt8)) != 1 + }) + lv.SetInt8(lv.GetInt8() << rv.GetUint()) case Int16Type: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt16())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt16)) != 1 + }) + lv.SetInt16(lv.GetInt16() << rv.GetUint()) case Int32Type, UntypedRuneType: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt32())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt32)) != 1 + }) + lv.SetInt32(lv.GetInt32() << rv.GetUint()) case Int64Type: + checkOverflow(func() bool { + l := big.NewInt(lv.GetInt64()) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt64)) != 1 + }) + lv.SetInt64(lv.GetInt64() << rv.GetUint()) case UintType: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(0).SetUint64(math.MaxUint)) != 1 + }) + lv.SetUint(lv.GetUint() << rv.GetUint()) case Uint8Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint8())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint8)) != 1 + }) + lv.SetUint8(lv.GetUint8() << rv.GetUint()) case DataByteType: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetDataByte())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint8)) != 1 + }) + lv.SetDataByte(lv.GetDataByte() << rv.GetUint()) case Uint16Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint16())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint16)) != 1 + }) + lv.SetUint16(lv.GetUint16() << rv.GetUint()) case Uint32Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint32())) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint32)) != 1 + }) + lv.SetUint32(lv.GetUint32() << rv.GetUint()) case Uint64Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(lv.GetUint64()) + r := big.NewInt(0).Lsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(0).SetUint64(math.MaxUint64)) != 1 + }) + lv.SetUint64(lv.GetUint64() << rv.GetUint()) case BigintType, UntypedBigintType: lb := lv.GetBigInt() @@ -1236,32 +1321,116 @@ func shlAssign(lv, rv *TypedValue) { } // for doOpShr and doOpShrAssign. -func shrAssign(lv, rv *TypedValue) { +func shrAssign(m *Machine, lv, rv *TypedValue) { rv.AssertNonNegative("runtime error: negative shift amount") + + checkOverflow := func(v func() bool) { + if m.PreprocessorMode && !v() { + panic(`constant overflows`) + } + } + // set the result in lv. // NOTE: baseOf(rv.T) is always UintType. switch baseOf(lv.T) { case IntType: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt)) != 1 + }) + lv.SetInt(lv.GetInt() >> rv.GetUint()) case Int8Type: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt8())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt8)) != 1 + }) + lv.SetInt8(lv.GetInt8() >> rv.GetUint()) case Int16Type: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt16())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt16)) != 1 + }) + lv.SetInt16(lv.GetInt16() >> rv.GetUint()) case Int32Type, UntypedRuneType: + checkOverflow(func() bool { + l := big.NewInt(int64(lv.GetInt32())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt32)) != 1 + }) + lv.SetInt32(lv.GetInt32() >> rv.GetUint()) case Int64Type: + checkOverflow(func() bool { + l := big.NewInt(lv.GetInt64()) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxInt64)) != 1 + }) + lv.SetInt64(lv.GetInt64() >> rv.GetUint()) case UintType: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(0).SetUint64(math.MaxUint)) != 1 + }) + lv.SetUint(lv.GetUint() >> rv.GetUint()) case Uint8Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint8())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint8)) != 1 + }) + lv.SetUint8(lv.GetUint8() >> rv.GetUint()) case DataByteType: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetDataByte())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint8)) != 1 + }) + lv.SetDataByte(lv.GetDataByte() >> rv.GetUint()) case Uint16Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint16())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint16)) != 1 + }) + lv.SetUint16(lv.GetUint16() >> rv.GetUint()) case Uint32Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(uint64(lv.GetUint32())) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(math.MaxUint32)) != 1 + }) + lv.SetUint32(lv.GetUint32() >> rv.GetUint()) case Uint64Type: + checkOverflow(func() bool { + l := big.NewInt(0).SetUint64(lv.GetUint64()) + r := big.NewInt(0).Rsh(l, rv.GetUint()) + + return r.Cmp(big.NewInt(0).SetUint64(math.MaxUint64)) != 1 + }) + lv.SetUint64(lv.GetUint64() >> rv.GetUint()) case BigintType, UntypedBigintType: lb := lv.GetBigInt() diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index b9eb756512e..7198d4f6a98 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3258,7 +3258,10 @@ func evalConst(store Store, last BlockNode, x Expr) *ConstExpr { // TODO: some check or verification for ensuring x // is constant? From the machine? m := NewMachine(".dontcare", store) + m.PreprocessorMode = true + cv := m.EvalStatic(last, x) + m.PreprocessorMode = false m.Release() cx := &ConstExpr{ Source: x, diff --git a/gnovm/pkg/gnolang/values_conversions_test.go b/gnovm/pkg/gnolang/values_conversions_test.go index 5436347733f..7ffa3e98c71 100644 --- a/gnovm/pkg/gnolang/values_conversions_test.go +++ b/gnovm/pkg/gnolang/values_conversions_test.go @@ -2,6 +2,7 @@ package gnolang import ( "math" + "strings" "testing" "github.com/cockroachdb/apd/v3" @@ -25,3 +26,134 @@ func TestConvertUntypedBigdecToFloat(t *testing.T) { require.Equal(t, float64(0), dst.GetFloat64()) } + +func TestBitShiftingOverflow(t *testing.T) { + t.Parallel() + + testFunc := func(source, msg string) { + defer func() { + if len(msg) == 0 { + return + } + + r := recover() + + if r == nil { + t.Fail() + } + + err := r.(*PreprocessError) + c := strings.Contains(err.Error(), msg) + if !c { + t.Fatalf(`expected "%s", got "%s"`, msg, r) + } + }() + + m := NewMachine("test", nil) + + n := MustParseFile("main.go", source) + m.RunFiles(n) + m.RunMain() + } + + type cases struct { + source string + msg string + } + + tests := []cases{ + { + `package test + +func main() { + const a = int32(1) << 33 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const a1 = int8(1) << 8 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const a2 = int16(1) << 16 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const a3 = int32(1) << 33 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const a4 = int64(1) << 65 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const b1 = uint8(1) << 8 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const b2 = uint16(1) << 16 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const b3 = uint32(1) << 33 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + +func main() { + const b4 = uint64(1) << 65 +}`, + `test/main.go:3:1: constant overflows`, + }, + { + `package test + + func main() { + const c1 = 1 << 128 + }`, + ``, + }, + { + `package test + + func main() { + const c1 = 1 << 128 + println(c1) + }`, + `test/main.go:5:4: bigint overflows target kind`, + }, + } + + for _, tc := range tests { + testFunc(tc.source, tc.msg) + } +} From 95450fff471df1adc9ac2115006d87d90c529e4a Mon Sep 17 00:00:00 2001 From: Blake <104744707+r3v4s@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:33:26 +0900 Subject: [PATCH 257/345] ci: validate genesis.json (#3170) --- .github/workflows/genesis-verify.yml | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/genesis-verify.yml diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml new file mode 100644 index 00000000000..6c9955b7178 --- /dev/null +++ b/.github/workflows/genesis-verify.yml @@ -0,0 +1,56 @@ +name: genesis-verify + +on: + pull_request: + branches: + - master + paths: + - "misc/deployments/**/genesis.json" + +jobs: + verify: + strategy: + fail-fast: false + matrix: + testnet: ["test5.gno.land"] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v41 + with: + files: "misc/deployments/${{ matrix.testnet }}/genesis.json" + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Build gnogenesis + run: make -C contribs/gnogenesis + + - name: Verify each genesis file + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "Verifying $file" + gnogenesis verify -genesis-path $file + done + + - name: Build gnoland + run: make -C gno.land install.gnoland + + - name: Running latest gnoland with each genesis file + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "Running gnoland with $file" + timeout 60s gnoland start -lazy --genesis $file || exit_code=$? + if [ $exit_code -eq 124 ]; then + echo "Gnoland genesis state generated successfully" + else + echo "Gnoland failed to start with $file" + exit 1 + fi + done From 496bcba5fb5f9743711a37a14b2bdb95371e1244 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:58:36 +0100 Subject: [PATCH 258/345] chore(examples/gnoland): fix gnoland/home realm, rename the blog (#3177) ## Description Fixes a broken link on the home page and renames the gno.land blog. Two options for the blog name: - gno.land's blog - The Gno Blog Option 1 seems favorable.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/r/gnoland/blog/gnoblog.gno | 2 +- examples/gno.land/r/gnoland/blog/gnoblog_test.gno | 14 +++++++------- examples/gno.land/r/gnoland/home/home.gno | 2 +- examples/gno.land/r/gnoland/home/home_filetest.gno | 2 +- examples/gno.land/r/gov/dao/v2/prop2_filetest.gno | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/gno.land/r/gnoland/blog/gnoblog.gno b/examples/gno.land/r/gnoland/blog/gnoblog.gno index 1cdc95fe9a8..d2a163543e5 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog.gno @@ -7,7 +7,7 @@ import ( ) var b = &blog.Blog{ - Title: "Gnoland's Blog", + Title: "gno.land's blog", Prefix: "/r/gnoland/blog:", } diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno index 328fbe2baa4..b4658db4fb5 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno @@ -15,7 +15,7 @@ func TestPackage(t *testing.T) { { got := Render("") expected := ` -# Gnoland's Blog +# gno.land's blog No posts. ` @@ -28,7 +28,7 @@ No posts. ModAddPost("slug2", "title2", "body2", "2022-05-20T13:17:23Z", "moul", "tag1,tag3") got := Render("") expected := ` - # Gnoland's Blog + # gno.land's blog
@@ -59,7 +59,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) Written by moul on 20 May 2022 -Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog ---
Comment section @@ -74,12 +74,12 @@ Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog // list by tags. { got := Render("t/invalid") - expected := "# [Gnoland's Blog](/r/gnoland/blog:) / t / invalid\n\nNo posts." + expected := "# [gno.land's blog](/r/gnoland/blog:) / t / invalid\n\nNo posts." assertMDEquals(t, got, expected) got = Render("t/tag2") expected = ` -# [Gnoland's Blog](/r/gnoland/blog:) / t / tag2 +# [gno.land's blog](/r/gnoland/blog:) / t / tag2
@@ -110,7 +110,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) Written by moul on 20 May 2022 -Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog ---
Comment section @@ -152,7 +152,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4) Written by manfred on 20 May 2022 -Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog ---
Comment section diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index 6a520dba394..facb1817fe2 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -95,7 +95,7 @@ func latestHOFItems(num int) ui.Element { submissions := hof.RenderExhibWidget(num) return ui.Element{ - ui.H3("[Hall of Fame](/r/demo/hof)"), + ui.H3("[Hall of Fame](/r/leon/hof)"), ui.Text(submissions), } } diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index c587af9b817..4825c9fc588 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -78,7 +78,7 @@ func main() { //
//
// -// ### [Hall of Fame](/r/demo/hof) +// ### [Hall of Fame](/r/leon/hof) // // //
diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index bfd3f44f6dd..4eb993b80dc 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -82,7 +82,7 @@ func main() { // // // -- -// # Gnoland's Blog +// # gno.land's blog // // No posts. // -- @@ -101,7 +101,7 @@ func main() { // // // -- -// # Gnoland's Blog +// # gno.land's blog // //
// From 0e1016b4ba80e99a8768f251b21f0e03609c5498 Mon Sep 17 00:00:00 2001 From: Nathan Toups <612924+n2p5@users.noreply.github.com> Date: Tue, 26 Nov 2024 06:13:21 -0700 Subject: [PATCH 259/345] feat: adding home realm for r/n2p5/home and supporting packages (#3183) # TLDR There's no place like [home](https://gno.land/r/n2p5/home). This PR is for `gno` code in [/r/n2p5/home](https://gno.land/r/n2p5/home) and its supporting packages and realms. This is a more extensive PR than I'd hoped, but it is because I ended up structuring out two new little packages [chonk]() and [group](), as well as a [config]() pattern that works for my home realm, but is reusable as well. If you like this, vote for me on [Leon's Hall of Fame](https://gno.land/r/leon/hof) # Summary of changes ## [/p/n2p5/chonk](https://gno.land/p/n2p5/chonk/) `chonk` is a linked-list based string chunker. This allows for arbitrarily large strings to be stored on-chain across multiple transactions. The original idea for this was to be able to support large `Render(path string) string` calls. You can think of this as supporting a "static site generator" pattern, where Markdown can be broken up into chunks and then rendered as one large payload. It is used directly in `/r/n2p5/home`. ## [/p/n2p5/mgroup](https://gno.land/p/n2p5/mgroup/) `mgroup` (Managed Group) is a bit like `authorizable,` but the goals are a bit different. I wanted a "managed group" with a single `ownable` owner, but I wanted an arbitrary list of "backup owners," which are accounts that could "claim" ownership if the owner account became unavailable. I also wanted to be able to manage the group members themselves. Another difference here is has the ability to return information about the group such as - a list of all backup owners (there is a method for the complete list, but also a method for an offset and max count iterator for large groups) - a list of all members (there is a method for the complete list, but also a method for an offset and max count iterator for large groups) ## [/r/n2p5/config](https://gno.land/r/n2p5/config) Inspired by the config work done by @moul and @leohhhn for their home realms, I decided to take a crack at it as well. This config allows me to use `mgroup` to manage the config auth. This allows me to power cross-realm auth using this config realm, and it powers the auth for my home realm. ## [/r/n2p5/home](https://gno.land/r/n2p5/home) Bringing this all together, the home realm uses `chonk` and `/r/n2p5/config` to Render the Markdown stored in the chonk data store. Because you might submit data over multiple transactions, I've added the ability to "preview" and "promote" render data, you can see this in the two variations on the URL: - https://gno.land/r/n2p5/home - https://gno.land/r/n2p5/home:preview
Contributors' checklist... - [X] Added new tests, or not needed, or not feasible - [X] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [X] Updated the official documentation or not needed - [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [X] Added references to related issues and PRs - [X] Provided any useful hints for running manual tests
--- examples/gno.land/p/n2p5/chonk/chonk.gno | 84 ++++ examples/gno.land/p/n2p5/chonk/chonk_test.gno | 54 +++ examples/gno.land/p/n2p5/chonk/gno.mod | 1 + examples/gno.land/p/n2p5/mgroup/gno.mod | 7 + examples/gno.land/p/n2p5/mgroup/mgroup.gno | 184 ++++++++ .../gno.land/p/n2p5/mgroup/mgroup_test.gno | 420 ++++++++++++++++++ examples/gno.land/r/n2p5/config/config.gno | 120 +++++ examples/gno.land/r/n2p5/config/gno.mod | 6 + examples/gno.land/r/n2p5/home/gno.mod | 7 + examples/gno.land/r/n2p5/home/home.gno | 73 +++ 10 files changed, 956 insertions(+) create mode 100644 examples/gno.land/p/n2p5/chonk/chonk.gno create mode 100644 examples/gno.land/p/n2p5/chonk/chonk_test.gno create mode 100644 examples/gno.land/p/n2p5/chonk/gno.mod create mode 100644 examples/gno.land/p/n2p5/mgroup/gno.mod create mode 100644 examples/gno.land/p/n2p5/mgroup/mgroup.gno create mode 100644 examples/gno.land/p/n2p5/mgroup/mgroup_test.gno create mode 100644 examples/gno.land/r/n2p5/config/config.gno create mode 100644 examples/gno.land/r/n2p5/config/gno.mod create mode 100644 examples/gno.land/r/n2p5/home/gno.mod create mode 100644 examples/gno.land/r/n2p5/home/home.gno diff --git a/examples/gno.land/p/n2p5/chonk/chonk.gno b/examples/gno.land/p/n2p5/chonk/chonk.gno new file mode 100644 index 00000000000..8b7425eafd0 --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/chonk.gno @@ -0,0 +1,84 @@ +// Package chonk provides a simple way to store arbitrarily large strings +// in a linked list across transactions for efficient storage and retrieval. +// A Chonk support three operations: Add, Flush, and Scanner. +// - Add appends a string to the Chonk. +// - Flush clears the Chonk. +// - Scanner is used to iterate over the chunks in the Chonk. +package chonk + +// Chonk is a linked list string storage and +// retrieval system for fine bois. +type Chonk struct { + first *chunk + last *chunk +} + +// chunk is a linked list node for Chonk +type chunk struct { + text string + next *chunk +} + +// New creates a reference to a new Chonk +func New() *Chonk { + return &Chonk{} +} + +// Add appends a string to the Chonk. If the Chonk is empty, +// the string will be the first and last chunk. Otherwise, +// the string will be appended to the end of the Chonk. +func (c *Chonk) Add(text string) { + next := &chunk{text: text} + if c.first == nil { + c.first = next + c.last = next + return + } + c.last.next = next + c.last = next +} + +// Flush clears the Chonk by setting the first and last +// chunks to nil. This will allow the garbage collector to +// free the memory used by the Chonk. +func (c *Chonk) Flush() { + c.first = nil + c.last = nil +} + +// Scanner returns a new Scanner for the Chonk. The Scanner +// is used to iterate over the chunks in the Chonk. +func (c *Chonk) Scanner() *Scanner { + return &Scanner{ + next: c.first, + } +} + +// Scanner is a simple string scanner for Chonk. It is used +// to iterate over the chunks in a Chonk from first to last. +type Scanner struct { + current *chunk + next *chunk +} + +// Scan advances the scanner to the next chunk. It returns +// true if there is a next chunk, and false if there is not. +func (s *Scanner) Scan() bool { + if s.next != nil { + s.current = s.next + s.next = s.next.next + return true + } + return false +} + +// Text returns the current chunk. It is only valid to call +// this method after a call to Scan returns true. Expected usage: +// +// scanner := chonk.Scanner() +// for scanner.Scan() { +// fmt.Println(scanner.Text()) +// } +func (s *Scanner) Text() string { + return s.current.text +} diff --git a/examples/gno.land/p/n2p5/chonk/chonk_test.gno b/examples/gno.land/p/n2p5/chonk/chonk_test.gno new file mode 100644 index 00000000000..7caf1012d39 --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/chonk_test.gno @@ -0,0 +1,54 @@ +package chonk + +import ( + "testing" +) + +func TestChonk(t *testing.T) { + t.Parallel() + c := New() + testTable := []struct { + name string + chunks []string + }{ + { + name: "empty", + chunks: []string{}, + }, + { + name: "single chunk", + chunks: []string{"a"}, + }, + { + name: "multiple chunks", + chunks: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + }, + { + name: "multiline chunks", + chunks: []string{"1a\nb\nc\n\n", "d\ne\nf", "g\nh\ni", "j\nk\nl\n\n\n\n"}, + }, + { + name: "empty", + chunks: []string{}, + }, + } + testChonk := func(t *testing.T, c *Chonk, chunks []string) { + for _, chunk := range chunks { + c.Add(chunk) + } + scanner := c.Scanner() + i := 0 + for scanner.Scan() { + if scanner.Text() != chunks[i] { + t.Errorf("expected %s, got %s", chunks[i], scanner.Text()) + } + i++ + } + } + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + testChonk(t, c, test.chunks) + c.Flush() + }) + } +} diff --git a/examples/gno.land/p/n2p5/chonk/gno.mod b/examples/gno.land/p/n2p5/chonk/gno.mod new file mode 100644 index 00000000000..b0dee537b0e --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/chonk diff --git a/examples/gno.land/p/n2p5/mgroup/gno.mod b/examples/gno.land/p/n2p5/mgroup/gno.mod new file mode 100644 index 00000000000..95fdbe2f195 --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/n2p5/mgroup + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest +) diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup.gno b/examples/gno.land/p/n2p5/mgroup/mgroup.gno new file mode 100644 index 00000000000..0c029401ff7 --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/mgroup.gno @@ -0,0 +1,184 @@ +// Package mgroup is a simple managed group managing ownership and membership +// for authorization in gno realms. The ManagedGroup struct is used to manage +// the owner, backup owners, and members of a group. The owner is the primary +// owner of the group and can add and remove backup owners and members. Backup +// owners can claim ownership of the group. This is meant to provide backup +// accounts for the owner in case the owner account is lost or compromised. +// Members are used to authorize actions across realms. +package mgroup + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +var ( + ErrCannotRemoveOwner = errors.New("mgroup: cannot remove owner") + ErrNotBackupOwner = errors.New("mgroup: not a backup owner") + ErrNotMember = errors.New("mgroup: not a member") + ErrInvalidAddress = errors.New("mgroup: address is invalid") +) + +type ManagedGroup struct { + owner *ownable.Ownable + backupOwners *avl.Tree + members *avl.Tree +} + +// New creates a new ManagedGroup with the owner set to the provided address. +// The owner is automatically added as a backup owner and member of the group. +func New(ownerAddress std.Address) *ManagedGroup { + g := &ManagedGroup{ + owner: ownable.NewWithAddress(ownerAddress), + backupOwners: avl.NewTree(), + members: avl.NewTree(), + } + g.AddBackupOwner(ownerAddress) + g.AddMember(ownerAddress) + return g +} + +// AddBackupOwner adds a backup owner to the group by std.Address. +// If the caller is not the owner, an error is returned. +func (g *ManagedGroup) AddBackupOwner(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + g.backupOwners.Set(addr.String(), struct{}{}) + return nil +} + +// RemoveBackupOwner removes a backup owner from the group by std.Address. +// The owner cannot be removed. If the caller is not the owner, an error is returned. +func (g *ManagedGroup) RemoveBackupOwner(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + if addr == g.Owner() { + return ErrCannotRemoveOwner + } + g.backupOwners.Remove(addr.String()) + return nil +} + +// ClaimOwnership allows a backup owner to claim ownership of the group. +// If the caller is not a backup owner, an error is returned. +// The caller is automatically added as a member of the group. +func (g *ManagedGroup) ClaimOwnership() error { + caller := std.PrevRealm().Addr() + // already owner, skip + if caller == g.Owner() { + return nil + } + if !g.IsBackupOwner(caller) { + return ErrNotMember + } + g.owner = ownable.NewWithAddress(caller) + g.AddMember(caller) + return nil +} + +// AddMember adds a member to the group by std.Address. +// If the caller is not the owner, an error is returned. +func (g *ManagedGroup) AddMember(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + g.members.Set(addr.String(), struct{}{}) + return nil +} + +// RemoveMember removes a member from the group by std.Address. +// The owner cannot be removed. If the caller is not the owner, +// an error is returned. +func (g *ManagedGroup) RemoveMember(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + if addr == g.Owner() { + return ErrCannotRemoveOwner + } + g.members.Remove(addr.String()) + return nil +} + +// MemberCount returns the number of members in the group. +func (g *ManagedGroup) MemberCount() int { + return g.members.Size() +} + +// BackupOwnerCount returns the number of backup owners in the group. +func (g *ManagedGroup) BackupOwnerCount() int { + return g.backupOwners.Size() +} + +// IsMember checks if an address is a member of the group. +func (g *ManagedGroup) IsMember(addr std.Address) bool { + return g.members.Has(addr.String()) +} + +// IsBackupOwner checks if an address is a backup owner in the group. +func (g *ManagedGroup) IsBackupOwner(addr std.Address) bool { + return g.backupOwners.Has(addr.String()) +} + +// Owner returns the owner of the group. +func (g *ManagedGroup) Owner() std.Address { + return g.owner.Owner() +} + +// BackupOwners returns a slice of all backup owners in the group, using the underlying +// avl.Tree to iterate over the backup owners. If you have a large group, you may +// want to use BackupOwnersWithOffset to iterate over backup owners in chunks. +func (g *ManagedGroup) BackupOwners() []string { + return g.BackupOwnersWithOffset(0, g.BackupOwnerCount()) +} + +// Members returns a slice of all members in the group, using the underlying +// avl.Tree to iterate over the members. If you have a large group, you may +// want to use MembersWithOffset to iterate over members in chunks. +func (g *ManagedGroup) Members() []string { + return g.MembersWithOffset(0, g.MemberCount()) +} + +// BackupOwnersWithOffset returns a slice of backup owners in the group, using the underlying +// avl.Tree to iterate over the backup owners. The offset and count parameters allow you +// to iterate over backup owners in chunks to support patterns such as pagination. +func (g *ManagedGroup) BackupOwnersWithOffset(offset, count int) []string { + return sliceWithOffset(g.backupOwners, offset, count) +} + +// MembersWithOffset returns a slice of members in the group, using the underlying +// avl.Tree to iterate over the members. The offset and count parameters allow you +// to iterate over members in chunks to support patterns such as pagination. +func (g *ManagedGroup) MembersWithOffset(offset, count int) []string { + return sliceWithOffset(g.members, offset, count) +} + +// sliceWithOffset is a helper function to iterate over an avl.Tree with an offset and count. +func sliceWithOffset(t *avl.Tree, offset, count int) []string { + var result []string + t.IterateByOffset(offset, count, func(k string, _ interface{}) bool { + if k == "" { + return true + } + result = append(result, k) + return false + }) + return result +} diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno new file mode 100644 index 00000000000..7ef0619188f --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno @@ -0,0 +1,420 @@ +package mgroup + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" +) + +func TestManagedGroup(t *testing.T) { + t.Parallel() + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + u3 := testutils.TestAddress("u3") + + t.Run("AddBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + err := g.AddBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.AddBackupOwner(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.AddBackupOwner(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + }) + t.Run("RemoveBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + g.AddBackupOwner(u2) + err := g.RemoveBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u1) + err := g.RemoveBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.RemoveBackupOwner(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error()) + } + } + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.RemoveBackupOwner(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + { + std.TestSetOrigCaller(u1) + err := g.RemoveBackupOwner(u1) + if err != ErrCannotRemoveOwner { + t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error()) + } + } + }) + t.Run("ClaimOwnership", func(t *testing.T) { + t.Parallel() + g := New(u1) + g.AddBackupOwner(u2) + // happy path + { + std.TestSetOrigCaller(u2) + err := g.ClaimOwnership() + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if g.Owner() != u2 { + t.Errorf("expected %v, got %v", u2, g.Owner()) + } + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u2) + err := g.ClaimOwnership() + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u3) + err := g.ClaimOwnership() + if err != ErrNotMember { + t.Errorf("expected %v, got %v", ErrNotMember.Error(), err.Error()) + } + } + }) + t.Run("AddMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + err := g.AddMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.AddMember(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.AddMember(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + }) + t.Run("RemoveMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + g.AddMember(u2) + err := g.RemoveMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if g.IsMember(u2) { + t.Errorf("expected %v to not be a member", u2) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u1) + err := g.RemoveMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.RemoveMember(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.RemoveMember(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + // ensure owner cannot be removed + { + std.TestSetOrigCaller(u1) + err := g.RemoveMember(u1) + if err != ErrCannotRemoveOwner { + t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error()) + } + } + }) + t.Run("MemberCount", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.MemberCount() != 1 { + t.Errorf("expected 0, got %v", g.MemberCount()) + } + g.AddMember(u2) + if g.MemberCount() != 2 { + t.Errorf("expected 1, got %v", g.MemberCount()) + } + g.AddMember(u3) + if g.MemberCount() != 3 { + t.Errorf("expected 2, got %v", g.MemberCount()) + } + g.RemoveMember(u2) + if g.MemberCount() != 2 { + t.Errorf("expected 1, got %v", g.MemberCount()) + } + }) + t.Run("BackupOwnerCount", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.BackupOwnerCount() != 1 { + t.Errorf("expected 0, got %v", g.BackupOwnerCount()) + } + g.AddBackupOwner(u2) + if g.BackupOwnerCount() != 2 { + t.Errorf("expected 1, got %v", g.BackupOwnerCount()) + } + g.AddBackupOwner(u3) + if g.BackupOwnerCount() != 3 { + t.Errorf("expected 2, got %v", g.BackupOwnerCount()) + } + g.RemoveBackupOwner(u2) + if g.BackupOwnerCount() != 2 { + t.Errorf("expected 1, got %v", g.BackupOwnerCount()) + } + }) + t.Run("IsMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + if !g.IsMember(u1) { + t.Errorf("expected %v to be a member", u1) + } + if g.IsMember(u2) { + t.Errorf("expected %v to not be a member", u2) + } + g.AddMember(u2) + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + }) + t.Run("IsBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + if !g.IsBackupOwner(u1) { + t.Errorf("expected %v to be a backup owner", u1) + } + if g.IsBackupOwner(u2) { + t.Errorf("expected %v to not be a backup owner", u2) + } + g.AddBackupOwner(u2) + if !g.IsBackupOwner(u2) { + t.Errorf("expected %v to be a backup owner", u2) + } + }) + t.Run("Owner", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.Owner() != u1 { + t.Errorf("expected %v, got %v", u1, g.Owner()) + } + g.AddBackupOwner(u2) + if g.Owner() != u1 { + t.Errorf("expected %v, got %v", u1, g.Owner()) + } + std.TestSetOrigCaller(u2) + g.ClaimOwnership() + if g.Owner() != u2 { + t.Errorf("expected %v, got %v", u2, g.Owner()) + } + }) + t.Run("BackupOwners", func(t *testing.T) { + t.Parallel() + std.TestSetOrigCaller(u1) + g := New(u1) + g.AddBackupOwner(u2) + g.AddBackupOwner(u3) + owners := g.BackupOwners() + if len(owners) != 3 { + t.Errorf("expected 2, got %v", len(owners)) + } + if owners[0] != u2.String() { + t.Errorf("expected %v, got %v", u2, owners[0]) + } + if owners[1] != u3.String() { + t.Errorf("expected %v, got %v", u3, owners[1]) + } + if owners[2] != u1.String() { + t.Errorf("expected %v, got %v", u3, owners[1]) + } + }) + t.Run("Members", func(t *testing.T) { + t.Parallel() + std.TestSetOrigCaller(u1) + g := New(u1) + g.AddMember(u2) + g.AddMember(u3) + members := g.Members() + if len(members) != 3 { + t.Errorf("expected 2, got %v", len(members)) + } + if members[0] != u2.String() { + t.Errorf("expected %v, got %v", u2, members[0]) + } + if members[1] != u3.String() { + t.Errorf("expected %v, got %v", u3, members[1]) + } + if members[2] != u1.String() { + t.Errorf("expected %v, got %v", u3, members[1]) + } + }) +} + +func TestSliceWithOffset(t *testing.T) { + t.Parallel() + testTable := []struct { + name string + slice []string + offset int + count int + expected []string + expectedCount int + }{ + { + name: "empty", + slice: []string{}, + offset: 0, + count: 0, + expected: []string{}, + expectedCount: 0, + }, + { + name: "single", + slice: []string{"a"}, + offset: 0, + count: 1, + expected: []string{"a"}, + expectedCount: 1, + }, + { + name: "single offset", + slice: []string{"a"}, + offset: 1, + count: 1, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 0, + count: 10, + expected: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + expectedCount: 10, + }, + { + name: "multiple offset", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 5, + count: 5, + expected: []string{"f", "g", "h", "i", "j"}, + expectedCount: 5, + }, + { + name: "multiple offset end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 10, + count: 5, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple offset past end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 11, + count: 5, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple offset count past end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 5, + count: 20, + expected: []string{"f", "g", "h", "i", "j"}, + expectedCount: 5, + }, + } + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + tree := avl.NewTree() + for _, s := range test.slice { + tree.Set(s, struct{}{}) + } + slice := sliceWithOffset(tree, test.offset, test.count) + if len(slice) != test.expectedCount { + t.Errorf("expected %v, got %v", test.expectedCount, len(slice)) + } + }) + } +} diff --git a/examples/gno.land/r/n2p5/config/config.gno b/examples/gno.land/r/n2p5/config/config.gno new file mode 100644 index 00000000000..42cb587eaf5 --- /dev/null +++ b/examples/gno.land/r/n2p5/config/config.gno @@ -0,0 +1,120 @@ +package config + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/n2p5/mgroup" +) + +const ( + originalOwner = "g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t" // n2p5 +) + +var ( + adminGroup = mgroup.New(originalOwner) + description = "" +) + +// AddBackupOwner adds a backup owner to the Owner Group. +// A backup owner can claim ownership of the contract. +func AddBackupOwner(addr std.Address) { + err := adminGroup.AddBackupOwner(addr) + if err != nil { + panic(err) + } +} + +// RemoveBackupOwner removes a backup owner from the Owner Group. +// The primary owner cannot be removed. +func RemoveBackupOwner(addr std.Address) { + err := adminGroup.RemoveBackupOwner(addr) + if err != nil { + panic(err) + } +} + +// ClaimOwnership allows an authorized user in the ownerGroup +// to claim ownership of the contract. +func ClaimOwnership() { + err := adminGroup.ClaimOwnership() + if err != nil { + panic(err) + } +} + +// AddAdmin adds an admin to the Admin Group. +func AddAdmin(addr std.Address) { + err := adminGroup.AddMember(addr) + if err != nil { + panic(err) + } +} + +// RemoveAdmin removes an admin from the Admin Group. +// The primary owner cannot be removed. +func RemoveAdmin(addr std.Address) { + err := adminGroup.RemoveMember(addr) + if err != nil { + panic(err) + } +} + +// Owner returns the current owner of the claims contract. +func Owner() std.Address { + return adminGroup.Owner() +} + +// BackupOwners returns the current backup owners of the claims contract. +func BackupOwners() []string { + return adminGroup.BackupOwners() +} + +// Admins returns the current admin members of the claims contract. +func Admins() []string { + return adminGroup.Members() +} + +// IsAdmin checks if an address is in the config adminGroup. +func IsAdmin(addr std.Address) bool { + return adminGroup.IsMember(addr) +} + +// toMarkdownList formats a slice of strings as a markdown list. +func toMarkdownList(items []string) string { + var result string + for _, item := range items { + result += ufmt.Sprintf("- %s\n", item) + } + return result +} + +func Render(path string) string { + owner := adminGroup.Owner().String() + backupOwners := toMarkdownList(BackupOwners()) + adminMembers := toMarkdownList(Admins()) + return ufmt.Sprintf(` +# Config Dashboard + +This dashboard shows the current configuration owner, backup owners, and admin members. +- The owner has the exclusive ability to manage the backup owners and admin members. +- Backup owners can claim ownership of the contract and become the owner. +- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home). + +#### Owner + +%s + +#### Backup Owners + +%s + +#### Admin Members + +%s + +`, + owner, + backupOwners, + adminMembers) +} diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod new file mode 100644 index 00000000000..33f9276a409 --- /dev/null +++ b/examples/gno.land/r/n2p5/config/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/n2p5/config + +require ( + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/n2p5/mgroup v0.0.0-latest +) diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod new file mode 100644 index 00000000000..779aa914989 --- /dev/null +++ b/examples/gno.land/r/n2p5/home/gno.mod @@ -0,0 +1,7 @@ +module gno.land/r/n2p5/home + +require ( + gno.land/p/n2p5/chonk v0.0.0-latest + gno.land/r/leon/hof v0.0.0-latest + gno.land/r/n2p5/config v0.0.0-latest +) diff --git a/examples/gno.land/r/n2p5/home/home.gno b/examples/gno.land/r/n2p5/home/home.gno new file mode 100644 index 00000000000..69b82e86d68 --- /dev/null +++ b/examples/gno.land/r/n2p5/home/home.gno @@ -0,0 +1,73 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/n2p5/chonk" + + "gno.land/r/leon/hof" + "gno.land/r/n2p5/config" +) + +var ( + active = chonk.New() + preview = chonk.New() +) + +func init() { + hof.Register() +} + +// Add appends a string to the preview Chonk. +func Add(chunk string) { + assertAdmin() + preview.Add(chunk) +} + +// Flush clears the preview Chonk. +func Flush() { + assertAdmin() + preview.Flush() +} + +// Promote promotes the preview Chonk to the active Chonk +// and creates a new preview Chonk. +func Promote() { + assertAdmin() + active = preview + preview = chonk.New() +} + +// Render returns the contents of the scanner for the active or preview Chonk +// based on the path provided. +func Render(path string) string { + var result string + scanner := getScanner(path) + for scanner.Scan() { + result += scanner.Text() + } + return result +} + +// assertAdmin panics if the caller is not an admin as defined in the config realm. +func assertAdmin() { + caller := std.PrevRealm().Addr() + if !config.IsAdmin(caller) { + panic("forbidden: must be admin") + } +} + +// getScanner returns the scanner for the active or preview Chonk based +// on the path provided. +func getScanner(path string) *chonk.Scanner { + if isPreview(path) { + return preview.Scanner() + } + return active.Scanner() +} + +// isPreview returns true if the path prefix is "preview". +func isPreview(path string) bool { + return strings.HasPrefix(path, "preview") +} From 18e4eb9d7e19fed97b093697e12a6820fa14aaca Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 26 Nov 2024 15:19:51 +0100 Subject: [PATCH 260/345] feat(gnovm): test execution refactor + speedup (#3119) This PR tackles a large amount of improvements in our current internal testing systems for the gnovm, as well as those for `gno test`. The primary target is the execution of filetests; however many improvements have been implemented to remove dead or duplicate code, and bring over some of the improvements that have been implemented in tests to filetests, when they come at little added "cost". The biggest headline concerns the execution of filetests. I wrote the specific improvements undertaken in a [blog post on "diary of a gnome"](https://gno.howl.moe/faster-tests/), but here's a side-by-side comparison of the execution in this PR (left) and the execution on master (right). ![filetests](https://github.com/user-attachments/assets/049680f2-baeb-4f24-8f0f-60ae5fa4bce5) - Fixes #1084 - Fixes #2826 (by addressing root cause of slowness) - Fixes #588, only running `_long` filetests on master and generally speeding up test execution. ## Impact - Test context (tests and filetests) - Tests and filetests now share the same `test.Store` when running. Coupled with `test.LoadImports`, it allows us to "eagerly load" imports ahead of the test execution, and save it in the store. This is the primary performance improvement of this PR, as all pure packages can be run and preprocessed only once, whereas before it was once per package, and once per filetest. (More info in the blog post.) - One of the consequences of this is that package initialization happens outside of the test; so a filetest can no longer check the output of a `println` used in package initialization. - The default user no longer has 200 gnot by default. There are two mechanisms that make this unnecessary: `// SEND:`, and `std.TestIssueCoin`. Some test balances had to be updated. - Filetests - Running filetests in `gno test -v` now also prints their output. - Realm tests are no longer executed in `main.gno`, but in a filename following the name of the filetest; this changed some `// Realm:` directives. - The sytem to read directives and update them in the filetests has been re-written, and should now be more resilient and with fewer "exceptions". This means that leading and trailing whitespace in output now is correctly considered as output, leading to the addition of many empty `//` in the tests. - `// Error:` directives are now treated the same as everything else; and are updated if the `-update-golden-tests` flag is passed. - Removed the `imports` metric from the runtime metrics, as it's now no longer straightforward to calculate (given that imports are loaded "eagerly"). - Removed support for different "modes" of importing stdlibs; further removing support for gonative (#1361). The remaining gonative libraries are `os`, `fmt`, `encoding/json`, `internal/os_test` and `math/big`. This removes the `-with-native-fallback` flag from `gno test`. - Consequently, filetests ending with `_native.gno` have largely been removed, and those ending with `_stdlibs.go` have had their suffix removed. - Some files testing `gonative` types and functions which were only used in a small subset of these tests, have been removed. - Tests - `gno test`, for testing packages, created a `main_test.gno` file that is then called directly from the machine on each test. This creates two identifiers, `tests` and `runtest`, which could come into conflict with those defined by the tests themselves. We now call `testing.RunTest` directly, without an intermediary. - Exports in the normal tests (ie. defined in `pkg`) are now visible in the tests of `pkg_test`. This is most useful for some standard library tests, where there often is an `export_test.go` with the sole scope of exporting some internal functions and methods for testing in the "real" testing file. - `gno lint`, and other occasions where we use `issueFromError` (like when parsing errors in `gno test`), will now also print the column of the error if given. ## Summary of internal changes - pkg/gnolang - `TestFiles` is the new function running the filetests. - `eval_test.go` has been removed, moving some of its improvements over to `TestFiles`, and reducing the scope of the corresponding tests in `debugger.go`, to what it's actually supposed to test for. - As a consequence of removing all of type mappings in `imports.go`, I removed `SetStrictGo2GnoMapping` in favour of always being "strict", and not allowing defining native types of defined types any more. - The tests in `gonative_test` where primarily related to this, so I removed them. Similarly for `preprocess_test`. - `TestFunc` / `TestMemPackage` have been removed as redundant, including some of their features in `cmd/go/test.go` (like supporting exporting symbols in `_test.gno` files) - The `Store` no longer has `ClearCache` and `SetStrictGo2GnoMapping`; `ClearCache` now has a better substitute in `BeginTransaction`. - the `testing` stdlib no longer caches `matchPat` and `matchString` as pure packages should not be able to modify their values during execution, and as a result of other changes this was failing. - The tests/ directory has been removed / moved to `gnovm/pkg/test`. This directory should eventually contain the internal code for `gno test` as well; but for now I wanted to clean `tests` to ensure it is a directory just for integration tests, rather than code and testing systems. - `TestMachine` and `TestStore` have been renamed to Machine and Store, to avoid redundancy with the `test` package name. - Removed plenty instructions in `gnovm/Makefile` as now most tests are just in `pkg/gnolang`. - I removed `MsgContext.Msg` as unused. It can be re-added later if necessary. - In the CI, tests are now run with `-short`, at least until we figure out how to make the VM efficient enough to run these tests. Aside from some filetests, this now excludes some stdlibs: `bytes`, `strconv`, `regexp/syntax`. I accept other proposals that could make us have fast tests on PR's, while still testing (mostly) everything. --- [![Open Source Saturday](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F-open%20source%20saturday-F64060.svg)](https://lu.ma/open-source-saturday-torino) --------- Co-authored-by: Marc Vertes --- .github/workflows/examples.yml | 6 +- .github/workflows/gnovm.yml | 2 + .../gno.land/p/demo/avl/pager/z_filetest.gno | 1 + examples/gno.land/p/demo/avl/z_0_filetest.gno | 8 +- examples/gno.land/p/demo/avl/z_1_filetest.gno | 8 +- .../p/demo/tamagotchi/z0_filetest.gno | 1 + .../gno.land/r/demo/banktest/z_0_filetest.gno | 4 +- .../gno.land/r/demo/banktest/z_2_filetest.gno | 4 +- .../gno.land/r/demo/boards/z_0_filetest.gno | 2 + .../r/demo/boards/z_10_c_filetest.gno | 1 + .../r/demo/boards/z_11_d_filetest.gno | 1 + .../gno.land/r/demo/boards/z_11_filetest.gno | 1 + .../gno.land/r/demo/boards/z_12_filetest.gno | 2 + .../gno.land/r/demo/boards/z_1_filetest.gno | 1 + .../gno.land/r/demo/boards/z_2_filetest.gno | 1 + .../gno.land/r/demo/boards/z_3_filetest.gno | 1 + .../gno.land/r/demo/boards/z_4_filetest.gno | 1 + .../gno.land/r/demo/boards/z_5_c_filetest.gno | 1 + .../gno.land/r/demo/boards/z_5_filetest.gno | 1 + .../gno.land/r/demo/boards/z_6_filetest.gno | 1 + .../gno.land/r/demo/boards/z_7_filetest.gno | 2 + .../gno.land/r/demo/boards/z_8_filetest.gno | 1 + .../gno.land/r/demo/boards/z_9_filetest.gno | 1 + .../gno.land/r/demo/disperse/z_0_filetest.gno | 4 +- .../gno.land/r/demo/disperse/z_1_filetest.gno | 4 +- .../gno.land/r/demo/groups/z_0_c_filetest.gno | 1 + .../gno.land/r/demo/groups/z_1_a_filetest.gno | 2 + .../gno.land/r/demo/groups/z_2_a_filetest.gno | 2 + .../gno.land/r/demo/groups/z_2_e_filetest.gno | 2 + .../releases_example/releases0_filetest.gno | 1 + .../r/demo/tamagotchi/z0_filetest.gno | 1 + .../gno.land/r/demo/users/z_5_filetest.gno | 2 +- .../gno.land/r/demo/wugnot/z0_filetest.gno | 6 +- .../gno.land/r/gnoland/faucet/faucet_test.gno | 6 +- .../gno.land/r/gnoland/faucet/z0_filetest.gno | 2 + .../gno.land/r/gnoland/faucet/z1_filetest.gno | 2 + .../gno.land/r/gnoland/faucet/z2_filetest.gno | 2 + .../gno.land/r/gnoland/faucet/z3_filetest.gno | 2 + .../gno.land/r/gov/dao/v2/prop1_filetest.gno | 1 + .../gno.land/r/gov/dao/v2/prop4_filetest.gno | 2 + examples/gno.land/r/moul/home/z1_filetest.gno | 2 + examples/gno.land/r/moul/home/z2_filetest.gno | 2 + gno.land/pkg/sdk/vm/keeper.go | 5 - gnovm/Makefile | 20 +- gnovm/cmd/gno/lint.go | 16 +- gnovm/cmd/gno/lint_test.go | 8 +- gnovm/cmd/gno/run.go | 10 +- gnovm/cmd/gno/run_test.go | 2 +- gnovm/cmd/gno/test.go | 497 ++---------- .../gno/testdata/gno_lint/bad_import.txtar | 2 +- .../gno/testdata/gno_lint/file_error.txtar | 2 +- .../gno/testdata/gno_lint/not_declared.txtar | 2 +- .../gno/testdata/gno_test/error_correct.txtar | 1 - .../testdata/gno_test/error_incorrect.txtar | 5 +- .../gno/testdata/gno_test/error_sync.txtar | 7 +- .../testdata/gno_test/failing_filetest.txtar | 3 +- .../testdata/gno_test/filetest_events.txtar | 2 +- .../gno_test/flag_print-runtime-metrics.txtar | 3 +- .../testdata/gno_test/output_correct.txtar | 3 +- .../testdata/gno_test/output_incorrect.txtar | 7 +- .../gno/testdata/gno_test/output_sync.txtar | 6 +- .../gno_test/pkg_underscore_test.txtar | 3 +- .../gno/testdata/gno_test/realm_correct.txtar | 23 +- .../testdata/gno_test/realm_incorrect.txtar | 12 +- .../gno/testdata/gno_test/realm_sync.txtar | 28 +- .../gno_test/test_with-native-fallback.txtar | 32 - .../gno/testdata/gno_test/unknow_lib.txtar | 32 - .../testdata/gno_test/unknown_package.txtar | 24 + .../testdata/gno_test/valid_filetest.txtar | 2 +- .../gobuild_flag_build_error.txtar | 5 +- gnovm/cmd/gno/util.go | 14 - gnovm/pkg/gnolang/debugger_test.go | 33 +- gnovm/pkg/gnolang/eval_test.go | 132 ---- gnovm/pkg/gnolang/files_test.go | 141 ++++ gnovm/pkg/gnolang/gonative.go | 84 +-- gnovm/pkg/gnolang/gonative_test.go | 149 ---- gnovm/pkg/gnolang/machine.go | 157 +--- gnovm/pkg/gnolang/nodes.go | 33 - gnovm/pkg/gnolang/preprocess.go | 27 +- gnovm/pkg/gnolang/preprocess_test.go | 62 -- gnovm/pkg/gnolang/store.go | 25 +- gnovm/pkg/gnolang/store_test.go | 2 - gnovm/pkg/repl/repl.go | 6 +- gnovm/pkg/test/filetest.go | 407 ++++++++++ gnovm/pkg/test/imports.go | 265 +++++++ gnovm/pkg/test/test.go | 483 ++++++++++++ gnovm/{cmd/gno => pkg/test}/util_match.go | 2 +- gnovm/stdlibs/io/example_test.gno | 37 +- gnovm/stdlibs/io/multi_test.gno | 12 +- gnovm/stdlibs/std/context.go | 1 - gnovm/stdlibs/strconv/example_test.gno | 3 +- gnovm/stdlibs/testing/match.gno | 37 +- gnovm/stdlibs/testing/testing.gno | 2 +- gnovm/tests/README.md | 36 +- gnovm/tests/file.go | 713 ------------------ gnovm/tests/file_test.go | 144 ---- .../{access0_stdlibs.gno => access0.gno} | 0 .../{access1_stdlibs.gno => access1.gno} | 2 +- .../{access2_stdlibs.gno => access2.gno} | 0 .../{access3_stdlibs.gno => access3.gno} | 0 .../{access4_stdlibs.gno => access4.gno} | 2 +- .../{access5_stdlibs.gno => access5.gno} | 0 .../{access6_stdlibs.gno => access6.gno} | 2 +- .../{access7_stdlibs.gno => access7.gno} | 2 +- .../files/{addr0b_stdlibs.gno => addr0b.gno} | 0 gnovm/tests/files/addr0b_native.gno | 25 - gnovm/tests/files/addr2b.gno | 10 +- .../{assign0b_stdlibs.gno => assign0b.gno} | 0 gnovm/tests/files/assign0b_native.gno | 19 - ... => cross_realm_compositelit_filetest.gno} | 0 .../more/realm_compositelit_filetest.gno | 4 +- gnovm/tests/files/bin1.gno | 8 +- gnovm/tests/files/bin5.gno | 15 - gnovm/tests/files/binstruct_ptr_map0.gno | 7 +- gnovm/tests/files/binstruct_ptr_slice0.gno | 17 - gnovm/tests/files/binstruct_slice0.gno | 7 +- gnovm/tests/files/composite11.gno | 7 +- gnovm/tests/files/const14.gno | 8 +- gnovm/tests/files/const22.gno | 2 +- gnovm/tests/files/context.gno | 19 - gnovm/tests/files/context2.gno | 22 - gnovm/tests/files/defer4.gno | 23 - gnovm/tests/files/extern/p1/s1.gno | 5 - .../timtadh/data_structures/types/string.gno | 18 +- .../files/{float5_stdlibs.gno => float5.gno} | 0 gnovm/tests/files/fun6.gno | 21 - gnovm/tests/files/fun6b.gno | 20 - gnovm/tests/files/fun7.gno | 18 - gnovm/tests/files/heap_alloc_forloop9_1.gno | 2 +- gnovm/tests/files/heap_item_value.gno | 4 +- gnovm/tests/files/heap_item_value_init.gno | 8 +- gnovm/tests/files/import3.gno | 5 +- gnovm/tests/files/import5.gno | 2 - gnovm/tests/files/interp.gi | 13 - gnovm/tests/files/interp2.gi | 16 - .../tests/files/{io0_stdlibs.gno => io0.gno} | 0 gnovm/tests/files/io0_native.gno | 18 - gnovm/tests/files/io2.gno | 5 +- ...{issue_558b_stdlibs.gno => issue_558b.gno} | 4 +- gnovm/tests/files/issue_782.gno | 2 +- gnovm/tests/files/l3_long.gno | 163 ---- gnovm/tests/files/l4_long.gno | 7 - gnovm/tests/files/l5_long.gno | 164 ---- gnovm/tests/files/{l2_long.gno => loop0.gno} | 0 gnovm/tests/files/loop1.gno | 51 ++ gnovm/tests/files/map27.gno | 3 +- .../files/{map29_stdlibs.gno => map29.gno} | 0 gnovm/tests/files/map29_native.gno | 26 - .../files/{math0_stdlibs.gno => math0.gno} | 0 gnovm/tests/files/math3.gno | 25 +- .../files/{math_native.gno => math5.gno} | 0 gnovm/tests/files/method16b.gno | 2 +- gnovm/tests/files/method18.gno | 29 - gnovm/tests/files/method20.gno | 7 +- gnovm/tests/files/method24.gno | 33 - gnovm/tests/files/method25.gno | 33 - gnovm/tests/files/op0.gno | 2 +- gnovm/tests/files/print0.gno | 1 + gnovm/tests/files/sample.plugin | 19 - gnovm/tests/files/secure.gi | 33 - .../files/{std0_stdlibs.gno => std0.gno} | 0 .../files/{std10_stdlibs.gno => std10.gno} | 0 .../files/{std11_stdlibs.gno => std11.gno} | 0 .../files/{std2_stdlibs.gno => std2.gno} | 0 .../files/{std3_stdlibs.gno => std3.gno} | 0 .../files/{std4_stdlibs.gno => std4.gno} | 0 .../files/{std5_stdlibs.gno => std5.gno} | 2 +- .../files/{std6_stdlibs.gno => std6.gno} | 0 .../files/{std7_stdlibs.gno => std7.gno} | 0 .../files/{std8_stdlibs.gno => std8.gno} | 4 +- .../files/{std9_stdlibs.gno => std9.gno} | 0 .../{stdbanker_stdlibs.gno => stdbanker.gno} | 0 .../{stdlibs_stdlibs.gno => stdlibs.gno} | 0 .../{struct13_stdlibs.gno => struct13.gno} | 0 gnovm/tests/files/struct13_native.gno | 19 - gnovm/tests/files/switch21.gno | 2 +- .../files/{time0_stdlibs.gno => time0.gno} | 0 gnovm/tests/files/time0_native.gno | 13 - .../files/{time1_stdlibs.gno => time1.gno} | 0 .../files/{time11_stdlibs.gno => time11.gno} | 0 gnovm/tests/files/time11_native.gno | 15 - .../files/{time12_stdlibs.gno => time12.gno} | 0 gnovm/tests/files/time12_native.gno | 15 - .../files/{time13_stdlibs.gno => time13.gno} | 0 gnovm/tests/files/time13_native.gno | 18 - .../files/{time14_stdlibs.gno => time14.gno} | 0 gnovm/tests/files/time14_native.gno | 20 - gnovm/tests/files/time16_native.gno | 14 - gnovm/tests/files/time17_native.gno | 20 - gnovm/tests/files/time1_native.gno | 15 - .../files/{time2_stdlibs.gno => time2.gno} | 0 gnovm/tests/files/time2_native.gno | 15 - .../files/{time3_stdlibs.gno => time3.gno} | 0 gnovm/tests/files/time3_native.gno | 15 - .../files/{time4_stdlibs.gno => time4.gno} | 0 gnovm/tests/files/time4_native.gno | 15 - .../files/{time6_stdlibs.gno => time6.gno} | 0 gnovm/tests/files/time6_native.gno | 16 - .../files/{time7_stdlibs.gno => time7.gno} | 0 gnovm/tests/files/time7_native.gno | 13 - .../files/{time9_stdlibs.gno => time9.gno} | 0 gnovm/tests/files/time9_native.gno | 13 - gnovm/tests/files/type11.gno | 11 +- .../files/{type2_stdlibs.gno => type2.gno} | 0 gnovm/tests/files/type2_native.gno | 24 - ...typeassert7_native.gno => typeassert7.gno} | 0 ...peassert7a_native.gno => typeassert7a.gno} | 2 +- ...ssign_f0_stdlibs.gno => add_assign_f0.gno} | 2 +- ...ssign_f1_stdlibs.gno => add_assign_f1.gno} | 2 +- ...ssign_f2_stdlibs.gno => add_assign_f2.gno} | 2 +- .../types/{add_f0_stdlibs.gno => add_f0.gno} | 2 +- .../types/{add_f1_stdlibs.gno => add_f1.gno} | 2 +- .../types/{and_f0_stdlibs.gno => and_f0.gno} | 2 +- .../types/{and_f1_stdlibs.gno => and_f1.gno} | 2 +- ...mp_iface_0_stdlibs.gno => cmp_iface_0.gno} | 0 ...mp_iface_3_stdlibs.gno => cmp_iface_3.gno} | 0 .../{eql_0f8_stdlibs.gno => cmp_iface_5.gno} | 2 +- .../types/{eql_0b4_native.gno => eql_0b4.gno} | 2 +- gnovm/tests/files/types/eql_0b4_stdlibs.gno | 13 - .../types/{eql_0f0_native.gno => eql_0f0.gno} | 2 +- gnovm/tests/files/types/eql_0f0_stdlibs.gno | 28 - .../{eql_0f1_stdlibs.gno => eql_0f1.gno} | 2 +- .../{eql_0f27_stdlibs.gno => eql_0f27.gno} | 2 +- .../{eql_0f2b_native.gno => eql_0f2b.gno} | 2 +- gnovm/tests/files/types/eql_0f2b_stdlibs.gno | 28 - .../{eql_0f2c_native.gno => eql_0f2c.gno} | 2 +- gnovm/tests/files/types/eql_0f2c_stdlibs.gno | 28 - .../{eql_0f40_stdlibs.gno => eql_0f40.gno} | 0 .../{eql_0f41_stdlibs.gno => eql_0f41.gno} | 2 +- .../{cmp_iface_5_stdlibs.gno => eql_0f8.gno} | 2 +- .../files/types/explicit_conversion_0.gno | 2 +- .../files/types/explicit_conversion_1.gno | 2 +- .../files/types/explicit_conversion_2.gno | 2 +- .../types/{or_f0_stdlibs.gno => or_f0.gno} | 2 +- .../types/{or_f1_stdlibs.gno => or_f1.gno} | 2 +- gnovm/tests/files/types/shift_b0.gno | 2 +- gnovm/tests/files/types/shift_b1.gno | 2 +- gnovm/tests/files/types/shift_b10.gno | 2 +- gnovm/tests/files/types/shift_b11.gno | 2 +- gnovm/tests/files/types/shift_b2.gno | 2 +- gnovm/tests/files/types/shift_b3.gno | 2 +- gnovm/tests/files/types/shift_b4.gno | 2 +- gnovm/tests/files/types/shift_b5.gno | 2 +- gnovm/tests/files/types/shift_b6.gno | 2 +- gnovm/tests/files/types/shift_b6a.gno | 2 +- gnovm/tests/files/types/shift_b7.gno | 2 +- gnovm/tests/files/types/shift_b8.gno | 2 +- gnovm/tests/files/types/shift_b9.gno | 2 +- gnovm/tests/files/types/shift_c3.gno | 2 +- gnovm/tests/files/types/shift_c4.gno | 2 +- gnovm/tests/files/types/shift_c6.gno | 2 +- gnovm/tests/files/types/shift_c7.gno | 2 +- gnovm/tests/files/types/shift_c8.gno | 2 +- gnovm/tests/files/types/shift_c9.gno | 2 +- gnovm/tests/files/types/shift_d12.gno | 2 +- gnovm/tests/files/types/shift_d13.gno | 2 +- gnovm/tests/files/types/shift_d29.gno | 2 +- gnovm/tests/files/types/shift_d30.gno | 2 +- gnovm/tests/files/types/shift_d32.gno | 2 +- gnovm/tests/files/types/shift_d32a.gno | 2 +- gnovm/tests/files/types/shift_d33.gno | 2 +- gnovm/tests/files/types/shift_d34.gno | 2 +- gnovm/tests/files/types/shift_d35.gno | 2 +- gnovm/tests/files/types/shift_d39.gno | 2 +- gnovm/tests/files/types/shift_d50.gno | 2 +- gnovm/tests/files/types/shift_d53.gno | 2 +- gnovm/tests/files/types/shift_d54.gno | 2 +- gnovm/tests/files/types/shift_d55.gno | 2 +- gnovm/tests/files/types/shift_d56.gno | 2 +- gnovm/tests/files/types/shift_f5.gno | 2 +- gnovm/tests/files/types/time_native.gno | 13 - gnovm/tests/files/zrealm0.gno | 4 +- gnovm/tests/files/zrealm1.gno | 4 +- gnovm/tests/files/zrealm12_stdlibs.gno | 29 - gnovm/tests/files/zrealm2.gno | 8 +- gnovm/tests/files/zrealm3.gno | 8 +- gnovm/tests/files/zrealm4.gno | 8 +- gnovm/tests/files/zrealm5.gno | 8 +- gnovm/tests/files/zrealm6.gno | 8 +- gnovm/tests/files/zrealm7.gno | 8 +- gnovm/tests/files/zrealm_avl0.gno | 8 +- gnovm/tests/files/zrealm_avl1.gno | 8 +- ...alm_const_stdlibs.gno => zrealm_const.gno} | 0 ...lm0_stdlibs.gno => zrealm_crossrealm0.gno} | 0 ...lm1_stdlibs.gno => zrealm_crossrealm1.gno} | 0 ...10_stdlibs.gno => zrealm_crossrealm10.gno} | 0 ...11_stdlibs.gno => zrealm_crossrealm11.gno} | 0 ...12_stdlibs.gno => zrealm_crossrealm12.gno} | 0 ...13_stdlibs.gno => zrealm_crossrealm13.gno} | 0 ...a_stdlibs.gno => zrealm_crossrealm13a.gno} | 0 ...lm2_stdlibs.gno => zrealm_crossrealm2.gno} | 0 ...lm3_stdlibs.gno => zrealm_crossrealm3.gno} | 0 ...lm4_stdlibs.gno => zrealm_crossrealm4.gno} | 0 ...lm5_stdlibs.gno => zrealm_crossrealm5.gno} | 0 ...lm6_stdlibs.gno => zrealm_crossrealm6.gno} | 0 ...lm7_stdlibs.gno => zrealm_crossrealm7.gno} | 0 ...lm8_stdlibs.gno => zrealm_crossrealm8.gno} | 0 ...lm9_stdlibs.gno => zrealm_crossrealm9.gno} | 0 ...initctx_stdlibs.gno => zrealm_initctx.gno} | 0 ...tbind0_stdlibs.gno => zrealm_natbind0.gno} | 8 +- gnovm/tests/files/zrealm_panic.gno | 7 +- ...realm_std0_stdlibs.gno => zrealm_std0.gno} | 0 ...realm_std1_stdlibs.gno => zrealm_std1.gno} | 0 ...realm_std2_stdlibs.gno => zrealm_std2.gno} | 0 ...realm_std3_stdlibs.gno => zrealm_std3.gno} | 0 ...realm_std4_stdlibs.gno => zrealm_std4.gno} | 0 ...realm_std5_stdlibs.gno => zrealm_std5.gno} | 0 ...realm_std6_stdlibs.gno => zrealm_std6.gno} | 0 ...m_tests0_stdlibs.gno => zrealm_tests0.gno} | 3 + ...ils0_stdlibs.gno => zrealm_testutils0.gno} | 0 .../{zregexp_stdlibs.gno => zregexp.gno} | 0 gnovm/tests/imports.go | 492 ------------ gnovm/tests/machine_test.go | 65 -- gnovm/tests/package_test.go | 90 --- gnovm/tests/selector_test.go | 174 ----- gnovm/tests/stdlibs/generated.go | 12 - gnovm/tests/stdlibs/std/std.gno | 1 - gnovm/tests/stdlibs/std/std.go | 85 ++- .../TestMemPackage/fail/file_test.gno | 7 - .../TestMemPackage/success/file_test.gno | 5 - 320 files changed, 1928 insertions(+), 4452 deletions(-) delete mode 100644 gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar delete mode 100644 gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar delete mode 100644 gnovm/pkg/gnolang/eval_test.go create mode 100644 gnovm/pkg/gnolang/files_test.go delete mode 100644 gnovm/pkg/gnolang/gonative_test.go delete mode 100644 gnovm/pkg/gnolang/preprocess_test.go create mode 100644 gnovm/pkg/test/filetest.go create mode 100644 gnovm/pkg/test/imports.go create mode 100644 gnovm/pkg/test/test.go rename gnovm/{cmd/gno => pkg/test}/util_match.go (99%) delete mode 100644 gnovm/tests/file.go delete mode 100644 gnovm/tests/file_test.go rename gnovm/tests/files/{access0_stdlibs.gno => access0.gno} (100%) rename gnovm/tests/files/{access1_stdlibs.gno => access1.gno} (52%) rename gnovm/tests/files/{access2_stdlibs.gno => access2.gno} (100%) rename gnovm/tests/files/{access3_stdlibs.gno => access3.gno} (100%) rename gnovm/tests/files/{access4_stdlibs.gno => access4.gno} (56%) rename gnovm/tests/files/{access5_stdlibs.gno => access5.gno} (100%) rename gnovm/tests/files/{access6_stdlibs.gno => access6.gno} (61%) rename gnovm/tests/files/{access7_stdlibs.gno => access7.gno} (67%) rename gnovm/tests/files/{addr0b_stdlibs.gno => addr0b.gno} (100%) delete mode 100644 gnovm/tests/files/addr0b_native.gno rename gnovm/tests/files/{assign0b_stdlibs.gno => assign0b.gno} (100%) delete mode 100644 gnovm/tests/files/assign0b_native.gno rename gnovm/tests/files/assign_unnamed_type/more/{cross_realm_compositelit_filetest_stdlibs.gno => cross_realm_compositelit_filetest.gno} (100%) delete mode 100644 gnovm/tests/files/bin5.gno delete mode 100644 gnovm/tests/files/binstruct_ptr_slice0.gno delete mode 100644 gnovm/tests/files/context.gno delete mode 100644 gnovm/tests/files/context2.gno delete mode 100644 gnovm/tests/files/defer4.gno delete mode 100644 gnovm/tests/files/extern/p1/s1.gno rename gnovm/tests/files/{float5_stdlibs.gno => float5.gno} (100%) delete mode 100644 gnovm/tests/files/fun6.gno delete mode 100644 gnovm/tests/files/fun6b.gno delete mode 100644 gnovm/tests/files/fun7.gno delete mode 100644 gnovm/tests/files/interp.gi delete mode 100644 gnovm/tests/files/interp2.gi rename gnovm/tests/files/{io0_stdlibs.gno => io0.gno} (100%) delete mode 100644 gnovm/tests/files/io0_native.gno rename gnovm/tests/files/{issue_558b_stdlibs.gno => issue_558b.gno} (97%) delete mode 100644 gnovm/tests/files/l3_long.gno delete mode 100644 gnovm/tests/files/l4_long.gno delete mode 100644 gnovm/tests/files/l5_long.gno rename gnovm/tests/files/{l2_long.gno => loop0.gno} (100%) create mode 100644 gnovm/tests/files/loop1.gno rename gnovm/tests/files/{map29_stdlibs.gno => map29.gno} (100%) delete mode 100644 gnovm/tests/files/map29_native.gno rename gnovm/tests/files/{math0_stdlibs.gno => math0.gno} (100%) rename gnovm/tests/files/{math_native.gno => math5.gno} (100%) delete mode 100644 gnovm/tests/files/method18.gno delete mode 100644 gnovm/tests/files/method24.gno delete mode 100644 gnovm/tests/files/method25.gno delete mode 100644 gnovm/tests/files/sample.plugin delete mode 100644 gnovm/tests/files/secure.gi rename gnovm/tests/files/{std0_stdlibs.gno => std0.gno} (100%) rename gnovm/tests/files/{std10_stdlibs.gno => std10.gno} (100%) rename gnovm/tests/files/{std11_stdlibs.gno => std11.gno} (100%) rename gnovm/tests/files/{std2_stdlibs.gno => std2.gno} (100%) rename gnovm/tests/files/{std3_stdlibs.gno => std3.gno} (100%) rename gnovm/tests/files/{std4_stdlibs.gno => std4.gno} (100%) rename gnovm/tests/files/{std5_stdlibs.gno => std5.gno} (90%) rename gnovm/tests/files/{std6_stdlibs.gno => std6.gno} (100%) rename gnovm/tests/files/{std7_stdlibs.gno => std7.gno} (100%) rename gnovm/tests/files/{std8_stdlibs.gno => std8.gno} (89%) rename gnovm/tests/files/{std9_stdlibs.gno => std9.gno} (100%) rename gnovm/tests/files/{stdbanker_stdlibs.gno => stdbanker.gno} (100%) rename gnovm/tests/files/{stdlibs_stdlibs.gno => stdlibs.gno} (100%) rename gnovm/tests/files/{struct13_stdlibs.gno => struct13.gno} (100%) delete mode 100644 gnovm/tests/files/struct13_native.gno rename gnovm/tests/files/{time0_stdlibs.gno => time0.gno} (100%) delete mode 100644 gnovm/tests/files/time0_native.gno rename gnovm/tests/files/{time1_stdlibs.gno => time1.gno} (100%) rename gnovm/tests/files/{time11_stdlibs.gno => time11.gno} (100%) delete mode 100644 gnovm/tests/files/time11_native.gno rename gnovm/tests/files/{time12_stdlibs.gno => time12.gno} (100%) delete mode 100644 gnovm/tests/files/time12_native.gno rename gnovm/tests/files/{time13_stdlibs.gno => time13.gno} (100%) delete mode 100644 gnovm/tests/files/time13_native.gno rename gnovm/tests/files/{time14_stdlibs.gno => time14.gno} (100%) delete mode 100644 gnovm/tests/files/time14_native.gno delete mode 100644 gnovm/tests/files/time16_native.gno delete mode 100644 gnovm/tests/files/time17_native.gno delete mode 100644 gnovm/tests/files/time1_native.gno rename gnovm/tests/files/{time2_stdlibs.gno => time2.gno} (100%) delete mode 100644 gnovm/tests/files/time2_native.gno rename gnovm/tests/files/{time3_stdlibs.gno => time3.gno} (100%) delete mode 100644 gnovm/tests/files/time3_native.gno rename gnovm/tests/files/{time4_stdlibs.gno => time4.gno} (100%) delete mode 100644 gnovm/tests/files/time4_native.gno rename gnovm/tests/files/{time6_stdlibs.gno => time6.gno} (100%) delete mode 100644 gnovm/tests/files/time6_native.gno rename gnovm/tests/files/{time7_stdlibs.gno => time7.gno} (100%) delete mode 100644 gnovm/tests/files/time7_native.gno rename gnovm/tests/files/{time9_stdlibs.gno => time9.gno} (100%) delete mode 100644 gnovm/tests/files/time9_native.gno rename gnovm/tests/files/{type2_stdlibs.gno => type2.gno} (100%) delete mode 100644 gnovm/tests/files/type2_native.gno rename gnovm/tests/files/{typeassert7_native.gno => typeassert7.gno} (100%) rename gnovm/tests/files/{typeassert7a_native.gno => typeassert7a.gno} (87%) rename gnovm/tests/files/types/{add_assign_f0_stdlibs.gno => add_assign_f0.gno} (71%) rename gnovm/tests/files/types/{add_assign_f1_stdlibs.gno => add_assign_f1.gno} (81%) rename gnovm/tests/files/types/{add_assign_f2_stdlibs.gno => add_assign_f2.gno} (75%) rename gnovm/tests/files/types/{add_f0_stdlibs.gno => add_f0.gno} (74%) rename gnovm/tests/files/types/{add_f1_stdlibs.gno => add_f1.gno} (75%) rename gnovm/tests/files/types/{and_f0_stdlibs.gno => and_f0.gno} (74%) rename gnovm/tests/files/types/{and_f1_stdlibs.gno => and_f1.gno} (75%) rename gnovm/tests/files/types/{cmp_iface_0_stdlibs.gno => cmp_iface_0.gno} (100%) rename gnovm/tests/files/types/{cmp_iface_3_stdlibs.gno => cmp_iface_3.gno} (100%) rename gnovm/tests/files/types/{eql_0f8_stdlibs.gno => cmp_iface_5.gno} (75%) rename gnovm/tests/files/types/{eql_0b4_native.gno => eql_0b4.gno} (50%) delete mode 100644 gnovm/tests/files/types/eql_0b4_stdlibs.gno rename gnovm/tests/files/types/{eql_0f0_native.gno => eql_0f0.gno} (75%) delete mode 100644 gnovm/tests/files/types/eql_0f0_stdlibs.gno rename gnovm/tests/files/types/{eql_0f1_stdlibs.gno => eql_0f1.gno} (76%) rename gnovm/tests/files/types/{eql_0f27_stdlibs.gno => eql_0f27.gno} (75%) rename gnovm/tests/files/types/{eql_0f2b_native.gno => eql_0f2b.gno} (80%) delete mode 100644 gnovm/tests/files/types/eql_0f2b_stdlibs.gno rename gnovm/tests/files/types/{eql_0f2c_native.gno => eql_0f2c.gno} (80%) delete mode 100644 gnovm/tests/files/types/eql_0f2c_stdlibs.gno rename gnovm/tests/files/types/{eql_0f40_stdlibs.gno => eql_0f40.gno} (100%) rename gnovm/tests/files/types/{eql_0f41_stdlibs.gno => eql_0f41.gno} (77%) rename gnovm/tests/files/types/{cmp_iface_5_stdlibs.gno => eql_0f8.gno} (75%) rename gnovm/tests/files/types/{or_f0_stdlibs.gno => or_f0.gno} (75%) rename gnovm/tests/files/types/{or_f1_stdlibs.gno => or_f1.gno} (75%) delete mode 100644 gnovm/tests/files/types/time_native.gno delete mode 100644 gnovm/tests/files/zrealm12_stdlibs.gno rename gnovm/tests/files/{zrealm_const_stdlibs.gno => zrealm_const.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm0_stdlibs.gno => zrealm_crossrealm0.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm1_stdlibs.gno => zrealm_crossrealm1.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm10_stdlibs.gno => zrealm_crossrealm10.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm11_stdlibs.gno => zrealm_crossrealm11.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm12_stdlibs.gno => zrealm_crossrealm12.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm13_stdlibs.gno => zrealm_crossrealm13.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm13a_stdlibs.gno => zrealm_crossrealm13a.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm2_stdlibs.gno => zrealm_crossrealm2.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm3_stdlibs.gno => zrealm_crossrealm3.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm4_stdlibs.gno => zrealm_crossrealm4.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm5_stdlibs.gno => zrealm_crossrealm5.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm6_stdlibs.gno => zrealm_crossrealm6.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm7_stdlibs.gno => zrealm_crossrealm7.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm8_stdlibs.gno => zrealm_crossrealm8.gno} (100%) rename gnovm/tests/files/{zrealm_crossrealm9_stdlibs.gno => zrealm_crossrealm9.gno} (100%) rename gnovm/tests/files/{zrealm_initctx_stdlibs.gno => zrealm_initctx.gno} (100%) rename gnovm/tests/files/{zrealm_natbind0_stdlibs.gno => zrealm_natbind0.gno} (95%) rename gnovm/tests/files/{zrealm_std0_stdlibs.gno => zrealm_std0.gno} (100%) rename gnovm/tests/files/{zrealm_std1_stdlibs.gno => zrealm_std1.gno} (100%) rename gnovm/tests/files/{zrealm_std2_stdlibs.gno => zrealm_std2.gno} (100%) rename gnovm/tests/files/{zrealm_std3_stdlibs.gno => zrealm_std3.gno} (100%) rename gnovm/tests/files/{zrealm_std4_stdlibs.gno => zrealm_std4.gno} (100%) rename gnovm/tests/files/{zrealm_std5_stdlibs.gno => zrealm_std5.gno} (100%) rename gnovm/tests/files/{zrealm_std6_stdlibs.gno => zrealm_std6.gno} (100%) rename gnovm/tests/files/{zrealm_tests0_stdlibs.gno => zrealm_tests0.gno} (99%) rename gnovm/tests/files/{zrealm_testutils0_stdlibs.gno => zrealm_testutils0.gno} (100%) rename gnovm/tests/files/{zregexp_stdlibs.gno => zregexp.gno} (100%) delete mode 100644 gnovm/tests/imports.go delete mode 100644 gnovm/tests/machine_test.go delete mode 100644 gnovm/tests/package_test.go delete mode 100644 gnovm/tests/selector_test.go delete mode 100644 gnovm/tests/testdata/TestMemPackage/fail/file_test.gno delete mode 100644 gnovm/tests/testdata/TestMemPackage/success/file_test.gno diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 7c4bb35526f..41d579c4567 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -3,7 +3,7 @@ name: examples on: pull_request: push: - branches: [ "master" ] + branches: ["master"] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -70,7 +70,7 @@ jobs: strategy: fail-fast: false matrix: - goversion: [ "1.22.x" ] + goversion: ["1.22.x"] runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -86,7 +86,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [ "1.22.x" ] + go-version: ["1.22.x"] # unittests: TODO: matrix with contracts runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 7e7586b23d9..8311d113047 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -13,6 +13,8 @@ jobs: uses: ./.github/workflows/main_template.yml with: modulepath: "gnovm" + # in pull requests, append -short so that the CI runs quickly. + tests-extra-args: ${{ github.event_name == 'pull_request' && '-short' || '' }} secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} fmt: diff --git a/examples/gno.land/p/demo/avl/pager/z_filetest.gno b/examples/gno.land/p/demo/avl/pager/z_filetest.gno index 91c20115469..17029f57861 100644 --- a/examples/gno.land/p/demo/avl/pager/z_filetest.gno +++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno @@ -99,3 +99,4 @@ func main() { // // ## Page 7 of 6 // [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_ +// diff --git a/examples/gno.land/p/demo/avl/z_0_filetest.gno b/examples/gno.land/p/demo/avl/z_0_filetest.gno index aff79ffabc6..2dce5e7f1ac 100644 --- a/examples/gno.land/p/demo/avl/z_0_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_0_filetest.gno @@ -267,7 +267,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_0.gno", // "IsMethod": false, // "Name": "init.1", // "NativeName": "", @@ -278,7 +278,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_0.gno", // "Line": "10", // "PkgPath": "gno.land/r/test" // } @@ -303,7 +303,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_0.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -314,7 +314,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_0.gno", // "Line": "15", // "PkgPath": "gno.land/r/test" // } diff --git a/examples/gno.land/p/demo/avl/z_1_filetest.gno b/examples/gno.land/p/demo/avl/z_1_filetest.gno index 3b6d40d5ecd..97ca5ed2135 100644 --- a/examples/gno.land/p/demo/avl/z_1_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_1_filetest.gno @@ -340,7 +340,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_1.gno", // "IsMethod": false, // "Name": "init.1", // "NativeName": "", @@ -351,7 +351,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_1.gno", // "Line": "10", // "PkgPath": "gno.land/r/test" // } @@ -376,7 +376,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_1.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -387,7 +387,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_1.gno", // "Line": "15", // "PkgPath": "gno.land/r/test" // } diff --git a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno index 4b2c04b6d5c..17d6c466ed5 100644 --- a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno +++ b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno @@ -44,6 +44,7 @@ func main() { } // Output: +// // -- INITIAL // // # Gnome 😃 diff --git a/examples/gno.land/r/demo/banktest/z_0_filetest.gno b/examples/gno.land/r/demo/banktest/z_0_filetest.gno index 61289dfe071..5a8c8d70a48 100644 --- a/examples/gno.land/r/demo/banktest/z_0_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_0_filetest.gno @@ -42,9 +42,9 @@ func main() { } // Output: -// main before: 300000000ugnot +// main before: 100000000ugnot // Deposit(): returned! -// main after: 250000000ugnot +// main after: 50000000ugnot // ## recent activity // // * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC diff --git a/examples/gno.land/r/demo/banktest/z_2_filetest.gno b/examples/gno.land/r/demo/banktest/z_2_filetest.gno index 2dc9c84e767..e839f60354a 100644 --- a/examples/gno.land/r/demo/banktest/z_2_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_2_filetest.gno @@ -39,9 +39,9 @@ func main() { } // Output: -// main before: 200000000ugnot +// main before: // Deposit(): returned! -// main after: 255000000ugnot +// main after: 55000000ugnot // ## recent activity // // * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno index 4fc224da9b2..a649895cb01 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -37,3 +37,5 @@ func main() { // // Body of the second post. (body) // \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) +// +// diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno index f746877b5c7..7dd460500d6 100644 --- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno @@ -46,3 +46,4 @@ func main() { // // Body of the first post. (body) // \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno index de2b6aa463b..f64b4c84bba 100644 --- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno @@ -50,3 +50,4 @@ func main() { // > Edited: First reply of the First post // > // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno index 49ee0ee0273..3f56293b3bd 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -40,3 +40,4 @@ func main() { // // Edited: Body of the first post. (body) // \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno index 02953352dd2..ac4adf6ee7b 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -38,3 +38,5 @@ func main() { // // Body of the first post. (body) // \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) +// +// diff --git a/examples/gno.land/r/demo/boards/z_1_filetest.gno b/examples/gno.land/r/demo/boards/z_1_filetest.gno index ba0a277e2f1..4d46c81b83d 100644 --- a/examples/gno.land/r/demo/boards/z_1_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_1_filetest.gno @@ -26,3 +26,4 @@ func main() { // // * [/r/demo/boards:test_board_1](/r/demo/boards:test_board_1) // * [/r/demo/boards:test_board_2](/r/demo/boards:test_board_2) +// diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno index 53c0a1965da..31b39644b24 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -36,3 +36,4 @@ func main() { // // > Reply of the second post // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno index 89e5a3f12ff..0b2a2df2f91 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -38,3 +38,4 @@ func main() { // // > Reply of the second post // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index fa0b9e20be5..c6cf6397b3a 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -44,6 +44,7 @@ func main() { // // > Second reply of the second post // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// // Realm: // switchrealm["gno.land/r/demo/users"] diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno index b20d8cdfed8..723e6a10204 100644 --- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno @@ -37,3 +37,4 @@ func main() { // // > Reply of the first post // > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno index c0614bb9da3..712af483891 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -41,3 +41,4 @@ func main() { // > Second reply of the second post // > // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno index 6ddd8b9cf3f..ec40cf5f8e9 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -47,3 +47,4 @@ func main() { // > Second reply of the second post // > // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno index 534095b99cf..353b84f6d87 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -29,3 +29,5 @@ func main() { // // Body of the first post. (body) // \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// +// diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno index f5477026805..4896dfcfccf 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -42,3 +42,4 @@ func main() { // > First reply of the first reply // > // > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno index 4be9d2bdfa6..ca37e306bda 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -35,3 +35,4 @@ func main() { // // Body of the first post. (body) // \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)] +// diff --git a/examples/gno.land/r/demo/disperse/z_0_filetest.gno b/examples/gno.land/r/demo/disperse/z_0_filetest.gno index e54b34ae3bf..ca1e9ea0ce8 100644 --- a/examples/gno.land/r/demo/disperse/z_0_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_0_filetest.gno @@ -30,5 +30,5 @@ func main() { } // Output: -// main before: 200000200ugnot -// main after: 200000000ugnot +// main before: 200ugnot +// main after: diff --git a/examples/gno.land/r/demo/disperse/z_1_filetest.gno b/examples/gno.land/r/demo/disperse/z_1_filetest.gno index 62018fe965e..4c27c50749f 100644 --- a/examples/gno.land/r/demo/disperse/z_1_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_1_filetest.gno @@ -30,5 +30,5 @@ func main() { } // Output: -// main before: 200000300ugnot -// main after: 200000100ugnot +// main before: 300ugnot +// main after: 100ugnot diff --git a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno index cf5902928db..60600e38b78 100644 --- a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno @@ -22,3 +22,4 @@ func main() { // List of all Groups: // // * [test_group](/r/demo/groups:test_group) +// diff --git a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno index 18799e31a67..71da1b966ec 100644 --- a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno @@ -76,3 +76,5 @@ func main() { // Group Members: // // [0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001], +// +// diff --git a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno index 7c97b01ccf5..0c482e1b52f 100644 --- a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno @@ -76,3 +76,5 @@ func main() { // Group Last MemberID: 0000000001 // // Group Members: +// +// diff --git a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno index cbfff97c7a7..ff38acf45a4 100644 --- a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno @@ -21,3 +21,5 @@ func main() { // Output: // 1 // List of all Groups: +// +// diff --git a/examples/gno.land/r/demo/releases_example/releases0_filetest.gno b/examples/gno.land/r/demo/releases_example/releases0_filetest.gno index 193f9bdbc90..ca599a54892 100644 --- a/examples/gno.land/r/demo/releases_example/releases0_filetest.gno +++ b/examples/gno.land/r/demo/releases_example/releases0_filetest.gno @@ -49,3 +49,4 @@ func main() { // // * various improvements // * new shiny logo +// diff --git a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno index 1ea56b4a3f9..4072c0b30d7 100644 --- a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno +++ b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno @@ -23,3 +23,4 @@ func main() { // * [Play](/r/demo/tamagotchi$help&func=Play) // * [Heal](/r/demo/tamagotchi$help&func=Heal) // * [Reset](/r/demo/tamagotchi$help&func=Reset) +// diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno index dcb957f2155..6465cc9c378 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -38,7 +38,7 @@ func main() { } // Output: -// * [archives](/r/demo/users:archives) +// * [archives](/r/demo/users:archives) // * [demo](/r/demo/users:demo) // * [gno](/r/demo/users:gno) // * [gnoland](/r/demo/users:gnoland) diff --git a/examples/gno.land/r/demo/wugnot/z0_filetest.gno b/examples/gno.land/r/demo/wugnot/z0_filetest.gno index bef65c03b68..264bc8f19aa 100644 --- a/examples/gno.land/r/demo/wugnot/z0_filetest.gno +++ b/examples/gno.land/r/demo/wugnot/z0_filetest.gno @@ -55,17 +55,17 @@ func printBalances() { // Output: // ----------- -// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=200000000 | +// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=0 | // | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 | // | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 | // ----------- // ----------- -// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=200000000 | +// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=0 | // | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 | // | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 | // ----------- // ----------- -// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=200004242 | +// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=4242 | // | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=99995759 | // | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 | // ----------- diff --git a/examples/gno.land/r/gnoland/faucet/faucet_test.gno b/examples/gno.land/r/gnoland/faucet/faucet_test.gno index 1f492adb2dc..cecbb2ebcd6 100644 --- a/examples/gno.land/r/gnoland/faucet/faucet_test.gno +++ b/examples/gno.land/r/gnoland/faucet/faucet_test.gno @@ -28,7 +28,7 @@ func TestPackage(t *testing.T) { ) // deposit 1000gnot to faucet contract std.TestIssueCoins(faucetaddr, std.Coins{{"ugnot", 1000000000}}) - assertBalance(t, faucetaddr, 1200000000) + assertBalance(t, faucetaddr, 1000000000) // by default, balance is empty, and as a user I cannot call Transfer, or Admin commands. @@ -43,7 +43,7 @@ func TestPackage(t *testing.T) { // as an admin, add the controller to contract and deposit more 2000gnot to contract std.TestSetOrigCaller(adminaddr) assertNoErr(t, faucet.AdminAddController(controlleraddr1)) - assertBalance(t, faucetaddr, 1200000000) + assertBalance(t, faucetaddr, 1000000000) // now, send some tokens as controller. std.TestSetOrigCaller(controlleraddr1) @@ -51,7 +51,7 @@ func TestPackage(t *testing.T) { assertBalance(t, test1addr, 1000000) assertNoErr(t, faucet.Transfer(test1addr, 1000000)) assertBalance(t, test1addr, 2000000) - assertBalance(t, faucetaddr, 1198000000) + assertBalance(t, faucetaddr, 998000000) // remove controller // as an admin, remove controller diff --git a/examples/gno.land/r/gnoland/faucet/z0_filetest.gno b/examples/gno.land/r/gnoland/faucet/z0_filetest.gno index bcc75897c85..7e729bdd358 100644 --- a/examples/gno.land/r/gnoland/faucet/z0_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z0_filetest.gno @@ -33,3 +33,5 @@ func main() { // // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/faucet/z1_filetest.gno b/examples/gno.land/r/gnoland/faucet/z1_filetest.gno index 6afb14b024b..c6fd6298488 100644 --- a/examples/gno.land/r/gnoland/faucet/z1_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z1_filetest.gno @@ -33,3 +33,5 @@ func main() { // // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/faucet/z2_filetest.gno b/examples/gno.land/r/gnoland/faucet/z2_filetest.gno index 054e5329476..d0616b3afcd 100644 --- a/examples/gno.land/r/gnoland/faucet/z2_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z2_filetest.gno @@ -48,3 +48,5 @@ func main() { // g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/faucet/z3_filetest.gno b/examples/gno.land/r/gnoland/faucet/z3_filetest.gno index 4a48ca390e2..0da06593710 100644 --- a/examples/gno.land/r/gnoland/faucet/z3_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z3_filetest.gno @@ -60,3 +60,5 @@ func main() { // g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index e889dde4f48..7b25eeb1db3 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -126,6 +126,7 @@ func main() { // - #123: g12345678 (10) // - #123: g000000000 (10) // - #123: g000000000 (0) +// // Events: // [ diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno index c90e76727da..8eff79ffb5a 100644 --- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -80,6 +80,8 @@ func main() { // Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) // // Threshold met: true +// +// // Events: // [ diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno index 5203e07ada7..b26c919dd3a 100644 --- a/examples/gno.land/r/moul/home/z1_filetest.gno +++ b/examples/gno.land/r/moul/home/z1_filetest.gno @@ -17,3 +17,5 @@ func main() { // // ## Personal ToDo List // - [ ] fill this todo list... +// +// diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno index 02d08cd591e..489dc2aeecd 100644 --- a/examples/gno.land/r/moul/home/z2_filetest.gno +++ b/examples/gno.land/r/moul/home/z2_filetest.gno @@ -33,3 +33,5 @@ func main() { // - [ ] bbb // - [ ] ddd // - [ ] eee +// +// diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 5fa2075b8f7..0dca794ee71 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -365,7 +365,6 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { ChainID: ctx.ChainID(), Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), - Msg: msg, OrigCaller: creator.Bech32(), OrigSend: deposit, OrigSendSpent: new(std.Coins), @@ -466,7 +465,6 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { ChainID: ctx.ChainID(), Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), - Msg: msg, OrigCaller: caller.Bech32(), OrigSend: send, OrigSendSpent: new(std.Coins), @@ -565,7 +563,6 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { ChainID: ctx.ChainID(), Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), - Msg: msg, OrigCaller: caller.Bech32(), OrigSend: send, OrigSendSpent: new(std.Coins), @@ -729,7 +726,6 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res ChainID: ctx.ChainID(), Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), - // Msg: msg, // OrigCaller: caller, // OrigSend: send, // OrigSendSpent: nil, @@ -796,7 +792,6 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string ChainID: ctx.ChainID(), Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), - // Msg: msg, // OrigCaller: caller, // OrigSend: jsend, // OrigSendSpent: nil, diff --git a/gnovm/Makefile b/gnovm/Makefile index d27395d9cd1..31daf942554 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -64,7 +64,7 @@ imports: ######################################## # Test suite .PHONY: test -test: _test.cmd _test.pkg _test.gnolang +test: _test.cmd _test.pkg _test.stdlibs .PHONY: _test.cmd _test.cmd: @@ -92,20 +92,10 @@ test.cmd.coverage_view: test.cmd.coverage _test.pkg: go test ./pkg/... $(GOTEST_FLAGS) -.PHONY: _test.gnolang -_test.gnolang: _test.gnolang.native _test.gnolang.stdlibs _test.gnolang.realm _test.gnolang.pkg0 _test.gnolang.pkg1 _test.gnolang.pkg2 _test.gnolang.other -_test.gnolang.other:; go test tests/*.go -run "(TestFileStr|TestSelectors)" $(GOTEST_FLAGS) -_test.gnolang.realm:; go test tests/*.go -run "TestFiles/^zrealm" $(GOTEST_FLAGS) -_test.gnolang.pkg0:; go test tests/*.go -run "TestStdlibs/(bufio|crypto|encoding|errors|internal|io|math|sort|std|strconv|strings|testing|unicode)" $(GOTEST_FLAGS) -_test.gnolang.pkg1:; go test tests/*.go -run "TestStdlibs/regexp" $(GOTEST_FLAGS) -_test.gnolang.pkg2:; go test tests/*.go -run "TestStdlibs/bytes" $(GOTEST_FLAGS) -_test.gnolang.native:; go test tests/*.go -test.short -run "TestFilesNative/" $(GOTEST_FLAGS) -_test.gnolang.stdlibs:; go test tests/*.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS) -_test.gnolang.native.sync:; go test tests/*.go -test.short -run "TestFilesNative/" --update-golden-tests $(GOTEST_FLAGS) -_test.gnolang.stdlibs.sync:; go test tests/*.go -test.short -run 'TestFiles$$/' --update-golden-tests $(GOTEST_FLAGS) -# NOTE: challenges are current GnoVM bugs which are supposed to fail. -# If any of these tests pass, it should be moved to a normal test. -_test.gnolang.challenges:; go test tests/*.go -test.short -run 'TestChallenges$$/' $(GOTEST_FLAGS) +.PHONY: _test.stdlibs +_test.stdlibs: + go run ./cmd/gno test -v ./stdlibs/... + ######################################## # Code gen diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index c6008117f13..ef35cf9af83 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -14,7 +14,7 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/tests" + "github.com/gnolang/gno/gnovm/pkg/test" "github.com/gnolang/gno/tm2/pkg/commands" osm "github.com/gnolang/gno/tm2/pkg/os" "go.uber.org/multierr" @@ -91,10 +91,9 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { // Handle runtime errors hasError = catchRuntimeError(pkgPath, io.Err(), func() { stdout, stdin, stderr := io.Out(), io.In(), io.Err() - testStore := tests.TestStore( - rootDir, "", + _, testStore := test.Store( + rootDir, false, stdin, stdout, stderr, - tests.ImportModeStdlibsOnly, ) targetPath := pkgPath @@ -104,7 +103,8 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { } memPkg := gno.ReadMemPackage(targetPath, targetPath) - tm := tests.TestMachine(testStore, stdout, memPkg.Name) + tm := test.Machine(testStore, stdout, memPkg.Path) + defer tm.Release() // Check package tm.RunMemPackage(memPkg, true) @@ -161,7 +161,7 @@ func guessSourcePath(pkg, source string) string { // reParseRecover is a regex designed to parse error details from a string. // It extracts the file location, line number, and error message from a formatted error string. // XXX: Ideally, error handling should encapsulate location details within a dedicated error type. -var reParseRecover = regexp.MustCompile(`^([^:]+):(\d+)(?::\d+)?:? *(.*)$`) +var reParseRecover = regexp.MustCompile(`^([^:]+)((?::(?:\d+)){1,2}):? *(.*)$`) func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (hasError bool) { defer func() { @@ -230,9 +230,9 @@ func issueFromError(pkgPath string, err error) lintIssue { parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") matches := reParseRecover.FindStringSubmatch(parsedError) - if len(matches) == 4 { + if len(matches) > 0 { sourcepath := guessSourcePath(pkgPath, matches[1]) - issue.Location = fmt.Sprintf("%s:%s", sourcepath, matches[2]) + issue.Location = sourcepath + matches[2] issue.Msg = strings.TrimSpace(matches[3]) } else { issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath)) diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 20d21c05d05..031c252bc79 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -13,19 +13,19 @@ func TestLintApp(t *testing.T) { errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, - stderrShouldContain: "undefined_variables_test.gno:6: name toto not declared (code=2)", + stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"}, - stderrShouldContain: "main.gno:4: name fmt not declared (code=2).", + stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2).", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6", + stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6: expected '}', found 'EOF' (code=2).\n", + stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/run_main/"}, diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index f174c2b4cc5..9a9beac5cd1 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/tests" + "github.com/gnolang/gno/gnovm/pkg/test" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -92,9 +92,9 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { stderr := io.Err() // init store and machine - testStore := tests.TestStore(cfg.rootDir, - "", stdin, stdout, stderr, - tests.ImportModeStdlibsPreferred) + _, testStore := test.Store( + cfg.rootDir, false, + stdin, stdout, stderr) if cfg.verbose { testStore.SetLogStoreOps(true) } @@ -115,7 +115,7 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { var send std.Coins pkgPath := string(files[0].PkgName) - ctx := tests.TestContext(pkgPath, send) + ctx := test.Context(pkgPath, send) m := gno.NewMachineWithOptions(gno.MachineOptions{ PkgPath: pkgPath, Output: stdout, diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index e5aa1bd6279..74f99f7490c 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -85,7 +85,7 @@ func TestRunApp(t *testing.T) { }, { args: []string{"run", "../../tests/integ/several-files-multiple-errors/"}, - stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6: expected '}', found 'EOF' (code=2).", + stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", errShouldBe: "exit code: 1", }, // TODO: a test file diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index d54b12f6a4f..04a3808718d 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -1,33 +1,21 @@ package main import ( - "bytes" "context" - "encoding/json" "flag" "fmt" + goio "io" "log" - "math" - "os" "path/filepath" - "runtime/debug" - "sort" "strings" - "text/template" "time" - "go.uber.org/multierr" - - "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" - "github.com/gnolang/gno/gnovm/tests" - teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" + "github.com/gnolang/gno/gnovm/pkg/test" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/random" - "github.com/gnolang/gno/tm2/pkg/testutils" ) type testCfg struct { @@ -38,7 +26,6 @@ type testCfg struct { updateGoldenTests bool printRuntimeMetrics bool printEvents bool - withNativeFallback bool } func newTestCmd(io commands.IO) *commands.Command { @@ -66,34 +53,38 @@ module name found in 'gno.mod', or else it is randomly generated like - "*_filetest.gno" files on the other hand are kind of unique. They exist to provide a way to interact and assert a gno contract, thanks to a set of -specific instructions that can be added using code comments. +specific directives that can be added using code comments. "*_filetest.gno" must be declared in the 'main' package and so must have a 'main' function, that will be executed to test the target contract. -List of available instructions that can be used in "*_filetest.gno" files: - - "PKGPATH:" is a single line instruction that can be used to define the +These single-line directives can set "input parameters" for the machine used +to perform the test: + - "PKGPATH:" is a single line directive that can be used to define the package used to interact with the tested package. If not specified, "main" is used. - - "MAXALLOC:" is a signle line instruction that can be used to define a limit + - "MAXALLOC:" is a single line directive that can be used to define a limit to the VM allocator. If this limit is exceeded, the VM will panic. Default to 0, no limit. - - "SEND:" is a single line instruction that can be used to send an amount of + - "SEND:" is a single line directive that can be used to send an amount of token along with the transaction. The format is for example "1000000ugnot". Default is empty. - - "Output:\n" (*) is a multiple lines instruction that can be used to assert - the output of the "*_filetest.gno" file. Any prints executed inside the - 'main' function must match the lines that follows the "Output:\n" - instruction, or else the test fails. - - "Error:\n" works similarly to "Output:\n", except that it asserts the - stderr of the program, which in that case, comes from the VM because of a - panic, rather than the 'main' function. - - "Realm:\n" (*) is a multiple lines instruction that can be used to assert - what has been recorded in the store following the execution of the 'main' - function. - -(*) The 'update-golden-tests' flag can be set to fill out the content of the -instruction with the actual content of the test instead of failing. + +These directives, instead, match the comment that follows with the result +of the GnoVM, acting as a "golden test": + - "Output:" tests the following comment with the standard output of the + filetest. + - "Error:" tests the following comment with any panic, or other kind of + error that the filetest generates (like a parsing or preprocessing error). + - "Realm:" tests the following comment against the store log, which can show + what realm information is stored. + - "Stacktrace:" can be used to verify the following lines against the + stacktrace of the error. + - "Events:" can be used to verify the emitted events against a JSON. + +To speed up execution, imports of pure packages are processed separately from +the execution of the tests. This makes testing faster, but means that the +initialization of imported pure packages cannot be checked in filetests. `, }, cfg, @@ -115,7 +106,7 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { &c.updateGoldenTests, "update-golden-tests", false, - `writes actual as wanted for "Output:" and "Realm:" instructions`, + `writes actual as wanted for "golden" directives in filetests`, ) fs.StringVar( @@ -139,13 +130,6 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { "max execution time", ) - fs.BoolVar( - &c.withNativeFallback, - "with-native-fallback", - false, - "use stdlibs/* if present, otherwise use supported native Go packages", - ) - fs.BoolVar( &c.printRuntimeMetrics, "print-runtime-metrics", @@ -192,6 +176,18 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { return fmt.Errorf("list sub packages: %w", err) } + // Set up options to run tests. + stdout := goio.Discard + if cfg.verbose { + stdout = io.Out() + } + opts := test.NewTestOptions(cfg.rootDir, io.In(), stdout, io.Err()) + opts.RunFlag = cfg.run + opts.Sync = cfg.updateGoldenTests + opts.Verbose = cfg.verbose + opts.Metrics = cfg.printRuntimeMetrics + opts.Events = cfg.printEvents + buildErrCount := 0 testErrCount := 0 for _, pkg := range subPkgs { @@ -199,200 +195,48 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { io.ErrPrintfln("? %s \t[no test files]", pkg.Dir) continue } - - sort.Strings(pkg.TestGnoFiles) - sort.Strings(pkg.FiletestGnoFiles) - - startedAt := time.Now() - err = gnoTestPkg(pkg.Dir, pkg.TestGnoFiles, pkg.FiletestGnoFiles, cfg, io) - duration := time.Since(startedAt) - dstr := fmtDuration(duration) - - if err != nil { - io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err) - io.ErrPrintfln("FAIL") - io.ErrPrintfln("FAIL %s \t%s", pkg.Dir, dstr) - io.ErrPrintfln("FAIL") - testErrCount++ - } else { - io.ErrPrintfln("ok %s \t%s", pkg.Dir, dstr) - } - } - if testErrCount > 0 || buildErrCount > 0 { - io.ErrPrintfln("FAIL") - return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount) - } - - return nil -} - -func gnoTestPkg( - pkgPath string, - unittestFiles, - filetestFiles []string, - cfg *testCfg, - io commands.IO, -) error { - var ( - verbose = cfg.verbose - rootDir = cfg.rootDir - runFlag = cfg.run - printRuntimeMetrics = cfg.printRuntimeMetrics - printEvents = cfg.printEvents - - stdin = io.In() - stdout = io.Out() - stderr = io.Err() - errs error - ) - - mode := tests.ImportModeStdlibsOnly - if cfg.withNativeFallback { - // XXX: display a warn? - mode = tests.ImportModeStdlibsPreferred - } - if !verbose { - // TODO: speedup by ignoring if filter is file/*? - mockOut := bytes.NewBufferString("") - stdout = commands.WriteNopCloser(mockOut) - } - - // testing with *_test.gno - if len(unittestFiles) > 0 { // Determine gnoPkgPath by reading gno.mod var gnoPkgPath string - modfile, err := gnomod.ParseAt(pkgPath) + modfile, err := gnomod.ParseAt(pkg.Dir) if err == nil { gnoPkgPath = modfile.Module.Mod.Path } else { - gnoPkgPath = pkgPathFromRootDir(pkgPath, rootDir) + gnoPkgPath = pkgPathFromRootDir(pkg.Dir, cfg.rootDir) if gnoPkgPath == "" { // unable to read pkgPath from gno.mod, generate a random realm path io.ErrPrintfln("--- WARNING: unable to read package path from gno.mod or gno root directory; try creating a gno.mod file") - gnoPkgPath = gno.RealmPathPrefix + random.RandStr(8) + gnoPkgPath = gno.RealmPathPrefix + strings.ToLower(random.RandStr(8)) } } - memPkg := gno.ReadMemPackage(pkgPath, gnoPkgPath) - // tfiles, ifiles := gno.ParseMemPackageTests(memPkg) - var tfiles, ifiles *gno.FileSet + memPkg := gno.ReadMemPackage(pkg.Dir, gnoPkgPath) - hasError := catchRuntimeError(gnoPkgPath, stderr, func() { - tfiles, ifiles = parseMemPackageTests(memPkg) + startedAt := time.Now() + hasError := catchRuntimeError(gnoPkgPath, io.Err(), func() { + err = test.Test(memPkg, pkg.Dir, opts) }) - if hasError { - return commands.ExitCodeError(1) - } - testPkgName := getPkgNameFromFileset(ifiles) - - // run test files in pkg - if len(tfiles.Files) > 0 { - testStore := tests.TestStore( - rootDir, "", - stdin, stdout, stderr, - mode, - ) - if verbose { - testStore.SetLogStoreOps(true) - } - - m := tests.TestMachine(testStore, stdout, gnoPkgPath) - if printRuntimeMetrics { - // from tm2/pkg/sdk/vm/keeper.go - // XXX: make maxAllocTx configurable. - maxAllocTx := int64(math.MaxInt64) - - m.Alloc = gno.NewAllocator(maxAllocTx) - } - m.RunMemPackage(memPkg, true) - err := runTestFiles(m, tfiles, memPkg.Name, verbose, printRuntimeMetrics, printEvents, runFlag, io) - if err != nil { - errs = multierr.Append(errs, err) - } - } - - // test xxx_test pkg - if len(ifiles.Files) > 0 { - testStore := tests.TestStore( - rootDir, "", - stdin, stdout, stderr, - mode, - ) - if verbose { - testStore.SetLogStoreOps(true) - } - - m := tests.TestMachine(testStore, stdout, testPkgName) - - memFiles := make([]*gnovm.MemFile, 0, len(ifiles.FileNames())+1) - for _, f := range memPkg.Files { - for _, ifileName := range ifiles.FileNames() { - if f.Name == "gno.mod" || f.Name == ifileName { - memFiles = append(memFiles, f) - break - } - } - } - - memPkg.Files = memFiles - memPkg.Name = testPkgName - memPkg.Path = memPkg.Path + "_test" - m.RunMemPackage(memPkg, true) + duration := time.Since(startedAt) + dstr := fmtDuration(duration) - err := runTestFiles(m, ifiles, testPkgName, verbose, printRuntimeMetrics, printEvents, runFlag, io) + if hasError || err != nil { if err != nil { - errs = multierr.Append(errs, err) + io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err) } + io.ErrPrintfln("FAIL") + io.ErrPrintfln("FAIL %s \t%s", pkg.Dir, dstr) + io.ErrPrintfln("FAIL") + testErrCount++ + } else { + io.ErrPrintfln("ok %s \t%s", pkg.Dir, dstr) } } - - // testing with *_filetest.gno - { - filter := splitRegexp(runFlag) - for _, testFile := range filetestFiles { - testFileName := filepath.Base(testFile) - testName := "file/" + testFileName - if !shouldRun(filter, testName) { - continue - } - - startedAt := time.Now() - if verbose { - io.ErrPrintfln("=== RUN %s", testName) - } - - var closer func() (string, error) - if !verbose { - closer = testutils.CaptureStdoutAndStderr() - } - - testFilePath := filepath.Join(pkgPath, testFileName) - err := tests.RunFileTest(rootDir, testFilePath, tests.WithSyncWanted(cfg.updateGoldenTests)) - duration := time.Since(startedAt) - dstr := fmtDuration(duration) - - if err != nil { - errs = multierr.Append(errs, err) - io.ErrPrintfln("--- FAIL: %s (%s)", testName, dstr) - if verbose { - stdouterr, err := closer() - if err != nil { - panic(err) - } - fmt.Fprintln(os.Stderr, stdouterr) - } - continue - } - - if verbose { - io.ErrPrintfln("--- PASS: %s (%s)", testName, dstr) - } - // XXX: add per-test metrics - } + if testErrCount > 0 || buildErrCount > 0 { + io.ErrPrintfln("FAIL") + return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount) } - return errs + return nil } // attempts to determine the full gno pkg path by analyzing the directory. @@ -423,228 +267,3 @@ func pkgPathFromRootDir(pkgPath, rootDir string) string { } return "" } - -func runTestFiles( - m *gno.Machine, - files *gno.FileSet, - pkgName string, - verbose bool, - printRuntimeMetrics bool, - printEvents bool, - runFlag string, - io commands.IO, -) (errs error) { - defer func() { - if r := recover(); r != nil { - errs = multierr.Append(fmt.Errorf("panic: %v\nstack:\n%v\ngno machine: %v", r, string(debug.Stack()), m.String()), errs) - } - }() - - testFuncs := &testFuncs{ - PackageName: pkgName, - Verbose: verbose, - RunFlag: runFlag, - } - loadTestFuncs(pkgName, testFuncs, files) - - // before/after statistics - numPackagesBefore := m.Store.NumMemPackages() - - testmain, err := formatTestmain(testFuncs) - if err != nil { - log.Fatal(err) - } - - m.RunFiles(files.Files...) - n := gno.MustParseFile("main_test.gno", testmain) - m.RunFiles(n) - - for _, test := range testFuncs.Tests { - // cleanup machine between tests - tests.CleanupMachine(m) - - testFuncStr := fmt.Sprintf("%q", test.Name) - - eval := m.Eval(gno.Call("runtest", testFuncStr)) - - if printEvents { - events := m.Context.(*teststd.TestExecContext).EventLogger.Events() - if events != nil { - res, err := json.Marshal(events) - if err != nil { - panic(err) - } - io.ErrPrintfln("EVENTS: %s", string(res)) - } - } - - ret := eval[0].GetString() - if ret == "" { - err := errors.New("failed to execute unit test: %q", test.Name) - errs = multierr.Append(errs, err) - io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name) - continue - } - - // TODO: replace with amino or send native type? - var rep report - err = json.Unmarshal([]byte(ret), &rep) - if err != nil { - errs = multierr.Append(errs, err) - io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name) - continue - } - - if rep.Failed { - err := errors.New("failed: %q", test.Name) - errs = multierr.Append(errs, err) - } - - if printRuntimeMetrics { - imports := m.Store.NumMemPackages() - numPackagesBefore - 1 - // XXX: store changes - // XXX: max mem consumption - allocsVal := "n/a" - if m.Alloc != nil { - maxAllocs, allocs := m.Alloc.Status() - allocsVal = fmt.Sprintf("%s(%.2f%%)", - prettySize(allocs), - float64(allocs)/float64(maxAllocs)*100, - ) - } - io.ErrPrintfln("--- runtime: cycle=%s imports=%d allocs=%s", - prettySize(m.Cycles), - imports, - allocsVal, - ) - } - } - - return errs -} - -// mirror of stdlibs/testing.Report -type report struct { - Failed bool - Skipped bool -} - -var testmainTmpl = template.Must(template.New("testmain").Parse(` -package {{ .PackageName }} - -import ( - "testing" -) - -var tests = []testing.InternalTest{ -{{range .Tests}} - {"{{.Name}}", {{.Name}}}, -{{end}} -} - -func runtest(name string) (report string) { - for _, test := range tests { - if test.Name == name { - return testing.RunTest({{printf "%q" .RunFlag}}, {{.Verbose}}, test) - } - } - panic("no such test: " + name) - return "" -} -`)) - -type testFuncs struct { - Tests []testFunc - PackageName string - Verbose bool - RunFlag string -} - -type testFunc struct { - Package string - Name string -} - -func getPkgNameFromFileset(files *gno.FileSet) string { - if len(files.Files) <= 0 { - return "" - } - return string(files.Files[0].PkgName) -} - -func formatTestmain(t *testFuncs) (string, error) { - var buf bytes.Buffer - if err := testmainTmpl.Execute(&buf, t); err != nil { - return "", err - } - return buf.String(), nil -} - -func loadTestFuncs(pkgName string, t *testFuncs, tfiles *gno.FileSet) *testFuncs { - for _, tf := range tfiles.Files { - for _, d := range tf.Decls { - if fd, ok := d.(*gno.FuncDecl); ok { - fname := string(fd.Name) - if strings.HasPrefix(fname, "Test") { - tf := testFunc{ - Package: pkgName, - Name: fname, - } - t.Tests = append(t.Tests, tf) - } - } - } - } - return t -} - -// parseMemPackageTests is copied from gno.ParseMemPackageTests -// for except to _filetest.gno -func parseMemPackageTests(memPkg *gnovm.MemPackage) (tset, itset *gno.FileSet) { - tset = &gno.FileSet{} - itset = &gno.FileSet{} - var errs error - for _, mfile := range memPkg.Files { - if !strings.HasSuffix(mfile.Name, ".gno") { - continue // skip this file. - } - if strings.HasSuffix(mfile.Name, "_filetest.gno") { - continue - } - n, err := gno.ParseFile(mfile.Name, mfile.Body) - if err != nil { - errs = multierr.Append(errs, err) - continue - } - if n == nil { - panic("should not happen") - } - if strings.HasSuffix(mfile.Name, "_test.gno") { - // add test file. - if memPkg.Name+"_test" == string(n.PkgName) { - itset.AddFiles(n) - } else { - tset.AddFiles(n) - } - } else if memPkg.Name == string(n.PkgName) { - // skip package file. - } else { - panic(fmt.Sprintf( - "expected package name [%s] or [%s_test] but got [%s] file [%s]", - memPkg.Name, memPkg.Name, n.PkgName, mfile)) - } - } - if errs != nil { - panic(errs) - } - return tset, itset -} - -func shouldRun(filter filterMatch, path string) bool { - if filter == nil { - return true - } - elem := strings.Split(path, "/") - ok, _ := filter.matches(elem, matchString) - return ok -} diff --git a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar b/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar index fc4039d38c6..52141dff09b 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar +++ b/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar @@ -16,4 +16,4 @@ func main() { -- stdout.golden -- -- stderr.golden -- -bad_file.gno:3: unknown import path python (code=2). +bad_file.gno:3:8: unknown import path python (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar index 9482eeb1f4f..5aa3a3282d5 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar +++ b/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar @@ -17,4 +17,4 @@ func TestIHaveSomeError() { -- stdout.golden -- -- stderr.golden -- -i_have_error_test.gno:6: name undefined_variable not declared (code=2). +i_have_error_test.gno:6:7: name undefined_variable not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar b/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar index 7bd74a34855..b63c5c447e1 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar +++ b/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar @@ -17,4 +17,4 @@ func main() { -- stdout.golden -- -- stderr.golden -- -bad_file.gno:6: name hello not declared (code=2). +bad_file.gno:6:3: name hello not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar index 20a399881be..f9ce4dd9028 100644 --- a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar @@ -2,7 +2,6 @@ gno test -v . -stdout 'Machine\.RunMain\(\) panic: oups' stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' stderr 'ok \. \d\.\d\ds' diff --git a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar index 00737d8dd67..621397d8d1f 100644 --- a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar @@ -2,9 +2,10 @@ ! gno test -v . -stdout 'Machine\.RunMain\(\) panic: oups' stderr '=== RUN file/x_filetest.gno' -stderr 'panic: fail on x_filetest.gno: got "oups", want: "xxx"' +stderr 'Error diff:' +stderr '-xxx' +stderr '\+oups' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar index e2b67cb3333..067489c41f2 100644 --- a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar @@ -3,9 +3,9 @@ # by the '-update-golden-tests' flag. The Error is only updated when it is # empty. -! gno test -v . +gno test -update-golden-tests -v . -stdout 'Machine\.RunMain\(\) panic: oups' +! stdout .+ stderr '=== RUN file/x_filetest.gno' cmp x_filetest.gno x_filetest.gno.golden @@ -18,7 +18,6 @@ func main() { } // Error: - -- x_filetest.gno.golden -- package main @@ -28,5 +27,3 @@ func main() { // Error: // oups -// *** CHECK THE ERR MESSAGES ABOVE, MAKE SURE IT'S WHAT YOU EXPECTED, DELETE THIS LINE AND RUN TEST AGAIN *** - diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar index 91431e4f7bb..7b57729ee91 100644 --- a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar @@ -2,9 +2,8 @@ ! gno test -v . -stdout 'Machine.RunMain\(\) panic: beep boop' stderr '=== RUN file/failing_filetest.gno' -stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop' +stderr 'unexpected panic: beep boop' -- failing.gno -- package failing diff --git a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar b/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar index 0236872e78a..34da5fe2ff0 100644 --- a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar @@ -7,7 +7,7 @@ stderr 'ok \. \d\.\d\ds' gno test -print-events -v . -! stdout .+ +stdout 'test' stderr '=== RUN file/valid_filetest.gno' stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' stderr 'ok \. \d\.\d\ds' diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar b/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar index e065d00d55a..99747a0a241 100644 --- a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar @@ -3,7 +3,7 @@ gno test --print-runtime-metrics . ! stdout .+ -stderr '--- runtime: cycle=[\d\.kM]+ imports=\d+ allocs=[\d\.kM]+\(\d\.\d\d%\)' +stderr '--- runtime: cycle=[\d\.kM]+ allocs=[\d\.kM]+\(\d\.\d\d%\)' -- metrics.gno -- package metrics @@ -20,4 +20,3 @@ func TestTimeout(t *testing.T) { println("plop") } } - diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar index e734dad7934..a8aa878e0a4 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar @@ -2,7 +2,8 @@ gno test -v . -! stdout .+ # stdout should be empty +stdout 'hey' +stdout 'hru?' stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' stderr 'ok \. \d\.\d\ds' diff --git a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar index 009d09623a0..60a38933d47 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar @@ -1,13 +1,14 @@ # Test Output instruction incorrect +# with -v, stdout should contain output (unmodified). ! gno test -v . -! stdout .+ # stdout should be empty +stdout 'hey' + stderr '=== RUN file/x_filetest.gno' -stderr 'panic: fail on x_filetest.gno: diff:' stderr '--- Expected' stderr '\+\+\+ Actual' -stderr '@@ -1,2 \+1 @@' +stderr '@@ -1,3 \+1,2 @@' stderr 'hey' stderr '-hru?' diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar index 45e6e5c79be..45385a7eef9 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar @@ -2,7 +2,9 @@ gno test -v . -update-golden-tests -! stdout .+ # stdout should be empty +stdout 'hey' +stdout '^hru\?' + stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' stderr 'ok \. \d\.\d\ds' @@ -19,7 +21,6 @@ func main() { // Output: // hey - -- x_filetest.gno.golden -- package main @@ -31,4 +32,3 @@ func main() { // Output: // hey // hru? - diff --git a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar b/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar index b38683adf81..7d204bdb98d 100644 --- a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar @@ -66,4 +66,5 @@ func main() { println("filetest " + hello.Name) } -// Output: filetest foo +// Output: +// filetest foo diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar index 99e6fccd42d..ced183bec67 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar @@ -8,8 +8,8 @@ stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- -// PKGPATH: gno.land/r/x -package x +// PKGPATH: gno.land/r/xx +package xx var x int @@ -18,11 +18,11 @@ func main() { } // Realm: -// switchrealm["gno.land/r/x"] -// u[58cde29876a8d185e30c727361981efb068f4726:2]={ +// switchrealm["gno.land/r/xx"] +// u[aea84df38908f9569d0f552575606e6e6e7e22dd:2]={ // "Blank": {}, // "ObjectInfo": { -// "ID": "58cde29876a8d185e30c727361981efb068f4726:2", +// "ID": "aea84df38908f9569d0f552575606e6e6e7e22dd:2", // "IsEscaped": true, // "ModTime": "3", // "RefCount": "2" @@ -35,7 +35,7 @@ func main() { // "Column": "0", // "File": "", // "Line": "0", -// "PkgPath": "gno.land/r/x" +// "PkgPath": "gno.land/r/xx" // } // }, // "Values": [ @@ -57,22 +57,22 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "58cde29876a8d185e30c727361981efb068f4726:3" +// "ObjectID": "aea84df38908f9569d0f552575606e6e6e7e22dd:3" // }, -// "FileName": "main.gno", +// "FileName": "x.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", // "NativePkg": "", -// "PkgPath": "gno.land/r/x", +// "PkgPath": "gno.land/r/xx", // "Source": { // "@type": "/gno.RefNode", // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "x.gno", // "Line": "6", -// "PkgPath": "gno.land/r/x" +// "PkgPath": "gno.land/r/xx" // } // }, // "Type": { @@ -84,4 +84,3 @@ func main() { // } // ] // } - diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar index 6dfd6d70bb9..234d0f81e77 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar @@ -4,16 +4,17 @@ ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' -stderr 'panic: fail on x_filetest.gno: diff:' +stderr 'Realm diff:' stderr '--- Expected' stderr '\+\+\+ Actual' -stderr '@@ -1 \+1,66 @@' +stderr '@@ -1,2 \+1,67 @@' stderr '-xxx' -stderr '\+switchrealm\["gno.land/r/x"\]' +stderr '\+switchrealm\["gno.land/r/xx"\]' +stderr 'x_filetest.gno failed' -- x_filetest.gno -- -// PKGPATH: gno.land/r/x -package x +// PKGPATH: gno.land/r/xx +package xx var x int @@ -23,4 +24,3 @@ func main() { // Realm: // xxxx - diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar index 3d27ab4fde0..c93e6d86e8f 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar @@ -10,8 +10,8 @@ stderr 'ok \. \d\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden -- x_filetest.gno -- -// PKGPATH: gno.land/r/x -package x +// PKGPATH: gno.land/r/xx +package xx var x int @@ -21,10 +21,9 @@ func main() { // Realm: // xxx - -- x_filetest.gno.golden -- -// PKGPATH: gno.land/r/x -package x +// PKGPATH: gno.land/r/xx +package xx var x int @@ -33,11 +32,11 @@ func main() { } // Realm: -// switchrealm["gno.land/r/x"] -// u[58cde29876a8d185e30c727361981efb068f4726:2]={ +// switchrealm["gno.land/r/xx"] +// u[aea84df38908f9569d0f552575606e6e6e7e22dd:2]={ // "Blank": {}, // "ObjectInfo": { -// "ID": "58cde29876a8d185e30c727361981efb068f4726:2", +// "ID": "aea84df38908f9569d0f552575606e6e6e7e22dd:2", // "IsEscaped": true, // "ModTime": "3", // "RefCount": "2" @@ -50,7 +49,7 @@ func main() { // "Column": "0", // "File": "", // "Line": "0", -// "PkgPath": "gno.land/r/x" +// "PkgPath": "gno.land/r/xx" // } // }, // "Values": [ @@ -72,22 +71,22 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "58cde29876a8d185e30c727361981efb068f4726:3" +// "ObjectID": "aea84df38908f9569d0f552575606e6e6e7e22dd:3" // }, -// "FileName": "main.gno", +// "FileName": "x.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", // "NativePkg": "", -// "PkgPath": "gno.land/r/x", +// "PkgPath": "gno.land/r/xx", // "Source": { // "@type": "/gno.RefNode", // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "x.gno", // "Line": "6", -// "PkgPath": "gno.land/r/x" +// "PkgPath": "gno.land/r/xx" // } // }, // "Type": { @@ -99,4 +98,3 @@ func main() { // } // ] // } - diff --git a/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar b/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar deleted file mode 100644 index 6099788a9a1..00000000000 --- a/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar +++ /dev/null @@ -1,32 +0,0 @@ -# Test native lib - -! gno test -v . - -! stdout .+ -stderr 'panic: unknown import path net \[recovered\]' -stderr ' panic: gno.land/r/\w{8}/contract.gno:3:8: unknown import path net' - -gno test -v --with-native-fallback . - -! stdout .+ -stderr '=== RUN TestFoo' -stderr '--- PASS: TestFoo' - --- contract.gno -- -package contract - -import "net" - -func Foo() { - _ = net.IPv4 -} - --- contract_test.gno -- -package contract - -import "testing" - -func TestFoo(t *testing.T) { - Foo() -} - diff --git a/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar b/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar deleted file mode 100644 index 37ef68f3d91..00000000000 --- a/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar +++ /dev/null @@ -1,32 +0,0 @@ -# Test unknow lib - -! gno test -v . - -! stdout .+ -stderr 'panic: unknown import path foobarbaz \[recovered\]' -stderr ' panic: gno.land/r/\w{8}/contract.gno:3:8: unknown import path foobarbaz' - -! gno test -v --with-native-fallback . - -! stdout .+ -stderr 'panic: unknown import path foobarbaz \[recovered\]' -stderr ' panic: gno.land/r/\w{8}/contract.gno:3:8: unknown import path foobarbaz' - --- contract.gno -- -package contract - -import "foobarbaz" - -func Foo() { - _ = foobarbaz.Gnognogno -} - --- contract_test.gno -- -package contract - -import "testing" - -func TestFoo(t *testing.T) { - Foo() -} - diff --git a/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar b/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar new file mode 100644 index 00000000000..0611d3440a4 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar @@ -0,0 +1,24 @@ +# Test for loading an unknown package + +! gno test -v . + +! stdout .+ +stderr 'contract.gno:3:8: unknown import path foobarbaz' + +-- contract.gno -- +package contract + +import "foobarbaz" + +func Foo() { + _ = foobarbaz.Gnognogno +} + +-- contract_test.gno -- +package contract + +import "testing" + +func TestFoo(t *testing.T) { + Foo() +} diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar index 02ae3f72304..4e24ad9ab08 100644 --- a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar @@ -7,7 +7,7 @@ stderr 'ok \. \d\.\d\ds' gno test -v . -! stdout .+ +stdout 'test' stderr '=== RUN file/valid_filetest.gno' stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' stderr 'ok \. \d\.\d\ds' diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar index d21390f9472..145fe796c09 100644 --- a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar +++ b/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar @@ -1,10 +1,11 @@ # Run gno transpile with -gobuild flag +# The error messages changed sometime in go1.23, so this avoids errors ! gno transpile -gobuild . ! stdout .+ -stderr '^main.gno:4:6: x declared and not used$' -stderr '^main.gno:5:6: y declared and not used$' +stderr '^main.gno:4:6: .*declared and not used' +stderr '^main.gno:5:6: .*declared and not used' stderr '^2 transpile error\(s\)$' cmp main.gno.gen.go main.gno.gen.go.golden diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index 90aedd5d27a..697aa94b3c6 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -338,17 +338,3 @@ func copyFile(src, dst string) error { return nil } - -// Adapted from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ -func prettySize(nb int64) string { - const unit = 1000 - if nb < unit { - return fmt.Sprintf("%d", nb) - } - div, exp := int64(unit), 0 - for n := nb / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f%c", float64(nb)/float64(div), "kMGTPE"[exp]) -} diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 63a3ee74675..926ff0595e6 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/tests" + "github.com/gnolang/gno/gnovm/pkg/test" ) type dtest struct{ in, out string } @@ -24,17 +24,12 @@ type writeNopCloser struct{ io.Writer } func (writeNopCloser) Close() error { return nil } // TODO (Marc): move evalTest to gnovm/tests package and remove code duplicates -func evalTest(debugAddr, in, file string) (out, err, stacktrace string) { +func evalTest(debugAddr, in, file string) (out, err string) { bout := bytes.NewBufferString("") berr := bytes.NewBufferString("") stdin := bytes.NewBufferString(in) stdout := writeNopCloser{bout} stderr := writeNopCloser{berr} - debug := in != "" || debugAddr != "" - mode := tests.ImportModeStdlibsPreferred - if strings.HasSuffix(file, "_native.gno") { - mode = tests.ImportModeNativePreferred - } defer func() { if r := recover(); r != nil { @@ -44,7 +39,7 @@ func evalTest(debugAddr, in, file string) (out, err, stacktrace string) { err = strings.TrimSpace(strings.ReplaceAll(err, "../../tests/files/", "files/")) }() - testStore := tests.TestStore(gnoenv.RootDir(), "../../tests/files", stdin, stdout, stderr, mode) + _, testStore := test.Store(gnoenv.RootDir(), false, stdin, stdout, stderr) f := gnolang.MustReadFile(file) @@ -53,23 +48,11 @@ func evalTest(debugAddr, in, file string) (out, err, stacktrace string) { Input: stdin, Output: stdout, Store: testStore, - Context: tests.TestContext(string(f.PkgName), nil), - Debug: debug, + Context: test.Context(string(f.PkgName), nil), + Debug: true, }) defer m.Release() - defer func() { - if r := recover(); r != nil { - switch r.(type) { - case gnolang.UnhandledPanicError: - stacktrace = m.ExceptionsStacktrace() - default: - stacktrace = m.Stacktrace().String() - } - stacktrace = strings.TrimSpace(strings.ReplaceAll(stacktrace, "../../tests/files/", "files/")) - panic(r) - } - }() if debugAddr != "" { if e := m.Debugger.Serve(debugAddr); e != nil { @@ -81,7 +64,7 @@ func evalTest(debugAddr, in, file string) (out, err, stacktrace string) { m.RunFiles(f) ex, _ := gnolang.ParseExpr("main()") m.Eval(ex) - out, err, stacktrace = bout.String(), berr.String(), m.ExceptionsStacktrace() + out, err = bout.String(), berr.String() return } @@ -90,7 +73,7 @@ func runDebugTest(t *testing.T, targetPath string, tests []dtest) { for _, test := range tests { t.Run("", func(t *testing.T) { - out, err, _ := evalTest("", test.in, targetPath) + out, err := evalTest("", test.in, targetPath) t.Log("in:", test.in, "out:", out, "err:", err) if !strings.Contains(out, test.out) { t.Errorf("unexpected output\nwant\"%s\"\n got \"%s\"", test.out, out) @@ -206,7 +189,7 @@ func TestRemoteDebug(t *testing.T) { } func TestRemoteError(t *testing.T) { - _, err, _ := evalTest(":xxx", "", debugTarget) + _, err := evalTest(":xxx", "", debugTarget) t.Log("err:", err) if !strings.Contains(err, "tcp/xxx: unknown port") && !strings.Contains(err, "tcp/xxx: nodename nor servname provided, or not known") { diff --git a/gnovm/pkg/gnolang/eval_test.go b/gnovm/pkg/gnolang/eval_test.go deleted file mode 100644 index 9b83d673767..00000000000 --- a/gnovm/pkg/gnolang/eval_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package gnolang_test - -import ( - "io/fs" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "testing" -) - -func TestEvalFiles(t *testing.T) { - dir := "../../tests/files" - fsys := os.DirFS(dir) - err := fs.WalkDir(fsys, ".", func(path string, de fs.DirEntry, err error) error { - switch { - case err != nil: - return err - case path == "extern": - return fs.SkipDir - case de.IsDir(): - return nil - } - - fullPath := filepath.Join(dir, path) - wantOut, wantErr, wantStacktrace, ok := testData(fullPath) - if !ok { - return nil - } - - t.Run(path, func(t *testing.T) { - out, err, stacktrace := evalTest("", "", fullPath) - - if wantErr != "" && !strings.Contains(err, wantErr) || - wantErr == "" && err != "" { - t.Fatalf("unexpected error\nWant: %s\n Got: %s", wantErr, err) - } - - if wantStacktrace != "" && !strings.Contains(stacktrace, wantStacktrace) { - t.Fatalf("unexpected stacktrace\nWant: %s\n Got: %s", wantStacktrace, stacktrace) - } - if wantOut != "" && strings.TrimSpace(out) != strings.TrimSpace(wantOut) { - t.Fatalf("unexpected output\nWant: \"%s\"\n Got: \"%s\"", wantOut, out) - } - }) - - return nil - }) - if err != nil { - t.Fatal(err) - } -} - -// testData returns the expected output and error string, and true if entry is valid. -func testData(name string) (testOut, testErr, testStacktrace string, ok bool) { - if !strings.HasSuffix(name, ".gno") || strings.HasSuffix(name, "_long.gno") { - return - } - buf, err := os.ReadFile(name) - if err != nil { - return - } - str := string(buf) - if strings.Contains(str, "// PKGPATH:") { - return - } - res := commentFrom(str, []string{ - "// Output:", - "// Error:", - "// Stacktrace:", - }) - - return res[0], res[1], res[2], true -} - -type directive struct { - delim string - res string - index int -} - -// (?m) makes ^ and $ match start/end of string. -// Used to substitute from a comment all the //. -// Using a regex allows us to parse lines only containing "//" as an empty line. -var reCommentPrefix = regexp.MustCompile("(?m)^//(?: |$)") - -// commentFrom returns the comments from s that are between the delimiters. -// delims is a list of delimiters like "// Output:", which should be on a -// single line to mark the beginning of a directive. -// The return value is the content of each directive, matching the indexes -// of delims, ie. len(result) == len(delims). -func commentFrom(s string, delims []string) []string { - directives := make([]directive, len(delims)) - directivesFound := make([]*directive, 0, len(delims)) - - // Find directives - for i, delim := range delims { - // must find delim isolated on one line - delim = "\n" + delim + "\n" - index := strings.Index(s, delim) - directives[i] = directive{delim: delim, index: index} - if index >= 0 { - directivesFound = append(directivesFound, &directives[i]) - } - } - sort.Slice(directivesFound, func(i, j int) bool { - return directivesFound[i].index < directivesFound[j].index - }) - - for i := range directivesFound { - next := len(s) - if i != len(directivesFound)-1 { - next = directivesFound[i+1].index - } - - // Mark beginning of directive content from the line after the directive. - contentStart := directivesFound[i].index + len(directivesFound[i].delim) - content := s[contentStart:next] - - // Remove comment prefixes. - parsed := reCommentPrefix.ReplaceAllLiteralString(content, "") - directivesFound[i].res = strings.TrimSuffix(parsed, "\n") - } - - res := make([]string, len(directives)) - for i, d := range directives { - res[i] = d.res - } - - return res -} diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go new file mode 100644 index 00000000000..f1bc87d21d8 --- /dev/null +++ b/gnovm/pkg/gnolang/files_test.go @@ -0,0 +1,141 @@ +package gnolang_test + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/test" + "github.com/stretchr/testify/require" +) + +var withSync = flag.Bool("update-golden-tests", false, "rewrite tests updating Realm: and Output: with new values where changed") + +type nopReader struct{} + +func (nopReader) Read(p []byte) (int, error) { return 0, io.EOF } + +// TestFiles tests all the files in "gnovm/tests/files". +// +// Cheatsheet: +// +// fail on the first test: +// go test -run TestFiles -failfast +// run a specific test: +// go test -run TestFiles/addr0b +// fix a specific test: +// go test -run TestFiles/'^bin1.gno' -short -v -update-golden-tests . +func TestFiles(t *testing.T) { + rootDir, err := filepath.Abs("../../../") + require.NoError(t, err) + + opts := &test.TestOptions{ + RootDir: rootDir, + Output: io.Discard, + Error: io.Discard, + Sync: *withSync, + } + opts.BaseStore, opts.TestStore = test.Store( + rootDir, true, + nopReader{}, opts.WriterForStore(), io.Discard, + ) + + dir := "../../tests/" + fsys := os.DirFS(dir) + err = fs.WalkDir(fsys, "files", func(path string, de fs.DirEntry, err error) error { + switch { + case err != nil: + return err + case path == "files/extern": + return fs.SkipDir + case de.IsDir(): + return nil + } + subTestName := path[len("files/"):] + if strings.HasSuffix(path, "_long.gno") && testing.Short() { + t.Run(subTestName, func(t *testing.T) { + t.Skip("skipping in -short") + }) + return nil + } + + content, err := fs.ReadFile(fsys, path) + if err != nil { + return err + } + + var criticalError error + t.Run(subTestName, func(t *testing.T) { + changed, err := opts.RunFiletest(path, content) + if err != nil { + t.Fatal(err.Error()) + } + if changed != "" { + err = os.WriteFile(filepath.Join(dir, path), []byte(changed), de.Type()) + if err != nil { + criticalError = fmt.Errorf("could not fix golden file: %w", err) + } + } + }) + + return criticalError + }) + if err != nil { + t.Fatal(err) + } +} + +// TestStdlibs tests all the standard library packages. +func TestStdlibs(t *testing.T) { + rootDir, err := filepath.Abs("../../../") + require.NoError(t, err) + + var capture bytes.Buffer + out := io.Writer(&capture) + if testing.Verbose() { + out = os.Stdout + } + opts := test.NewTestOptions(rootDir, nopReader{}, out, out) + opts.Verbose = true + + dir := "../../stdlibs/" + fsys := os.DirFS(dir) + err = fs.WalkDir(fsys, ".", func(path string, de fs.DirEntry, err error) error { + switch { + case err != nil: + return err + case !de.IsDir() || path == ".": + return nil + } + + fp := filepath.Join(dir, path) + memPkg := gnolang.ReadMemPackage(fp, path) + t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) { + if testing.Short() { + switch memPkg.Path { + case "bytes", "strconv", "regexp/syntax": + t.Skip("Skipped because of -short, and this stdlib is very long currently.") + } + } + err := test.Test(memPkg, "", opts) + if !testing.Verbose() { + t.Log(capture.String()) + } + if err != nil { + t.Error(err) + } + }) + + return nil + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/gnovm/pkg/gnolang/gonative.go b/gnovm/pkg/gnolang/gonative.go index fe92f5bcd23..5a39c76b5e1 100644 --- a/gnovm/pkg/gnolang/gonative.go +++ b/gnovm/pkg/gnolang/gonative.go @@ -83,11 +83,6 @@ func go2GnoBaseType(rt reflect.Type) Type { } } -// Implements Store. -func (ds *defaultStore) SetStrictGo2GnoMapping(strict bool) { - ds.go2gnoStrict = strict -} - // Implements Store. // See go2GnoValue2(). Like go2GnoType() but also converts any // top-level complex types (or pointers to them). The result gets @@ -109,54 +104,9 @@ func (ds *defaultStore) Go2GnoType(rt reflect.Type) (t Type) { // wrap t with declared type. pkgPath := rt.PkgPath() if pkgPath != "" { - // mappings have been removed, so for any non-builtin type in strict mode, - // this will panic. - if ds.go2gnoStrict { - // mapping failed and strict: error. - gokey := pkgPath + "." + rt.Name() - panic(fmt.Sprintf("native type does not exist for %s", gokey)) - } - - // generate a new gno type for testing. - mtvs := []TypedValue(nil) - if t.Kind() == InterfaceKind { - // methods already set on t.Methods. - // *DT.Methods not used in Go for interfaces. - } else { - prt := rt - if rt.Kind() != reflect.Ptr { - // NOTE: go reflect requires ptr kind - // for methods with ptr receivers, - // whereas gno methods are all - // declared on the *DeclaredType. - prt = reflect.PointerTo(rt) - } - nm := prt.NumMethod() - mtvs = make([]TypedValue, nm) - for i := 0; i < nm; i++ { - mthd := prt.Method(i) - ft := ds.go2GnoFuncType(mthd.Type) - fv := &FuncValue{ - Type: ft, - IsMethod: true, - Source: nil, - Name: Name(mthd.Name), - Closure: nil, - PkgPath: pkgPath, - body: nil, // XXX - nativeBody: nil, - } - mtvs[i] = TypedValue{T: ft, V: fv} - } - } - dt := &DeclaredType{ - PkgPath: pkgPath, - Name: Name(rt.Name()), - Base: t, - Methods: mtvs, - } - dt.Seal() - t = dt + // mappings have been removed, so this should never happen. + gokey := pkgPath + "." + rt.Name() + panic(fmt.Sprintf("native type does not exist for %s", gokey)) } // memoize t to cache. if debug { @@ -1230,34 +1180,6 @@ func gno2GoValue(tv *TypedValue, rv reflect.Value) (ret reflect.Value) { // ---------------------------------------- // PackageNode methods -func (x *PackageNode) DefineGoNativeType(rt reflect.Type) { - if debug { - debug.Printf("*PackageNode.DefineGoNativeType(%s)\n", rt.String()) - } - pkgp := rt.PkgPath() - if pkgp == "" { - // DefineGoNativeType can only work with defined exported types. - // Unexported types should be composed, and primitive types - // should just use Gno types. - panic(fmt.Sprintf( - "reflect.Type %s has no package path", - rt.String())) - } - name := rt.Name() - if name == "" { - panic(fmt.Sprintf( - "reflect.Type %s is not named", - rt.String())) - } - if rt.PkgPath() == "" { - panic(fmt.Sprintf( - "reflect.Type %s is not defined/exported", - rt.String())) - } - nt := &NativeType{Type: rt} - x.Define(Name(name), asValue(nt)) -} - func (x *PackageNode) DefineGoNativeValue(name Name, nv interface{}) { x.defineGoNativeValue(false, name, nv) } diff --git a/gnovm/pkg/gnolang/gonative_test.go b/gnovm/pkg/gnolang/gonative_test.go deleted file mode 100644 index fa5415a8068..00000000000 --- a/gnovm/pkg/gnolang/gonative_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package gnolang - -import ( - "bytes" - "fmt" - "reflect" - "testing" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/stretchr/testify/assert" -) - -// args is an even number of elements, -// the even index items are package nodes, -// and the odd index items are corresponding package values. -func gonativeTestStore(args ...interface{}) Store { - store := NewStore(nil, nil, nil) - store.SetPackageGetter(func(pkgPath string, _ Store) (*PackageNode, *PackageValue) { - for i := 0; i < len(args)/2; i++ { - pn := args[i*2].(*PackageNode) - pv := args[i*2+1].(*PackageValue) - if pkgPath == pv.PkgPath { - return pn, pv - } - } - return nil, nil - }) - store.SetStrictGo2GnoMapping(false) - return store -} - -type Foo struct { - A int - B int32 - C int64 - D string -} - -func TestGoNativeDefine(t *testing.T) { - // Create package foo and define Foo. - pkg := NewPackageNode("foo", "test.foo", nil) - rt := reflect.TypeOf(Foo{}) - pkg.DefineGoNativeType(rt) - nt := pkg.GetValueRef(nil, Name("Foo"), true).GetType().(*NativeType) - assert.Equal(t, rt, nt.Type) - path := pkg.GetPathForName(nil, Name("Foo")) - assert.Equal(t, uint8(1), path.Depth) - assert.Equal(t, uint16(0), path.Index) - pv := pkg.NewPackage() - nt = pv.GetBlock(nil).GetPointerTo(nil, path).TV.GetType().(*NativeType) - assert.Equal(t, rt, nt.Type) - store := gonativeTestStore(pkg, pv) - - // Import above package and evaluate foo.Foo. - m := NewMachineWithOptions(MachineOptions{ - PkgPath: "test", - Store: store, - }) - m.RunDeclaration(ImportD("foo", "test.foo")) - tvs := m.Eval(Sel(Nx("foo"), "Foo")) - assert.Equal(t, 1, len(tvs)) - assert.Equal(t, nt, tvs[0].V.(TypeValue).Type) -} - -func TestGoNativeDefine2(t *testing.T) { - // Create package foo and define Foo. - pkg := NewPackageNode("foo", "test.foo", nil) - rt := reflect.TypeOf(Foo{}) - pkg.DefineGoNativeType(rt) - pv := pkg.NewPackage() - store := gonativeTestStore(pkg, pv) - - // Import above package and run file. - out := new(bytes.Buffer) - m := NewMachineWithOptions(MachineOptions{ - PkgPath: "main", - Output: out, - Store: store, - }) - - c := `package main -import foo "test.foo" -func main() { - f := foo.Foo{A:1} - println("A:", f.A) - println("B:", f.B) - println("C:", f.C) - println("D:", f.D) -}` - n := MustParseFile("main.go", c) - m.RunFiles(n) - m.RunMain() - // weird `+` is used to place a space, without having editors strip it away. - assert.Equal(t, `A: 1 -B: 0 -C: 0 -D: `+` -`, string(out.Bytes())) -} - -func TestGoNativeDefine3(t *testing.T) { - t.Parallel() - - // Create package foo and define Foo. - out := new(bytes.Buffer) - pkg := NewPackageNode("foo", "test.foo", nil) - pkg.DefineGoNativeType(reflect.TypeOf(Foo{})) - pkg.DefineGoNativeValue("PrintFoo", func(f Foo) { - out.Write([]byte(fmt.Sprintf("A: %v\n", f.A))) - out.Write([]byte(fmt.Sprintf("B: %v\n", f.B))) - out.Write([]byte(fmt.Sprintf("C: %v\n", f.C))) - out.Write([]byte(fmt.Sprintf("D: %v\n", f.D))) - }) - pv := pkg.NewPackage() - store := gonativeTestStore(pkg, pv) - - // Import above package and run file. - m := NewMachineWithOptions(MachineOptions{ - PkgPath: "main", - Output: out, - Store: store, - }) - - c := `package main -import foo "test.foo" -func main() { - f := foo.Foo{A:1} - foo.PrintFoo(f) -}` - n := MustParseFile("main.go", c) - m.RunFiles(n) - m.RunMain() - assert.Equal(t, `A: 1 -B: 0 -C: 0 -D: `+` -`, out.String()) -} - -func TestCrypto(t *testing.T) { - t.Parallel() - - addr := crypto.Address{} - store := gonativeTestStore() - tv := Go2GnoValue(nilAllocator, store, reflect.ValueOf(addr)) - assert.Equal(t, - `(array[0x0000000000000000000000000000000000000000] github.com/gnolang/gno/tm2/pkg/crypto.Address)`, - tv.String()) -} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index e341ef8e9f1..4f4c7c188f3 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -3,7 +3,6 @@ package gnolang // XXX rename file to machine.go. import ( - "encoding/json" "fmt" "io" "reflect" @@ -11,7 +10,6 @@ import ( "strconv" "strings" "sync" - "testing" "github.com/gnolang/overflow" @@ -299,7 +297,7 @@ func (m *Machine) runMemPackage(memPkg *gnovm.MemPackage, save, overrides bool) } m.SetActivePackage(pv) // run files. - updates := m.RunFileDecls(files.Files...) + updates := m.runFileDecls(files.Files...) // save package value and mempackage. // XXX save condition will be removed once gonative is removed. var throwaway *Realm @@ -400,103 +398,6 @@ func destar(x Expr) Expr { return x } -// Tests all test files in a mempackage. -// Assumes that the importing of packages is handled elsewhere. -// The resulting package value and node become injected with TestMethods and -// other declarations, so it is expected that non-test code will not be run -// afterwards from the same store. -func (m *Machine) TestMemPackage(t *testing.T, memPkg *gnovm.MemPackage) { - defer m.injectLocOnPanic() - DisableDebug() - fmt.Println("DEBUG DISABLED (FOR TEST DEPENDENCIES INIT)") - // parse test files. - tfiles, itfiles := ParseMemPackageTests(memPkg) - { // first, tfiles which run in the same package. - pv := m.Store.GetPackage(memPkg.Path, false) - pvBlock := pv.GetBlock(m.Store) - pvSize := len(pvBlock.Values) - m.SetActivePackage(pv) - // run test files. - m.RunFiles(tfiles.Files...) - // run all tests in test files. - for i := pvSize; i < len(pvBlock.Values); i++ { - tv := pvBlock.Values[i] - m.TestFunc(t, tv) - } - } - { // run all (import) tests in test files. - pn := NewPackageNode(Name(memPkg.Name+"_test"), memPkg.Path+"_test", itfiles) - pv := pn.NewPackage() - m.Store.SetBlockNode(pn) - m.Store.SetCachePackage(pv) - pvBlock := pv.GetBlock(m.Store) - m.SetActivePackage(pv) - m.RunFiles(itfiles.Files...) - pn.PrepareNewValues(pv) - EnableDebug() - fmt.Println("DEBUG ENABLED") - for i := 0; i < len(pvBlock.Values); i++ { - tv := pvBlock.Values[i] - m.TestFunc(t, tv) - } - } -} - -// TestFunc calls tv with testing.RunTest, if tv is a function with a name that -// starts with `Test`. -func (m *Machine) TestFunc(t *testing.T, tv TypedValue) { - if !(tv.T.Kind() == FuncKind && - strings.HasPrefix(string(tv.V.(*FuncValue).Name), "Test")) { - return // not a test function. - } - // XXX ensure correct func type. - name := string(tv.V.(*FuncValue).Name) - // prefetch the testing package. - testingpv := m.Store.GetPackage("testing", false) - testingtv := TypedValue{T: gPackageType, V: testingpv} - testingcx := &ConstExpr{TypedValue: testingtv} - - t.Run(name, func(t *testing.T) { - defer m.injectLocOnPanic() - x := Call( - Sel(testingcx, "RunTest"), // Call testing.RunTest - Str(name), // First param, the name of the test - X("true"), // Second Param, verbose bool - &CompositeLitExpr{ // Third param, the testing.InternalTest - Type: Sel(testingcx, "InternalTest"), - Elts: KeyValueExprs{ - {Key: X("Name"), Value: Str(name)}, - {Key: X("F"), Value: X(name)}, - }, - }, - ) - res := m.Eval(x) - ret := res[0].GetString() - if ret == "" { - t.Errorf("failed to execute unit test: %q", name) - return - } - - // mirror of stdlibs/testing.Report - var report struct { - Skipped bool - Failed bool - } - err := json.Unmarshal([]byte(ret), &report) - if err != nil { - t.Errorf("failed to parse test output %q", name) - return - } - - switch { - case report.Skipped: - t.SkipNow() - case report.Failed: - t.Fail() - } - }) -} - // Stacktrace returns the stack trace of the machine. // It collects the executions and frames from the machine's frames and statements. func (m *Machine) Stacktrace() (stacktrace Stacktrace) { @@ -534,58 +435,6 @@ func (m *Machine) Stacktrace() (stacktrace Stacktrace) { return } -// in case of panic, inject location information to exception. -func (m *Machine) injectLocOnPanic() { - if r := recover(); r != nil { - // Show last location information. - // First, determine the line number of expression or statement if any. - lastLine := 0 - lastColumn := 0 - if len(m.Exprs) > 0 { - for i := len(m.Exprs) - 1; i >= 0; i-- { - expr := m.Exprs[i] - if expr.GetLine() > 0 { - lastLine = expr.GetLine() - lastColumn = expr.GetColumn() - break - } - } - } - if lastLine == 0 && len(m.Stmts) > 0 { - for i := len(m.Stmts) - 1; i >= 0; i-- { - stmt := m.Stmts[i] - if stmt.GetLine() > 0 { - lastLine = stmt.GetLine() - lastColumn = stmt.GetColumn() - break - } - } - } - // Append line number to block location. - lastLoc := Location{} - for i := len(m.Blocks) - 1; i >= 0; i-- { - block := m.Blocks[i] - src := block.GetSource(m.Store) - loc := src.GetLocation() - if !loc.IsZero() { - lastLoc = loc - if lastLine > 0 { - lastLoc.Line = lastLine - lastLoc.Column = lastColumn - } - break - } - } - // wrap panic with location information. - if !lastLoc.IsZero() { - fmt.Printf("%s: %v\n", lastLoc.String(), r) - panic(errors.Wrap(r, fmt.Sprintf("location: %s", lastLoc.String()))) - } else { - panic(r) - } - } -} - // Convenience for tests. // Production must not use this, because realm package init // must happen after persistence and realm finalization, @@ -602,10 +451,6 @@ func (m *Machine) RunFiles(fns ...*FileNode) { // Add files to the package's *FileSet and run decls in them. // This will also run each init function encountered. // Returns the updated typed values of package. -func (m *Machine) RunFileDecls(fns ...*FileNode) []TypedValue { - return m.runFileDecls(fns...) -} - func (m *Machine) runFileDecls(fns ...*FileNode) []TypedValue { // Files' package names must match the machine's active one. // if there is one. diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 45062f8e14c..dcc1ad41739 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/gnolang/gno/gnovm" - "github.com/gnolang/gno/tm2/pkg/errors" "go.uber.org/multierr" ) @@ -1257,38 +1256,6 @@ func ParseMemPackage(memPkg *gnovm.MemPackage) (fset *FileSet) { return fset } -func ParseMemPackageTests(memPkg *gnovm.MemPackage) (tset, itset *FileSet) { - tset = &FileSet{} - itset = &FileSet{} - for _, mfile := range memPkg.Files { - if !strings.HasSuffix(mfile.Name, ".gno") { - continue // skip this file. - } - n, err := ParseFile(mfile.Name, mfile.Body) - if err != nil { - panic(errors.Wrap(err, "parsing file "+mfile.Name)) - } - if n == nil { - panic("should not happen") - } - if strings.HasSuffix(mfile.Name, "_test.gno") { - // add test file. - if memPkg.Name+"_test" == string(n.PkgName) { - itset.AddFiles(n) - } else { - tset.AddFiles(n) - } - } else if memPkg.Name == string(n.PkgName) { - // skip package file. - } else { - panic(fmt.Sprintf( - "expected package name [%s] or [%s_test] but got [%s] file [%s]", - memPkg.Name, memPkg.Name, n.PkgName, mfile)) - } - } - return tset, itset -} - func (fs *FileSet) AddFiles(fns ...*FileNode) { fs.Files = append(fs.Files, fns...) } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 7198d4f6a98..4b556604f0b 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -358,6 +358,11 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { func doRecover(stack []BlockNode, n Node) { if r := recover(); r != nil { + if _, ok := r.(*PreprocessError); ok { + // re-panic directly if this is a PreprocessError already. + panic(r) + } + // before re-throwing the error, append location information to message. last := stack[len(stack)-1] loc := last.GetLocation() @@ -4018,23 +4023,7 @@ func checkIntegerKind(xt Type) { // preprocess-able before other file-level declarations are // preprocessed). func predefineNow(store Store, last BlockNode, d Decl) (Decl, bool) { - defer func() { - if r := recover(); r != nil { - // before re-throwing the error, append location information to message. - loc := last.GetLocation() - if nline := d.GetLine(); nline > 0 { - loc.Line = nline - loc.Column = d.GetColumn() - } - if rerr, ok := r.(error); ok { - // NOTE: gotuna/gorilla expects error exceptions. - panic(errors.Wrap(rerr, loc.String())) - } else { - // NOTE: gotuna/gorilla expects error exceptions. - panic(fmt.Errorf("%s: %v", loc.String(), r)) - } - } - }() + defer doRecover([]BlockNode{last}, d) stack := &[]Name{} return predefineNow2(store, last, d, stack) } @@ -4099,10 +4088,10 @@ func predefineNow2(store Store, last BlockNode, d Decl, stack *[]Name) (Decl, bo // check base type of receiver type, should not be pointer type or interface type assertValidReceiverType := func(t Type) { if _, ok := t.(*PointerType); ok { - panic(fmt.Sprintf("invalid receiver type %v (base type is pointer type)\n", rt)) + panic(fmt.Sprintf("invalid receiver type %v (base type is pointer type)", rt)) } if _, ok := t.(*InterfaceType); ok { - panic(fmt.Sprintf("invalid receiver type %v (base type is interface type)\n", rt)) + panic(fmt.Sprintf("invalid receiver type %v (base type is interface type)", rt)) } } diff --git a/gnovm/pkg/gnolang/preprocess_test.go b/gnovm/pkg/gnolang/preprocess_test.go deleted file mode 100644 index 53ad97dd972..00000000000 --- a/gnovm/pkg/gnolang/preprocess_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package gnolang - -import ( - "fmt" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestPreprocess_BinaryExpressionOneNative(t *testing.T) { - pn := NewPackageNode("time", "time", nil) - pn.DefineGoNativeConstValue("Millisecond", time.Millisecond) - pn.DefineGoNativeConstValue("Second", time.Second) - pn.DefineGoNativeType(reflect.TypeOf(time.Duration(0))) - pv := pn.NewPackage() - store := gonativeTestStore(pn, pv) - store.SetBlockNode(pn) - - const src = `package main - import "time" -func main() { - var a int64 = 2 - println(time.Second * a) - -}` - n := MustParseFile("main.go", src) - - defer func() { - err := recover() - assert.Contains(t, fmt.Sprint(err), "incompatible operands in binary expression") - }() - initStaticBlocks(store, pn, n) - Preprocess(store, pn, n) -} - -func TestPreprocess_BinaryExpressionBothNative(t *testing.T) { - pn := NewPackageNode("time", "time", nil) - pn.DefineGoNativeConstValue("March", time.March) - pn.DefineGoNativeConstValue("Wednesday", time.Wednesday) - pn.DefineGoNativeType(reflect.TypeOf(time.Month(0))) - pn.DefineGoNativeType(reflect.TypeOf(time.Weekday(0))) - pv := pn.NewPackage() - store := gonativeTestStore(pn, pv) - store.SetBlockNode(pn) - - const src = `package main - import "time" -func main() { - println(time.March * time.Wednesday) - -}` - n := MustParseFile("main.go", src) - - defer func() { - err := recover() - assert.Contains(t, fmt.Sprint(err), "incompatible operands in binary expression") - }() - initStaticBlocks(store, pn, n) - Preprocess(store, pn, n) -} diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 9410eede29e..2c0ee05a1d7 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -51,7 +51,6 @@ type Store interface { GetBlockNodeSafe(Location) BlockNode SetBlockNode(BlockNode) // UNSTABLE - SetStrictGo2GnoMapping(bool) Go2GnoType(rt reflect.Type) Type GetAllocator() *Allocator NumMemPackages() int64 @@ -68,7 +67,6 @@ type Store interface { SetLogStoreOps(enabled bool) SprintStoreOps() string LogSwitchRealm(rlmpath string) // to mark change of realm boundaries - ClearCache() Print() } @@ -98,7 +96,6 @@ type defaultStore struct { pkgGetter PackageGetter // non-realm packages cacheNativeTypes map[reflect.Type]Type // reflect doc: reflect.Type are comparable nativeStore NativeStore // for injecting natives - go2gnoStrict bool // if true, native->gno type conversion must be registered. // transient opslog []StoreOp // for debugging and testing. @@ -120,7 +117,6 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore pkgGetter: nil, cacheNativeTypes: make(map[reflect.Type]Type), nativeStore: nil, - go2gnoStrict: true, } InitStoreCaches(ds) return ds @@ -149,7 +145,6 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans pkgGetter: ds.pkgGetter, cacheNativeTypes: ds.cacheNativeTypes, nativeStore: ds.nativeStore, - go2gnoStrict: ds.go2gnoStrict, // transient current: nil, @@ -171,10 +166,6 @@ func (transactionStore) SetPackageGetter(pg PackageGetter) { panic("SetPackageGetter may not be called in a transaction store") } -func (transactionStore) ClearCache() { - panic("ClearCache may not be called in a transaction store") -} - // XXX: we should block Go2GnoType, because it uses a global cache map; // but it's called during preprocess and thus breaks some testing code. // let's wait until we remove Go2Gno entirely. @@ -187,10 +178,6 @@ func (transactionStore) SetNativeStore(ns NativeStore) { panic("SetNativeStore may not be called in a transaction store") } -func (transactionStore) SetStrictGo2GnoMapping(strict bool) { - panic("SetStrictGo2GnoMapping may not be called in a transaction store") -} - // CopyCachesFromStore allows to copy a store's internal object, type and // BlockNode cache into the dst store. // This is mostly useful for testing, where many stores have to be initialized. @@ -498,8 +485,7 @@ func (ds *defaultStore) SetCacheType(tt Type) { tid := tt.TypeID() if tt2, exists := ds.cacheTypes.Get(tid); exists { if tt != tt2 { - // NOTE: not sure why this would happen. - panic("should not happen") + panic(fmt.Sprintf("cannot re-register %q with different type", tid)) } else { // already set. } @@ -778,15 +764,6 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) { StoreOp{Type: StoreOpSwitchRealm, RlmPath: rlmpath}) } -func (ds *defaultStore) ClearCache() { - ds.cacheObjects = make(map[ObjectID]Object) - ds.cacheTypes = txlog.GoMap[TypeID, Type](map[TypeID]Type{}) - ds.cacheNodes = txlog.GoMap[Location, BlockNode](map[Location]BlockNode{}) - ds.cacheNativeTypes = make(map[reflect.Type]Type) - // restore builtin types to cache. - InitStoreCaches(ds) -} - // for debugging func (ds *defaultStore) Print() { fmt.Println(colors.Yellow("//----------------------------------------")) diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index 40f84b65375..f7f03b947f6 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -58,9 +58,7 @@ func TestTransactionStore_blockedMethods(t *testing.T) { // These methods should panic as they modify store settings, which should // only be changed in the root store. assert.Panics(t, func() { transactionStore{}.SetPackageGetter(nil) }) - assert.Panics(t, func() { transactionStore{}.ClearCache() }) assert.Panics(t, func() { transactionStore{}.SetNativeStore(nil) }) - assert.Panics(t, func() { transactionStore{}.SetStrictGo2GnoMapping(false) }) } func TestCopyFromCachedStore(t *testing.T) { diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go index e7b5ecea96f..b0944d21646 100644 --- a/gnovm/pkg/repl/repl.go +++ b/gnovm/pkg/repl/repl.go @@ -13,8 +13,9 @@ import ( "os" "text/template" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/tests" + "github.com/gnolang/gno/gnovm/pkg/test" ) const ( @@ -124,7 +125,8 @@ func NewRepl(opts ...ReplOption) *Repl { r.stderr = &b r.storeFunc = func() gno.Store { - return tests.TestStore("teststore", "", r.stdin, r.stdout, r.stderr, tests.ImportModeStdlibsOnly) + _, st := test.Store(gnoenv.RootDir(), false, r.stdin, r.stdout, r.stderr) + return st } for _, o := range opts { diff --git a/gnovm/pkg/test/filetest.go b/gnovm/pkg/test/filetest.go new file mode 100644 index 00000000000..12bc9ed7f28 --- /dev/null +++ b/gnovm/pkg/test/filetest.go @@ -0,0 +1,407 @@ +package test + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "regexp" + "runtime/debug" + "strconv" + "strings" + + "github.com/gnolang/gno/gnovm" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/pmezard/go-difflib/difflib" + "go.uber.org/multierr" +) + +// RunFiletest executes the program in source as a filetest. +// If opts.Sync is enabled, and the filetest's golden output has changed, +// the first string is set to the new generated content of the file. +func (opts *TestOptions) RunFiletest(filename string, source []byte) (string, error) { + opts.outWriter.w = opts.Output + + return opts.runFiletest(filename, source) +} + +var reEndOfLineSpaces = func() *regexp.Regexp { + re := regexp.MustCompile(" +\n") + re.Longest() + return re +}() + +func (opts *TestOptions) runFiletest(filename string, source []byte) (string, error) { + dirs, err := ParseDirectives(bytes.NewReader(source)) + if err != nil { + return "", fmt.Errorf("error parsing directives: %w", err) + } + + // Initialize Machine.Context and Machine.Alloc according to the input directives. + pkgPath := dirs.FirstDefault(DirectivePkgPath, "main") + coins, err := std.ParseCoins(dirs.FirstDefault(DirectiveSend, "")) + if err != nil { + return "", err + } + ctx := Context( + pkgPath, + coins, + ) + maxAllocRaw := dirs.FirstDefault(DirectiveMaxAlloc, "0") + maxAlloc, err := strconv.ParseInt(maxAllocRaw, 10, 64) + if err != nil { + return "", fmt.Errorf("could not parse MAXALLOC directive: %w", err) + } + + // Create machine for execution and run test + cw := opts.BaseStore.CacheWrap() + m := gno.NewMachineWithOptions(gno.MachineOptions{ + Output: &opts.outWriter, + Store: opts.TestStore.BeginTransaction(cw, cw), + Context: ctx, + MaxAllocBytes: maxAlloc, + }) + defer m.Release() + result := opts.runTest(m, pkgPath, filename, source) + + // updated tells whether the directives have been updated, and as such + // a new generated filetest should be returned. + // returnErr is used as the return value, and may be a MultiError if + // multiple mismatches occurred. + updated := false + var returnErr error + // match verifies the content against dir.Content; if different, + // either updates dir.Content (for opts.Sync) or appends a new returnErr. + match := func(dir *Directive, actual string) { + // Remove end-of-line spaces, as these are removed from `fmt` in the filetests anyway. + actual = reEndOfLineSpaces.ReplaceAllString(actual, "\n") + if dir.Content != actual { + if opts.Sync { + dir.Content = actual + updated = true + } else { + returnErr = multierr.Append( + returnErr, + fmt.Errorf("%s diff:\n%s", dir.Name, unifiedDiff(dir.Content, actual)), + ) + } + } + } + + // First, check if we have an error, whether we're supposed to get it. + if result.Error != "" { + // Ensure this error was supposed to happen. + errDirective := dirs.First(DirectiveError) + if errDirective == nil { + return "", fmt.Errorf("unexpected panic: %s\noutput:\n%s\nstack:\n%v", + result.Error, result.Output, string(result.GoPanicStack)) + } + + // The Error directive (and many others) will have one trailing newline, + // which is not in the output - so add it there. + match(errDirective, result.Error+"\n") + } else { + err = m.CheckEmpty() + if err != nil { + return "", fmt.Errorf("machine not empty after main: %w", err) + } + if gno.HasDebugErrors() { + return "", fmt.Errorf("got unexpected debug error(s): %v", gno.GetDebugErrors()) + } + } + + // Check through each directive and verify it against the values from the test. + for idx := range dirs { + dir := &dirs[idx] + switch dir.Name { + case DirectiveOutput: + if !strings.HasSuffix(result.Output, "\n") { + result.Output += "\n" + } + match(dir, result.Output) + case DirectiveRealm: + sops := m.Store.SprintStoreOps() + "\n" + match(dir, sops) + case DirectiveEvents: + events := m.Context.(*teststd.TestExecContext).EventLogger.Events() + evtjson, err := json.MarshalIndent(events, "", " ") + if err != nil { + panic(err) + } + evtstr := string(evtjson) + "\n" + match(dir, evtstr) + case DirectivePreprocessed: + pn := m.Store.GetBlockNode(gno.PackageNodeLocation(pkgPath)) + pre := pn.(*gno.PackageNode).FileSet.Files[0].String() + "\n" + match(dir, pre) + case DirectiveStacktrace: + match(dir, result.GnoStacktrace) + } + } + + if updated { // only true if sync == true + return dirs.FileTest(), returnErr + } + + return "", returnErr +} + +func unifiedDiff(wanted, actual string) string { + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(wanted), + B: difflib.SplitLines(actual), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + if err != nil { + panic(fmt.Errorf("error generating unified diff: %w", err)) + } + return diff +} + +type runResult struct { + Output string + Error string + // Set if there was a panic within gno code. + GnoStacktrace string + // Set if this was recovered from a panic. + GoPanicStack []byte +} + +func (opts *TestOptions) runTest(m *gno.Machine, pkgPath, filename string, content []byte) (rr runResult) { + // Eagerly load imports. + // This is executed using opts.Store, rather than the transaction store; + // it allows us to only have to load the imports once (and re-use the cached + // versions). Running the tests in separate "transactions" means that they + // don't get the parent store dirty. + if err := LoadImports(opts.TestStore, filename, content); err != nil { + // NOTE: we perform this here, so we can capture the runResult. + return runResult{Error: err.Error()} + } + + // Reset and start capturing stdout. + opts.filetestBuffer.Reset() + revert := opts.outWriter.tee(&opts.filetestBuffer) + defer revert() + + defer func() { + if r := recover(); r != nil { + rr.Output = opts.filetestBuffer.String() + rr.GoPanicStack = debug.Stack() + switch v := r.(type) { + case *gno.TypedValue: + rr.Error = v.Sprint(m) + case *gno.PreprocessError: + rr.Error = v.Unwrap().Error() + case gno.UnhandledPanicError: + rr.Error = v.Error() + rr.GnoStacktrace = m.ExceptionsStacktrace() + default: + rr.Error = fmt.Sprint(v) + rr.GnoStacktrace = m.Stacktrace().String() + } + } + }() + + // Use last element after / (works also if slash is missing). + pkgName := gno.Name(pkgPath[strings.LastIndexByte(pkgPath, '/')+1:]) + if !gno.IsRealmPath(pkgPath) { + // Simple case - pure package. + pn := gno.NewPackageNode(pkgName, pkgPath, &gno.FileSet{}) + pv := pn.NewPackage() + m.Store.SetBlockNode(pn) + m.Store.SetCachePackage(pv) + m.SetActivePackage(pv) + n := gno.MustParseFile(filename, string(content)) + m.RunFiles(n) + m.RunStatement(gno.S(gno.Call(gno.X("main")))) + } else { + // Realm case. + gno.DisableDebug() // until main call. + + // Remove filetest from name, as that can lead to the package not being + // parsed correctly when using RunMemPackage. + filename = strings.ReplaceAll(filename, "_filetest", "") + + // save package using realm crawl procedure. + memPkg := &gnovm.MemPackage{ + Name: string(pkgName), + Path: pkgPath, + Files: []*gnovm.MemFile{ + { + Name: filename, + Body: string(content), + }, + }, + } + orig, tx := m.Store, m.Store.BeginTransaction(nil, nil) + m.Store = tx + // Run decls and init functions. + m.RunMemPackage(memPkg, true) + // Clear store cache and reconstruct machine from committed info + // (mimicking on-chain behaviour). + tx.Write() + m.Store = orig + + pv2 := m.Store.GetPackage(pkgPath, false) + m.SetActivePackage(pv2) + gno.EnableDebug() + // clear store.opslog from init function(s). + m.Store.SetLogStoreOps(true) // resets. + m.RunStatement(gno.S(gno.Call(gno.X("main")))) + } + + return runResult{ + Output: opts.filetestBuffer.String(), + GnoStacktrace: m.Stacktrace().String(), + } +} + +// --------------------------------------- +// directives and directive parsing + +const ( + // These directives are used to set input variables, which should change for + // the specific filetest. They must be specified on a single line. + DirectivePkgPath = "PKGPATH" + DirectiveMaxAlloc = "MAXALLOC" + DirectiveSend = "SEND" + + // These are used to match the result of the filetest against known golden + // values. + DirectiveOutput = "Output" + DirectiveError = "Error" + DirectiveRealm = "Realm" + DirectiveEvents = "Events" + DirectivePreprocessed = "Preprocessed" + DirectiveStacktrace = "Stacktrace" +) + +// Directives contains the directives of a file. +// It may also contains directives with empty names, to indicate parts of the +// original source file (used to re-construct the filetest at the end). +type Directives []Directive + +// First returns the first directive with the corresponding name. +func (d Directives) First(name string) *Directive { + if name == "" { + return nil + } + for i := range d { + if d[i].Name == name { + return &d[i] + } + } + return nil +} + +// FirstDefault returns the [Directive.Content] of First(name); if First(name) +// returns nil, then defaultValue is returned. +func (d Directives) FirstDefault(name, defaultValue string) string { + v := d.First(name) + if v == nil { + return defaultValue + } + return v.Content +} + +// FileTest re-generates the filetest from the given directives; the inverse of ParseDirectives. +func (d Directives) FileTest() string { + var bld strings.Builder + for _, dir := range d { + switch { + case dir.Name == "": + bld.WriteString(dir.Content) + case strings.ToUpper(dir.Name) == dir.Name: // is it all uppercase? + bld.WriteString("// " + dir.Name + ": " + dir.Content + "\n") + default: + bld.WriteString("// " + dir.Name + ":\n") + cnt := strings.TrimSuffix(dir.Content, "\n") + lines := strings.Split(cnt, "\n") + for _, line := range lines { + if line == "" { + bld.WriteString("//\n") + continue + } + bld.WriteString("// ") + bld.WriteString(strings.TrimRight(line, " ")) + bld.WriteByte('\n') + } + } + } + return bld.String() +} + +// Directive represents a directive in a filetest. +// A [Directives] slice may also contain directives with empty Names: +// these compose the source file itself, and are used to re-construct the file +// when a directive is changed. +type Directive struct { + Name string + Content string +} + +// Allows either a `ALLCAPS: content` on a single line, or a `PascalCase:`, +// with content on the following lines. +var reDirectiveLine = regexp.MustCompile("^(?:([A-Z][a-z]*):|([A-Z]+): ?(.*))$") + +// ParseDirectives parses all the directives in the filetest given at source. +func ParseDirectives(source io.Reader) (Directives, error) { + sc := bufio.NewScanner(source) + parsed := make(Directives, 0, 8) + for sc.Scan() { + // Re-append trailing newline. + // Useful as we always use it anyway. + txt := sc.Text() + "\n" + if !strings.HasPrefix(txt, "//") { + if len(parsed) == 0 || parsed[len(parsed)-1].Name != "" { + parsed = append(parsed, Directive{Content: txt}) + continue + } + parsed[len(parsed)-1].Content += txt + continue + } + + comment := txt[2 : len(txt)-1] // leading double slash, trailing \n + comment = strings.TrimPrefix(comment, " ") // leading space (if any) + + // If we're already in a directive, simply append there. + if len(parsed) > 0 && parsed[len(parsed)-1].Name != "" { + parsed[len(parsed)-1].Content += comment + "\n" + continue + } + + // Find if there is a colon (indicating a possible directive). + subm := reDirectiveLine.FindStringSubmatch(comment) + switch { + case subm == nil: + // Not found; append to parsed as a line, or to the previous + // directive if it exists. + if len(parsed) == 0 { + parsed = append(parsed, Directive{Content: txt}) + continue + } + last := &parsed[len(parsed)-1] + if last.Name == "" { + last.Content += txt + } else { + last.Content += comment + "\n" + } + case subm[1] != "": // output directive, with content on newlines + parsed = append(parsed, Directive{Name: subm[1]}) + default: // subm[2] != "", all caps + parsed = append(parsed, + Directive{Name: subm[2], Content: subm[3]}, + // enforce new directive later + Directive{}, + ) + } + } + return parsed, sc.Err() +} diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go new file mode 100644 index 00000000000..dabb5644cdd --- /dev/null +++ b/gnovm/pkg/test/imports.go @@ -0,0 +1,265 @@ +package test + +import ( + "encoding/json" + "errors" + "fmt" + "go/parser" + "go/token" + "io" + "math/big" + "os" + "path/filepath" + "runtime/debug" + "strconv" + "strings" + "time" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + teststdlibs "github.com/gnolang/gno/gnovm/tests/stdlibs" + teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + storetypes "github.com/gnolang/gno/tm2/pkg/store/types" +) + +// NOTE: this isn't safe, should only be used for testing. +func Store( + rootDir string, + withExtern bool, + stdin io.Reader, + stdout, stderr io.Writer, +) ( + baseStore storetypes.CommitStore, + resStore gno.Store, +) { + getPackage := func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { + if pkgPath == "" { + panic(fmt.Sprintf("invalid zero package path in testStore().pkgGetter")) + } + + if withExtern { + // if _test package... + const testPath = "github.com/gnolang/gno/_test/" + if strings.HasPrefix(pkgPath, testPath) { + baseDir := filepath.Join(rootDir, "gnovm", "tests", "files", "extern", pkgPath[len(testPath):]) + memPkg := gno.ReadMemPackage(baseDir, pkgPath) + send := std.Coins{} + ctx := Context(pkgPath, send) + m2 := gno.NewMachineWithOptions(gno.MachineOptions{ + PkgPath: "test", + Output: stdout, + Store: store, + Context: ctx, + }) + return m2.RunMemPackage(memPkg, true) + } + } + + // gonative exceptions. + // These are values available using gonative; eventually they should all be removed. + switch pkgPath { + case "os": + pkg := gno.NewPackageNode("os", pkgPath, nil) + pkg.DefineGoNativeValue("Stdin", stdin) + pkg.DefineGoNativeValue("Stdout", stdout) + pkg.DefineGoNativeValue("Stderr", stderr) + return pkg, pkg.NewPackage() + case "fmt": + pkg := gno.NewPackageNode("fmt", pkgPath, nil) + pkg.DefineGoNativeValue("Println", func(a ...interface{}) (n int, err error) { + // NOTE: uncomment to debug long running tests + // fmt.Println(a...) + res := fmt.Sprintln(a...) + return stdout.Write([]byte(res)) + }) + pkg.DefineGoNativeValue("Printf", func(format string, a ...interface{}) (n int, err error) { + res := fmt.Sprintf(format, a...) + return stdout.Write([]byte(res)) + }) + pkg.DefineGoNativeValue("Print", func(a ...interface{}) (n int, err error) { + res := fmt.Sprint(a...) + return stdout.Write([]byte(res)) + }) + pkg.DefineGoNativeValue("Sprint", fmt.Sprint) + pkg.DefineGoNativeValue("Sprintf", fmt.Sprintf) + pkg.DefineGoNativeValue("Sprintln", fmt.Sprintln) + pkg.DefineGoNativeValue("Sscanf", fmt.Sscanf) + pkg.DefineGoNativeValue("Errorf", fmt.Errorf) + pkg.DefineGoNativeValue("Fprintln", fmt.Fprintln) + pkg.DefineGoNativeValue("Fprintf", fmt.Fprintf) + pkg.DefineGoNativeValue("Fprint", fmt.Fprint) + return pkg, pkg.NewPackage() + case "encoding/json": + pkg := gno.NewPackageNode("json", pkgPath, nil) + pkg.DefineGoNativeValue("Unmarshal", json.Unmarshal) + pkg.DefineGoNativeValue("Marshal", json.Marshal) + return pkg, pkg.NewPackage() + case "internal/os_test": + pkg := gno.NewPackageNode("os_test", pkgPath, nil) + pkg.DefineNative("Sleep", + gno.Flds( // params + "d", gno.AnyT(), // NOTE: should be time.Duration + ), + gno.Flds( // results + ), + func(m *gno.Machine) { + // For testing purposes here, nanoseconds are separately kept track. + arg0 := m.LastBlock().GetParams1().TV + d := arg0.GetInt64() + sec := d / int64(time.Second) + nano := d % int64(time.Second) + ctx := m.Context.(*teststd.TestExecContext) + ctx.Timestamp += sec + ctx.TimestampNano += nano + if ctx.TimestampNano >= int64(time.Second) { + ctx.Timestamp += 1 + ctx.TimestampNano -= int64(time.Second) + } + m.Context = ctx + }, + ) + return pkg, pkg.NewPackage() + case "math/big": + pkg := gno.NewPackageNode("big", pkgPath, nil) + pkg.DefineGoNativeValue("NewInt", big.NewInt) + return pkg, pkg.NewPackage() + } + + // Load normal stdlib. + pn, pv = loadStdlib(rootDir, pkgPath, store, stdout) + if pn != nil { + return + } + + // if examples package... + examplePath := filepath.Join(rootDir, "examples", pkgPath) + if osm.DirExists(examplePath) { + memPkg := gno.ReadMemPackage(examplePath, pkgPath) + if memPkg.IsEmpty() { + panic(fmt.Sprintf("found an empty package %q", pkgPath)) + } + + send := std.Coins{} + ctx := Context(pkgPath, send) + m2 := gno.NewMachineWithOptions(gno.MachineOptions{ + PkgPath: "test", + Output: stdout, + Store: store, + Context: ctx, + }) + pn, pv = m2.RunMemPackage(memPkg, true) + return + } + return nil, nil + } + db := memdb.NewMemDB() + baseStore = dbadapter.StoreConstructor(db, storetypes.StoreOptions{}) + // Make a new store. + resStore = gno.NewStore(nil, baseStore, baseStore) + resStore.SetPackageGetter(getPackage) + resStore.SetNativeStore(teststdlibs.NativeStore) + return +} + +func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) { + dirs := [...]string{ + // Normal stdlib path. + filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath), + // Override path. Definitions here override the previous if duplicate. + filepath.Join(rootDir, "gnovm", "tests", "stdlibs", pkgPath), + } + files := make([]string, 0, 32) // pre-alloc 32 as a likely high number of files + for _, path := range dirs { + dl, err := os.ReadDir(path) + if err != nil { + if os.IsNotExist(err) { + continue + } + panic(fmt.Errorf("could not access dir %q: %w", path, err)) + } + + for _, f := range dl { + // NOTE: RunMemPackage has other rules; those should be mostly useful + // for on-chain packages (ie. include README and gno.mod). + if !f.IsDir() && strings.HasSuffix(f.Name(), ".gno") { + files = append(files, filepath.Join(path, f.Name())) + } + } + } + if len(files) == 0 { + return nil, nil + } + + memPkg := gno.ReadMemPackageFromList(files, pkgPath) + m2 := gno.NewMachineWithOptions(gno.MachineOptions{ + // NOTE: see also pkgs/sdk/vm/builtins.go + // Needs PkgPath != its name because TestStore.getPackage is the package + // getter for the store, which calls loadStdlib, so it would be recursively called. + PkgPath: "stdlibload", + Output: stdout, + Store: store, + }) + return m2.RunMemPackageWithOverrides(memPkg, true) +} + +type stackWrappedError struct { + err error + stack []byte +} + +func (e *stackWrappedError) Error() string { return e.err.Error() } +func (e *stackWrappedError) Unwrap() error { return e.err } +func (e *stackWrappedError) String() string { + return fmt.Sprintf("%v\nstack:\n%v", e.err, string(e.stack)) +} + +// LoadImports parses the given file and attempts to retrieve all pure packages +// from the store. This is mostly useful for "eager import loading", whereby all +// imports are pre-loaded in a permanent store, so that the tests can use +// ephemeral transaction stores. +func LoadImports(store gno.Store, filename string, content []byte) (err error) { + defer func() { + // This is slightly different from the handling below; we do not have a + // machine to work with, as this comes from an import; so we need + // "machine-less" alternatives. (like v.String instead of v.Sprint) + if r := recover(); r != nil { + switch v := r.(type) { + case *gno.TypedValue: + err = errors.New(v.String()) + case *gno.PreprocessError: + err = v.Unwrap() + case gno.UnhandledPanicError: + err = v + case error: + err = &stackWrappedError{v, debug.Stack()} + default: + err = &stackWrappedError{fmt.Errorf("%v", v), debug.Stack()} + } + } + }() + + fset := token.NewFileSet() + fl, err := parser.ParseFile(fset, filename, content, parser.ImportsOnly) + if err != nil { + return fmt.Errorf("parse failure: %w", err) + } + for _, imp := range fl.Imports { + impPath, err := strconv.Unquote(imp.Path.Value) + if err != nil { + return fmt.Errorf("%v: unexpected invalid import path: %v", fset.Position(imp.Pos()).String(), imp.Path.Value) + } + if gno.IsRealmPath(impPath) { + // Don't eagerly load realms. + // Realms persist state and can change the state of other realms in initialization. + continue + } + pkg := store.GetPackage(impPath, true) + if pkg == nil { + return fmt.Errorf("%v: unknown import path %v", fset.Position(imp.Pos()).String(), impPath) + } + } + return nil +} diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go new file mode 100644 index 00000000000..9374db263ee --- /dev/null +++ b/gnovm/pkg/test/test.go @@ -0,0 +1,483 @@ +// Package test contains the code to parse and execute Gno tests and filetests. +package test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "os" + "path" + "path/filepath" + "runtime/debug" + "strconv" + "strings" + "time" + + "github.com/gnolang/gno/gnovm" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs" + teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" + storetypes "github.com/gnolang/gno/tm2/pkg/store/types" + "go.uber.org/multierr" +) + +const ( + // DefaultHeight is the default height used in the [Context]. + DefaultHeight = 123 + // DefaultTimestamp is the Timestamp value used by default in [Context]. + DefaultTimestamp = 1234567890 + // DefaultCaller is the result of gno.DerivePkgAddr("user1.gno"), + // used as the default caller in [Context]. + DefaultCaller crypto.Bech32Address = "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +) + +// Context returns a TestExecContext. Usable for test purpose only. +// The returned context has a mock banker, params and event logger. It will give +// the pkgAddr the coins in `send` by default, and only that. +// The Height and Timestamp parameters are set to the [DefaultHeight] and +// [DefaultTimestamp]. +func Context(pkgPath string, send std.Coins) *teststd.TestExecContext { + // FIXME: create a better package to manage this, with custom constructors + pkgAddr := gno.DerivePkgAddr(pkgPath) // the addr of the pkgPath called. + + banker := &teststd.TestBanker{ + CoinTable: map[crypto.Bech32Address]std.Coins{ + pkgAddr.Bech32(): send, + }, + } + ctx := stdlibs.ExecContext{ + ChainID: "dev", + Height: DefaultHeight, + Timestamp: DefaultTimestamp, + OrigCaller: DefaultCaller, + OrigPkgAddr: pkgAddr.Bech32(), + OrigSend: send, + OrigSendSpent: new(std.Coins), + Banker: banker, + Params: newTestParams(), + EventLogger: sdk.NewEventLogger(), + } + return &teststd.TestExecContext{ + ExecContext: ctx, + RealmFrames: make(map[*gno.Frame]teststd.RealmOverride), + } +} + +// Machine is a minimal machine, set up with just the Store, Output and Context. +func Machine(testStore gno.Store, output io.Writer, pkgPath string) *gno.Machine { + return gno.NewMachineWithOptions(gno.MachineOptions{ + Store: testStore, + Output: output, + Context: Context(pkgPath, nil), + }) +} + +// ---------------------------------------- +// testParams + +type testParams struct{} + +func newTestParams() *testParams { + return &testParams{} +} + +func (tp *testParams) SetBool(key string, val bool) { /* noop */ } +func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ } +func (tp *testParams) SetInt64(key string, val int64) { /* noop */ } +func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ } +func (tp *testParams) SetString(key string, val string) { /* noop */ } + +// ---------------------------------------- +// main test function + +// TestOptions is a list of options that must be passed to [Test]. +type TestOptions struct { + // BaseStore / TestStore to use for the tests. + BaseStore storetypes.CommitStore + TestStore gno.Store + // Gno root dir. + RootDir string + // Used for printing program output, during verbose logging. + Output io.Writer + // Used for os.Stderr, and for printing errors. + Error io.Writer + + // Not set by NewTestOptions: + + // Flag to filter tests to run. + RunFlag string + // Whether to update filetest directives. + Sync bool + // Uses Error to print when starting a test, and prints test output directly, + // unbuffered. + Verbose bool + // Uses Error to print runtime metrics for tests. + Metrics bool + // Uses Error to print the events emitted. + Events bool + + filetestBuffer bytes.Buffer + outWriter proxyWriter +} + +// WriterForStore is the writer that should be passed to [Store], so that +// [Test] is then able to swap it when needed. +func (opts *TestOptions) WriterForStore() io.Writer { + return &opts.outWriter +} + +// NewTestOptions sets up TestOptions, filling out all "required" parameters. +func NewTestOptions(rootDir string, stdin io.Reader, stdout, stderr io.Writer) *TestOptions { + opts := &TestOptions{ + RootDir: rootDir, + Output: stdout, + Error: stderr, + } + opts.BaseStore, opts.TestStore = Store( + rootDir, false, + stdin, opts.WriterForStore(), stderr, + ) + return opts +} + +// proxyWriter is a simple wrapper around a io.Writer, it exists so that the +// underlying writer can be swapped with another when necessary. +type proxyWriter struct { + w io.Writer +} + +func (p *proxyWriter) Write(b []byte) (int, error) { + return p.w.Write(b) +} + +// tee temporarily appends the writer w to an underlying MultiWriter, which +// should then be reverted using revert(). +func (p *proxyWriter) tee(w io.Writer) (revert func()) { + save := p.w + if save == io.Discard { + p.w = w + } else { + p.w = io.MultiWriter(save, w) + } + return func() { + p.w = save + } +} + +// Test runs tests on the specified memPkg. +// fsDir is the directory on filesystem of package; it's used in case opts.Sync +// is enabled, and points to the directory where the files are contained if they +// are to be updated. +// opts is a required set of options, which is often shared among different +// tests; you can use [NewTestOptions] for a common base configuration. +func Test(memPkg *gnovm.MemPackage, fsDir string, opts *TestOptions) error { + opts.outWriter.w = opts.Output + + var errs error + + // Stands for "test", "integration test", and "filetest". + // "integration test" are the test files with `package xxx_test` (they are + // not necessarily integration tests, it's just for our internal reference.) + tset, itset, itfiles, ftfiles := parseMemPackageTests(opts.TestStore, memPkg) + + // Testing with *_test.gno + if len(tset.Files)+len(itset.Files) > 0 { + // Create a common cw/gs for both the `pkg` tests as well as the `pkg_test` + // tests. This allows us to "export" symbols from the pkg tests and + // import them from the `pkg_test` tests. + cw := opts.BaseStore.CacheWrap() + gs := opts.TestStore.BeginTransaction(cw, cw) + + // Run test files in pkg. + if len(tset.Files) > 0 { + err := opts.runTestFiles(memPkg, tset, cw, gs) + if err != nil { + errs = multierr.Append(errs, err) + } + } + + // Test xxx_test pkg. + if len(itset.Files) > 0 { + itPkg := &gnovm.MemPackage{ + Name: memPkg.Name + "_test", + Path: memPkg.Path + "_test", + Files: itfiles, + } + + err := opts.runTestFiles(itPkg, itset, cw, gs) + if err != nil { + errs = multierr.Append(errs, err) + } + } + } + + // Testing with *_filetest.gno. + if len(ftfiles) > 0 { + filter := splitRegexp(opts.RunFlag) + for _, testFile := range ftfiles { + testFileName := testFile.Name + testFilePath := filepath.Join(fsDir, testFileName) + testName := "file/" + testFileName + if !shouldRun(filter, testName) { + continue + } + + startedAt := time.Now() + if opts.Verbose { + fmt.Fprintf(opts.Error, "=== RUN %s\n", testName) + } + + changed, err := opts.runFiletest(testFileName, []byte(testFile.Body)) + if changed != "" { + // Note: changed always == "" if opts.Sync == false. + err = os.WriteFile(testFilePath, []byte(changed), 0o644) + if err != nil { + panic(fmt.Errorf("could not fix golden file: %w", err)) + } + } + + duration := time.Since(startedAt) + dstr := fmtDuration(duration) + if err != nil { + fmt.Fprintf(opts.Error, "--- FAIL: %s (%s)\n", testName, dstr) + fmt.Fprintln(opts.Error, err.Error()) + errs = multierr.Append(errs, fmt.Errorf("%s failed", testName)) + } else if opts.Verbose { + fmt.Fprintf(opts.Error, "--- PASS: %s (%s)\n", testName, dstr) + } + + // XXX: add per-test metrics + } + } + + return errs +} + +func (opts *TestOptions) runTestFiles( + memPkg *gnovm.MemPackage, + files *gno.FileSet, + cw storetypes.Store, gs gno.TransactionStore, +) (errs error) { + var m *gno.Machine + defer func() { + if r := recover(); r != nil { + if st := m.ExceptionsStacktrace(); st != "" { + errs = multierr.Append(errors.New(st), errs) + } + errs = multierr.Append( + fmt.Errorf("panic: %v\ngo stacktrace:\n%v\ngno machine: %v\ngno stacktrace:\n%v", + r, string(debug.Stack()), m.String(), m.Stacktrace()), + errs, + ) + } + }() + + tests := loadTestFuncs(memPkg.Name, files) + + var alloc *gno.Allocator + if opts.Metrics { + alloc = gno.NewAllocator(math.MaxInt64) + } + + // Check if we already have the package - it may have been eagerly + // loaded. + m = Machine(gs, opts.WriterForStore(), memPkg.Path) + m.Alloc = alloc + if opts.TestStore.GetMemPackage(memPkg.Path) == nil { + m.RunMemPackage(memPkg, true) + } else { + m.SetActivePackage(gs.GetPackage(memPkg.Path, false)) + } + pv := m.Package + + m.RunFiles(files.Files...) + + for _, tf := range tests { + // TODO(morgan): we could theoretically use wrapping on the baseStore + // and gno store to achieve per-test isolation. However, that requires + // some deeper changes, as ideally we'd: + // - Run the MemPackage independently (so it can also be run as a + // consequence of an import) + // - Run the test files before this for loop (but persist it to store; + // RunFiles doesn't do that currently) + // - Wrap here. + m = Machine(gs, opts.Output, memPkg.Path) + m.Alloc = alloc + m.SetActivePackage(pv) + + testingpv := m.Store.GetPackage("testing", false) + testingtv := gno.TypedValue{T: &gno.PackageType{}, V: testingpv} + testingcx := &gno.ConstExpr{TypedValue: testingtv} + + eval := m.Eval(gno.Call( + gno.Sel(testingcx, "RunTest"), // Call testing.RunTest + gno.Str(opts.RunFlag), // run flag + gno.Nx(strconv.FormatBool(opts.Verbose)), // is verbose? + &gno.CompositeLitExpr{ // Third param, the testing.InternalTest + Type: gno.Sel(testingcx, "InternalTest"), + Elts: gno.KeyValueExprs{ + {Key: gno.X("Name"), Value: gno.Str(tf.Name)}, + {Key: gno.X("F"), Value: gno.Nx(tf.Name)}, + }, + }, + )) + + if opts.Events { + events := m.Context.(*teststd.TestExecContext).EventLogger.Events() + if events != nil { + res, err := json.Marshal(events) + if err != nil { + panic(err) + } + fmt.Fprintf(opts.Error, "EVENTS: %s\n", string(res)) + } + } + + ret := eval[0].GetString() + if ret == "" { + err := fmt.Errorf("failed to execute unit test: %q", tf.Name) + errs = multierr.Append(errs, err) + fmt.Fprintf(opts.Error, "--- FAIL: %s [internal gno testing error]", tf.Name) + continue + } + + // TODO: replace with amino or send native type? + var rep report + err := json.Unmarshal([]byte(ret), &rep) + if err != nil { + errs = multierr.Append(errs, err) + fmt.Fprintf(opts.Error, "--- FAIL: %s [internal gno testing error]", tf.Name) + continue + } + + if rep.Failed { + err := fmt.Errorf("failed: %q", tf.Name) + errs = multierr.Append(errs, err) + } + + if opts.Metrics { + // XXX: store changes + // XXX: max mem consumption + allocsVal := "n/a" + if m.Alloc != nil { + maxAllocs, allocs := m.Alloc.Status() + allocsVal = fmt.Sprintf("%s(%.2f%%)", + prettySize(allocs), + float64(allocs)/float64(maxAllocs)*100, + ) + } + fmt.Fprintf(opts.Error, "--- runtime: cycle=%s allocs=%s\n", + prettySize(m.Cycles), + allocsVal, + ) + } + } + + return errs +} + +// report is a mirror of Gno's stdlibs/testing.Report. +type report struct { + Failed bool + Skipped bool +} + +type testFunc struct { + Package string + Name string +} + +func loadTestFuncs(pkgName string, tfiles *gno.FileSet) (rt []testFunc) { + for _, tf := range tfiles.Files { + for _, d := range tf.Decls { + if fd, ok := d.(*gno.FuncDecl); ok { + fname := string(fd.Name) + if strings.HasPrefix(fname, "Test") { + tf := testFunc{ + Package: pkgName, + Name: fname, + } + rt = append(rt, tf) + } + } + } + } + return +} + +// parseMemPackageTests parses test files (skipping filetests) in the memPkg. +func parseMemPackageTests(store gno.Store, memPkg *gnovm.MemPackage) (tset, itset *gno.FileSet, itfiles, ftfiles []*gnovm.MemFile) { + tset = &gno.FileSet{} + itset = &gno.FileSet{} + var errs error + for _, mfile := range memPkg.Files { + if !strings.HasSuffix(mfile.Name, ".gno") { + continue // skip this file. + } + + if err := LoadImports(store, path.Join(memPkg.Path, mfile.Name), []byte(mfile.Body)); err != nil { + errs = multierr.Append(errs, err) + } + + n, err := gno.ParseFile(mfile.Name, mfile.Body) + if err != nil { + errs = multierr.Append(errs, err) + continue + } + if n == nil { + panic("should not happen") + } + switch { + case strings.HasSuffix(mfile.Name, "_filetest.gno"): + ftfiles = append(ftfiles, mfile) + case strings.HasSuffix(mfile.Name, "_test.gno") && memPkg.Name == string(n.PkgName): + tset.AddFiles(n) + case strings.HasSuffix(mfile.Name, "_test.gno") && memPkg.Name+"_test" == string(n.PkgName): + itset.AddFiles(n) + itfiles = append(itfiles, mfile) + case memPkg.Name == string(n.PkgName): + // normal package file + default: + panic(fmt.Sprintf( + "expected package name [%s] or [%s_test] but got [%s] file [%s]", + memPkg.Name, memPkg.Name, n.PkgName, mfile)) + } + } + if errs != nil { + panic(errs) + } + return +} + +func shouldRun(filter filterMatch, path string) bool { + if filter == nil { + return true + } + elem := strings.Split(path, "/") + ok, _ := filter.matches(elem, matchString) + return ok +} + +// Adapted from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ +func prettySize(nb int64) string { + const unit = 1000 + if nb < unit { + return fmt.Sprintf("%d", nb) + } + div, exp := int64(unit), 0 + for n := nb / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f%c", float64(nb)/float64(div), "kMGTPE"[exp]) +} + +func fmtDuration(d time.Duration) string { + return fmt.Sprintf("%.2fs", d.Seconds()) +} diff --git a/gnovm/cmd/gno/util_match.go b/gnovm/pkg/test/util_match.go similarity index 99% rename from gnovm/cmd/gno/util_match.go rename to gnovm/pkg/test/util_match.go index 34181f61254..a3fec22f389 100644 --- a/gnovm/cmd/gno/util_match.go +++ b/gnovm/pkg/test/util_match.go @@ -1,4 +1,4 @@ -package main +package test // Most of the code in this file is extracted from golang's src/testing/match.go. // diff --git a/gnovm/stdlibs/io/example_test.gno b/gnovm/stdlibs/io/example_test.gno index c781fb9166e..54a9e74f55a 100644 --- a/gnovm/stdlibs/io/example_test.gno +++ b/gnovm/stdlibs/io/example_test.gno @@ -8,7 +8,6 @@ import ( "bytes" "fmt" "io" - "log" "os" "strings" ) @@ -17,7 +16,7 @@ func ExampleCopy() { r := strings.NewReader("some io.Reader stream to be read\n") if _, err := io.Copy(os.Stdout, r); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -31,12 +30,12 @@ func ExampleCopyBuffer() { // buf is used here... if _, err := io.CopyBuffer(os.Stdout, r1, buf); err != nil { - log.Fatal(err) + panic(err) } // ... reused here also. No need to allocate an extra buffer. if _, err := io.CopyBuffer(os.Stdout, r2, buf); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -48,7 +47,7 @@ func ExampleCopyN() { r := strings.NewReader("some io.Reader stream to be read") if _, err := io.CopyN(os.Stdout, r, 4); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -60,7 +59,7 @@ func ExampleReadAtLeast() { buf := make([]byte, 14) if _, err := io.ReadAtLeast(r, buf, 4); err != nil { - log.Fatal(err) + panic(err) } fmt.Printf("%s\n", buf) @@ -87,7 +86,7 @@ func ExampleReadFull() { buf := make([]byte, 4) if _, err := io.ReadFull(r, buf); err != nil { - log.Fatal(err) + panic(err) } fmt.Printf("%s\n", buf) @@ -104,7 +103,7 @@ func ExampleReadFull() { func ExampleWriteString() { if _, err := io.WriteString(os.Stdout, "Hello World"); err != nil { - log.Fatal(err) + panic(err) } // Output: Hello World @@ -115,7 +114,7 @@ func ExampleLimitReader() { lr := io.LimitReader(r, 4) if _, err := io.Copy(os.Stdout, lr); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -129,7 +128,7 @@ func ExampleMultiReader() { r := io.MultiReader(r1, r2, r3) if _, err := io.Copy(os.Stdout, r); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -153,7 +152,7 @@ func ExampleSectionReader() { s := io.NewSectionReader(r, 5, 17) if _, err := io.Copy(os.Stdout, s); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -166,7 +165,7 @@ func ExampleSectionReader_ReadAt() { buf := make([]byte, 6) if _, err := s.ReadAt(buf, 10); err != nil { - log.Fatal(err) + panic(err) } fmt.Printf("%s\n", buf) @@ -180,11 +179,11 @@ func ExampleSectionReader_Seek() { s := io.NewSectionReader(r, 5, 17) if _, err := s.Seek(10, io.SeekStart); err != nil { - log.Fatal(err) + panic(err) } if _, err := io.Copy(os.Stdout, s); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -196,12 +195,12 @@ func ExampleSeeker_Seek() { r.Seek(5, io.SeekStart) // move to the 5th char from the start if _, err := io.Copy(os.Stdout, r); err != nil { - log.Fatal(err) + panic(err) } r.Seek(-5, io.SeekEnd) if _, err := io.Copy(os.Stdout, r); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -216,7 +215,7 @@ func ExampleMultiWriter() { w := io.MultiWriter(&buf1, &buf2) if _, err := io.Copy(w, r); err != nil { - log.Fatal(err) + panic(err) } fmt.Print(buf1.String()) @@ -237,7 +236,7 @@ func ExamplePipe() { }() if _, err := io.Copy(os.Stdout, r); err != nil { - log.Fatal(err) + panic(err) } // Output: @@ -250,7 +249,7 @@ func ExampleReadAll() { b, err := io.ReadAll(r) if err != nil { - log.Fatal(err) + panic(err) } fmt.Printf("%s", b) diff --git a/gnovm/stdlibs/io/multi_test.gno b/gnovm/stdlibs/io/multi_test.gno index 8932ace2e59..f39895ea776 100644 --- a/gnovm/stdlibs/io/multi_test.gno +++ b/gnovm/stdlibs/io/multi_test.gno @@ -6,7 +6,7 @@ package io import ( "bytes" - "crypto/sha1" + "crypto/sha256" "fmt" "strings" "testing" @@ -119,8 +119,8 @@ func testMultiWriter(t *testing.T, sink interface { Stringer }, ) { - sha1 := sha1.New() - mw := MultiWriter(sha1, sink) + var buf bytes.Buffer + mw := MultiWriter(&buf, sink) sourceString := "My input text." source := strings.NewReader(sourceString) @@ -134,9 +134,9 @@ func testMultiWriter(t *testing.T, sink interface { t.Errorf("unexpected error: %v", err) } - sha1hex := fmt.Sprintf("%x", sha1.Sum(nil)) - if sha1hex != "01cb303fa8c30a64123067c5aa6284ba7ec2d31b" { - t.Error("incorrect sha1 value") + sha1hex := fmt.Sprintf("%x", sha256.Sum256(buf.Bytes())) + if sha1hex != "d3e9e78d2a7e9c4756a4e8e57db6a57ccfd84c6d656d66b9d2bd2620b4ab81b8" { + t.Error("incorrect sha256 value") } if sink.String() != sourceString { diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index a0dafe5dc44..01e763ab82e 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -12,7 +12,6 @@ type ExecContext struct { Height int64 Timestamp int64 // seconds TimestampNano int64 // nanoseconds, only used for testing. - Msg sdk.Msg OrigCaller crypto.Bech32Address OrigPkgAddr crypto.Bech32Address OrigSend std.Coins diff --git a/gnovm/stdlibs/strconv/example_test.gno b/gnovm/stdlibs/strconv/example_test.gno index 428fde4e660..d3ef2cc4244 100644 --- a/gnovm/stdlibs/strconv/example_test.gno +++ b/gnovm/stdlibs/strconv/example_test.gno @@ -6,7 +6,6 @@ package strconv_test import ( "fmt" - "log" "strconv" ) @@ -409,7 +408,7 @@ func ExampleUnquote() { func ExampleUnquoteChar() { v, mb, t, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"') if err != nil { - log.Fatal(err) + panic(err) } fmt.Println("value:", string(v)) diff --git a/gnovm/stdlibs/testing/match.gno b/gnovm/stdlibs/testing/match.gno index 3b22602890d..8b099f37624 100644 --- a/gnovm/stdlibs/testing/match.gno +++ b/gnovm/stdlibs/testing/match.gno @@ -16,11 +16,11 @@ import ( type filterMatch interface { // matches checks the name against the receiver's pattern strings using the // given match function. - matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) + matches(name []string) (ok, partial bool) // verify checks that the receiver's pattern strings are valid filters by // calling the given match function. - verify(name string, matchString func(pat, str string) (bool, error)) error + verify(name string) error } // simpleMatch matches a test name if all of the pattern strings match in @@ -30,43 +30,43 @@ type simpleMatch []string // alternationMatch matches a test name if one of the alternations match. type alternationMatch []filterMatch -func (m simpleMatch) matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) { +func (m simpleMatch) matches(name []string) (ok, partial bool) { for i, s := range name { if i >= len(m) { break } - if ok, _ := matchString(m[i], s); !ok { + if ok, _ := regexp.MatchString(m[i], s); !ok { return false, false } } return true, len(name) < len(m) } -func (m simpleMatch) verify(name string, matchString func(pat, str string) (bool, error)) error { +func (m simpleMatch) verify(name string) error { for i, s := range m { m[i] = rewrite(s) } // Verify filters before doing any processing. for i, s := range m { - if _, err := matchString(s, "non-empty"); err != nil { + if _, err := regexp.MatchString(s, "non-empty"); err != nil { return fmt.Errorf("element %d of %s (%q): %s", i, name, s, err) } } return nil } -func (m alternationMatch) matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) { +func (m alternationMatch) matches(name []string) (ok, partial bool) { for _, m := range m { - if ok, partial = m.matches(name, matchString); ok { + if ok, partial = m.matches(name); ok { return ok, partial } } return false, false } -func (m alternationMatch) verify(name string, matchString func(pat, str string) (bool, error)) error { +func (m alternationMatch) verify(name string) error { for i, m := range m { - if err := m.verify(name, matchString); err != nil { + if err := m.verify(name); err != nil { return fmt.Errorf("alternation %d of %s", i, err) } } @@ -164,20 +164,3 @@ func isSpace(r rune) bool { } return false } - -var ( - matchPat string - matchRe *regexp.Regexp -) - -// based on testing/internal/testdeps.TestDeps.MatchString. -func matchString(pat, str string) (result bool, err error) { - if matchRe == nil || matchPat != pat { - matchPat = pat - matchRe, err = regexp.Compile(matchPat) - if err != nil { - return - } - } - return matchRe.MatchString(str), nil -} diff --git a/gnovm/stdlibs/testing/testing.gno b/gnovm/stdlibs/testing/testing.gno index 6e55c5cc283..fdafd9652ba 100644 --- a/gnovm/stdlibs/testing/testing.gno +++ b/gnovm/stdlibs/testing/testing.gno @@ -280,7 +280,7 @@ func (t *T) shouldRun(name string) bool { } elem := strings.Split(name, "/") - ok, partial := t.runFilter.matches(elem, matchString) + ok, partial := t.runFilter.matches(elem) _ = partial // we don't care right now return ok } diff --git a/gnovm/tests/README.md b/gnovm/tests/README.md index 378d5d9dc1b..d35c6590e2f 100644 --- a/gnovm/tests/README.md +++ b/gnovm/tests/README.md @@ -1 +1,35 @@ -All the files in the ./files directory are meant to be those derived/borrowed from Yaegi, Apache2.0. +# tests + +This directory contains integration tests for the GnoVM. This file aims to provide a brief overview. + +GnoVM tests and filetests run in a special context relating to its imports. +You can see the additional Gonative functions in [gnovm/pkg/test/imports.go](../pkg/test/imports.go). +You can see additional standard libraries and standard library functions +available in testing in [gnovm/tests/stdlibs](./stdlibs). + +## `files`: GnoVM filetests + +The most important directory is `files`, which contains filetests for the Gno +project. These are executed by the `TestFiles` test in the `gnovm/pkg/gnolang` +directory. + +The `files/extern` directory contains several packages used to test the import +system. The packages here are imported with the prefix +`github.com/gnolang/gno/_test/`, exclusively within these filetests. + +Tests with the `_long` suffix are skipped when the `-short` flag is passed. + +These tests are largely derived from Yaegi, licensed under Apache 2.0. + +## `stdlibs`: testing standard libraries + +These contain standard libraries which are only available in testing, and +extensions of them, like `std.TestSkipHeights`. + +## other directories + +- `backup` has been here since forever; and somebody should come around and delete it at some point. +- `challenges` contains code that supposedly doesn't work, but should. +- `integ` contains some files for integration tests which likely should have + been in some `testdata` directory to begin with. You guessed it, + they're here until someone bothers to move them out. diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go deleted file mode 100644 index 98dbab6ac0e..00000000000 --- a/gnovm/tests/file.go +++ /dev/null @@ -1,713 +0,0 @@ -package tests - -import ( - "bytes" - "encoding/json" - "fmt" - "go/ast" - "go/parser" - "go/token" - "io" - "os" - "regexp" - rtdb "runtime/debug" - "strconv" - "strings" - - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/gnovm" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/stdlibs" - teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" - "github.com/gnolang/gno/tm2/pkg/crypto" - osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/sdk" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/pmezard/go-difflib/difflib" -) - -type loggerFunc func(args ...interface{}) - -func TestMachine(store gno.Store, stdout io.Writer, pkgPath string) *gno.Machine { - // default values - var ( - send std.Coins - maxAlloc int64 - ) - - return testMachineCustom(store, pkgPath, stdout, maxAlloc, send) -} - -func testMachineCustom(store gno.Store, pkgPath string, stdout io.Writer, maxAlloc int64, send std.Coins) *gno.Machine { - ctx := TestContext(pkgPath, send) - m := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "", // set later. - Output: stdout, - Store: store, - Context: ctx, - MaxAllocBytes: maxAlloc, - }) - return m -} - -// TestContext returns a TestExecContext. Usable for test purpose only. -func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { - // FIXME: create a better package to manage this, with custom constructors - pkgAddr := gno.DerivePkgAddr(pkgPath) // the addr of the pkgPath called. - caller := gno.DerivePkgAddr("user1.gno") - - pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(send) // >= send. - banker := newTestBanker(pkgAddr.Bech32(), pkgCoins) - params := newTestParams() - ctx := stdlibs.ExecContext{ - ChainID: "dev", - Height: 123, - Timestamp: 1234567890, - Msg: nil, - OrigCaller: caller.Bech32(), - OrigPkgAddr: pkgAddr.Bech32(), - OrigSend: send, - OrigSendSpent: new(std.Coins), - Banker: banker, - Params: params, - EventLogger: sdk.NewEventLogger(), - } - return &teststd.TestExecContext{ - ExecContext: ctx, - RealmFrames: make(map[*gno.Frame]teststd.RealmOverride), - } -} - -// CleanupMachine can be called during two tests while reusing the same Machine instance. -func CleanupMachine(m *gno.Machine) { - prevCtx := m.Context.(*teststd.TestExecContext) - prevSend := prevCtx.OrigSend - - newCtx := TestContext("", prevCtx.OrigSend) - pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(prevSend) // >= send. - banker := newTestBanker(prevCtx.OrigPkgAddr, pkgCoins) - newCtx.OrigPkgAddr = prevCtx.OrigPkgAddr - newCtx.Banker = banker - m.Context = newCtx -} - -type runFileTestOptions struct { - nativeLibs bool - logger loggerFunc - syncWanted bool -} - -// RunFileTestOptions specify changing options in [RunFileTest], deviating -// from the zero value. -type RunFileTestOption func(*runFileTestOptions) - -// WithNativeLibs enables using go native libraries (ie, [ImportModeNativePreferred]) -// instead of using stdlibs/*. -func WithNativeLibs() RunFileTestOption { - return func(r *runFileTestOptions) { r.nativeLibs = true } -} - -// WithLoggerFunc sets a logging function for [RunFileTest]. -func WithLoggerFunc(f func(args ...interface{})) RunFileTestOption { - return func(r *runFileTestOptions) { r.logger = f } -} - -// WithSyncWanted sets the syncWanted flag to true. -// It rewrites tests files so that the values of Output: and of Realm: -// comments match the actual output or realm state after the test. -func WithSyncWanted(v bool) RunFileTestOption { - return func(r *runFileTestOptions) { r.syncWanted = v } -} - -// RunFileTest executes the filetest at the given path, using rootDir as -// the directory where to find the "stdlibs" directory. -func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { - var f runFileTestOptions - for _, opt := range opts { - opt(&f) - } - - directives, pkgPath, resWanted, errWanted, rops, eventsWanted, stacktraceWanted, maxAlloc, send, preWanted := wantedFromComment(path) - if pkgPath == "" { - pkgPath = "main" - } - pkgName := DefaultPkgName(pkgPath) - stdin := new(bytes.Buffer) - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - mode := ImportModeStdlibsPreferred - if f.nativeLibs { - mode = ImportModeNativePreferred - } - store := TestStore(rootDir, "./files", stdin, stdout, stderr, mode) - store.SetLogStoreOps(true) - m := testMachineCustom(store, pkgPath, stdout, maxAlloc, send) - checkMachineIsEmpty := true - - // TODO support stdlib groups, but make testing safe; - // e.g. not be able to make network connections. - // interp.New(interp.Options{GoPath: goPath, Stdout: &stdout, Stderr: &stderr}) - // m.Use(interp.Symbols) - // m.Use(stdlib.Symbols) - // m.Use(unsafe.Symbols) - bz, err := os.ReadFile(path) - if err != nil { - return err - } - { // Validate result, errors, etc. - var pnc interface{} - func() { - defer func() { - if r := recover(); r != nil { - // print output. - fmt.Printf("OUTPUT:\n%s\n", stdout.String()) - pnc = r - err := strings.TrimSpace(fmt.Sprintf("%v", pnc)) - // print stack if unexpected error. - if errWanted == "" || - !strings.Contains(err, errWanted) { - fmt.Printf("ERROR:\n%s\n", err) - // error didn't match: print stack - // NOTE: will fail testcase later. - rtdb.PrintStack() - } - } - }() - if f.logger != nil { - f.logger("========================================") - f.logger("RUN FILES & INIT") - f.logger("========================================") - } - if !gno.IsRealmPath(pkgPath) { - // simple case. - pn := gno.NewPackageNode(pkgName, pkgPath, &gno.FileSet{}) - pv := pn.NewPackage() - store.SetBlockNode(pn) - store.SetCachePackage(pv) - m.SetActivePackage(pv) - n := gno.MustParseFile(path, string(bz)) // "main.gno", string(bz)) - m.RunFiles(n) - if f.logger != nil { - f.logger("========================================") - f.logger("RUN MAIN") - f.logger("========================================") - } - m.RunMain() - if f.logger != nil { - f.logger("========================================") - f.logger("RUN MAIN END") - f.logger("========================================") - } - } else { - // realm case. - store.SetStrictGo2GnoMapping(true) // in gno.land, natives must be registered. - gno.DisableDebug() // until main call. - // save package using realm crawl procedure. - memPkg := &gnovm.MemPackage{ - Name: string(pkgName), - Path: pkgPath, - Files: []*gnovm.MemFile{ - { - Name: "main.gno", // dontcare - Body: string(bz), - }, - }, - } - // run decls and init functions. - m.RunMemPackage(memPkg, true) - // reconstruct machine and clear store cache. - // whether package is realm or not, since non-realm - // may call realm packages too. - if f.logger != nil { - f.logger("========================================") - f.logger("CLEAR STORE CACHE") - f.logger("========================================") - } - store.ClearCache() - /* - m = gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "", - Output: stdout, - Store: store, - Context: ctx, - MaxAllocBytes: maxAlloc, - }) - */ - if f.logger != nil { - store.Print() - f.logger("========================================") - f.logger("PREPROCESS ALL FILES") - f.logger("========================================") - } - m.PreprocessAllFilesAndSaveBlockNodes() - if f.logger != nil { - f.logger("========================================") - f.logger("RUN MAIN") - f.logger("========================================") - store.Print() - } - pv2 := store.GetPackage(pkgPath, false) - m.SetActivePackage(pv2) - gno.EnableDebug() - if rops != "" { - // clear store.opslog from init function(s), - // and PreprocessAllFilesAndSaveBlockNodes(). - store.SetLogStoreOps(true) // resets. - } - m.RunMain() - if f.logger != nil { - f.logger("========================================") - f.logger("RUN MAIN END") - f.logger("========================================") - } - } - }() - - for _, directive := range directives { - switch directive { - case "Error": - // errWanted given - if errWanted != "" { - if pnc == nil { - panic(fmt.Sprintf("fail on %s: got nil error, want: %q", path, errWanted)) - } - - errstr := "" - switch v := pnc.(type) { - case *gno.TypedValue: - errstr = v.Sprint(m) - case *gno.PreprocessError: - errstr = v.Unwrap().Error() - case gno.UnhandledPanicError: - errstr = v.Error() - default: - errstr = strings.TrimSpace(fmt.Sprintf("%v", pnc)) - } - - parts := strings.SplitN(errstr, ":\n--- preprocess stack ---", 2) - if len(parts) == 2 { - fmt.Println(parts[0]) - errstr = parts[0] - } - if errstr != errWanted { - if f.syncWanted { - // write error to file - replaceWantedInPlace(path, "Error", errstr) - } else { - panic(fmt.Sprintf("fail on %s: got %q, want: %q", path, errstr, errWanted)) - } - } - - // NOTE: ignores any gno.GetDebugErrors(). - gno.ClearDebugErrors() - checkMachineIsEmpty = false // nothing more to do. - } else { - // record errors when errWanted is empty and pnc not nil - if pnc != nil { - errstr := "" - if tv, ok := pnc.(*gno.TypedValue); ok { - errstr = tv.Sprint(m) - } else { - errstr = strings.TrimSpace(fmt.Sprintf("%v", pnc)) - } - parts := strings.SplitN(errstr, ":\n--- preprocess stack ---", 2) - if len(parts) == 2 { - fmt.Println(parts[0]) - errstr = parts[0] - } - // check tip line, write to file - ctl := errstr + - "\n*** CHECK THE ERR MESSAGES ABOVE, MAKE SURE IT'S WHAT YOU EXPECTED, " + - "DELETE THIS LINE AND RUN TEST AGAIN ***" - // write error to file - replaceWantedInPlace(path, "Error", ctl) - panic(fmt.Sprintf("fail on %s: err recorded, check the message and run test again", path)) - } - // check gno debug errors when errWanted is empty, pnc is nil - if gno.HasDebugErrors() { - panic(fmt.Sprintf("fail on %s: got unexpected debug error(s): %v", path, gno.GetDebugErrors())) - } - // pnc is nil, errWanted empty, no gno debug errors - checkMachineIsEmpty = false - } - case "Output": - // panic if got unexpected error - if pnc != nil { - if tv, ok := pnc.(*gno.TypedValue); ok { - panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m))) - } else { // happens on 'unknown import path ...' - panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc)) - } - } - // check result - res := strings.TrimSpace(stdout.String()) - res = trimTrailingSpaces(res) - if res != resWanted { - if f.syncWanted { - // write output to file. - replaceWantedInPlace(path, "Output", res) - } else { - // panic so tests immediately fail (for now). - if resWanted == "" { - panic(fmt.Sprintf("fail on %s: got unexpected output: %s", path, res)) - } else { - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(resWanted), - B: difflib.SplitLines(res), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) - } - } - } - case "Events": - // panic if got unexpected error - - if pnc != nil { - if tv, ok := pnc.(*gno.TypedValue); ok { - panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m))) - } else { // happens on 'unknown import path ...' - panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc)) - } - } - // check result - events := m.Context.(*teststd.TestExecContext).EventLogger.Events() - evtjson, err := json.MarshalIndent(events, "", " ") - if err != nil { - panic(err) - } - evtstr := trimTrailingSpaces(string(evtjson)) - if evtstr != eventsWanted { - if f.syncWanted { - // write output to file. - replaceWantedInPlace(path, "Events", evtstr) - } else { - // panic so tests immediately fail (for now). - if eventsWanted == "" { - panic(fmt.Sprintf("fail on %s: got unexpected events: %s", path, evtstr)) - } else { - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(eventsWanted), - B: difflib.SplitLines(evtstr), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) - } - } - } - case "Realm": - // panic if got unexpected error - if pnc != nil { - if tv, ok := pnc.(*gno.TypedValue); ok { - panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m))) - } else { // TODO: does this happen? - panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc)) - } - } - // check realm ops - if rops != "" { - rops2 := strings.TrimSpace(store.SprintStoreOps()) - if rops != rops2 { - if f.syncWanted { - // write output to file. - replaceWantedInPlace(path, "Realm", rops2) - } else { - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(rops), - B: difflib.SplitLines(rops2), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) - } - } - } - case "Preprocessed": - // check preprocessed AST. - pn := store.GetBlockNode(gno.PackageNodeLocation(pkgPath)) - pre := pn.(*gno.PackageNode).FileSet.Files[0].String() - if pre != preWanted { - if f.syncWanted { - // write error to file - replaceWantedInPlace(path, "Preprocessed", pre) - } else { - // panic so tests immediately fail (for now). - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(preWanted), - B: difflib.SplitLines(pre), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) - } - } - case "Stacktrace": - if stacktraceWanted != "" { - var stacktrace string - - switch pnc.(type) { - case gno.UnhandledPanicError: - stacktrace = m.ExceptionsStacktrace() - default: - stacktrace = m.Stacktrace().String() - } - - if f.syncWanted { - // write stacktrace to file - replaceWantedInPlace(path, "Stacktrace", stacktrace) - } else { - if !strings.Contains(stacktrace, stacktraceWanted) { - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(stacktraceWanted), - B: difflib.SplitLines(stacktrace), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) - } - } - } - checkMachineIsEmpty = false - default: - return nil - } - } - } - - if checkMachineIsEmpty { - // Check that machine is empty. - err = m.CheckEmpty() - if err != nil { - if f.logger != nil { - f.logger("last state: \n", m.String()) - } - panic(fmt.Sprintf("fail on %s: machine not empty after main: %v", path, err)) - } - } - return nil -} - -func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops, events, stacktrace string, maxAlloc int64, send std.Coins, pre string) { - fset := token.NewFileSet() - f, err2 := parser.ParseFile(fset, p, nil, parser.ParseComments) - if err2 != nil { - panic(err2) - } - if len(f.Comments) == 0 { - return - } - for _, comments := range f.Comments { - text := readComments(comments) - if strings.HasPrefix(text, "PKGPATH:") { - line := strings.SplitN(text, "\n", 2)[0] - pkgPath = strings.TrimSpace(strings.TrimPrefix(line, "PKGPATH:")) - } else if strings.HasPrefix(text, "MAXALLOC:") { - line := strings.SplitN(text, "\n", 2)[0] - maxstr := strings.TrimSpace(strings.TrimPrefix(line, "MAXALLOC:")) - maxint, err := strconv.Atoi(maxstr) - if err != nil { - panic(fmt.Sprintf("invalid maxalloc amount: %v", maxstr)) - } - maxAlloc = int64(maxint) - } else if strings.HasPrefix(text, "SEND:") { - line := strings.SplitN(text, "\n", 2)[0] - sendstr := strings.TrimSpace(strings.TrimPrefix(line, "SEND:")) - send = std.MustParseCoins(sendstr) - } else if strings.HasPrefix(text, "Output:\n") { - res = strings.TrimPrefix(text, "Output:\n") - res = strings.TrimSpace(res) - directives = append(directives, "Output") - } else if strings.HasPrefix(text, "Error:\n") { - err = strings.TrimPrefix(text, "Error:\n") - err = strings.TrimSpace(err) - // XXX temporary until we support line:column. - // If error starts with line:column, trim it. - re := regexp.MustCompile(`^[0-9]+:[0-9]+: `) - err = re.ReplaceAllString(err, "") - directives = append(directives, "Error") - } else if strings.HasPrefix(text, "Realm:\n") { - rops = strings.TrimPrefix(text, "Realm:\n") - rops = strings.TrimSpace(rops) - directives = append(directives, "Realm") - } else if strings.HasPrefix(text, "Events:\n") { - events = strings.TrimPrefix(text, "Events:\n") - events = strings.TrimSpace(events) - directives = append(directives, "Events") - } else if strings.HasPrefix(text, "Preprocessed:\n") { - pre = strings.TrimPrefix(text, "Preprocessed:\n") - pre = strings.TrimSpace(pre) - directives = append(directives, "Preprocessed") - } else if strings.HasPrefix(text, "Stacktrace:\n") { - stacktrace = strings.TrimPrefix(text, "Stacktrace:\n") - stacktrace = strings.TrimSpace(stacktrace) - directives = append(directives, "Stacktrace") - } else { - // ignore unexpected. - } - } - return -} - -// readComments returns //-style comments from cg, but without truncating empty -// lines like cg.Text(). -func readComments(cg *ast.CommentGroup) string { - var b strings.Builder - for _, c := range cg.List { - if len(c.Text) < 2 || c.Text[:2] != "//" { - // ignore no //-style comment - break - } - s := strings.TrimPrefix(c.Text[2:], " ") - b.WriteString(s + "\n") - } - return b.String() -} - -// Replace comment in file with given output given directive. -func replaceWantedInPlace(path string, directive string, output string) { - bz := osm.MustReadFile(path) - body := string(bz) - lines := strings.Split(body, "\n") - isReplacing := false - wroteDirective := false - newlines := []string(nil) - for _, line := range lines { - if line == "// "+directive+":" { - if wroteDirective { - isReplacing = true - continue - } else { - wroteDirective = true - isReplacing = true - newlines = append(newlines, "// "+directive+":") - outlines := strings.Split(output, "\n") - for _, outline := range outlines { - newlines = append(newlines, - strings.TrimRight("// "+outline, " ")) - } - continue - } - } else if isReplacing { - if strings.HasPrefix(line, "//") { - continue - } else { - isReplacing = false - } - } - newlines = append(newlines, line) - } - osm.MustWriteFile(path, []byte(strings.Join(newlines, "\n")), 0o644) -} - -func DefaultPkgName(gopkgPath string) gno.Name { - parts := strings.Split(gopkgPath, "/") - last := parts[len(parts)-1] - parts = strings.Split(last, "-") - name := parts[len(parts)-1] - name = strings.ToLower(name) - return gno.Name(name) -} - -// go comments strip trailing spaces. -func trimTrailingSpaces(result string) string { - lines := strings.Split(result, "\n") - for i, line := range lines { - lines[i] = strings.TrimRight(line, " \t") - } - return strings.Join(lines, "\n") -} - -// ---------------------------------------- -// testParams -type testParams struct{} - -func newTestParams() *testParams { - return &testParams{} -} - -func (tp *testParams) SetBool(key string, val bool) { /* noop */ } -func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ } -func (tp *testParams) SetInt64(key string, val int64) { /* noop */ } -func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ } -func (tp *testParams) SetString(key string, val string) { /* noop */ } - -// ---------------------------------------- -// testBanker - -type testBanker struct { - coinTable map[crypto.Bech32Address]std.Coins -} - -func newTestBanker(args ...interface{}) *testBanker { - coinTable := make(map[crypto.Bech32Address]std.Coins) - if len(args)%2 != 0 { - panic("newTestBanker requires even number of arguments; addr followed by coins") - } - for i := 0; i < len(args); i += 2 { - addr := args[i].(crypto.Bech32Address) - amount := args[i+1].(std.Coins) - coinTable[addr] = amount - } - return &testBanker{ - coinTable: coinTable, - } -} - -func (tb *testBanker) GetCoins(addr crypto.Bech32Address) (dst std.Coins) { - return tb.coinTable[addr] -} - -func (tb *testBanker) SendCoins(from, to crypto.Bech32Address, amt std.Coins) { - fcoins, fexists := tb.coinTable[from] - if !fexists { - panic(fmt.Sprintf( - "source address %s does not exist", - from.String())) - } - if !fcoins.IsAllGTE(amt) { - panic(fmt.Sprintf( - "source address %s has %s; cannot send %s", - from.String(), fcoins, amt)) - } - // First, subtract from 'from'. - frest := fcoins.Sub(amt) - tb.coinTable[from] = frest - // Second, add to 'to'. - // NOTE: even works when from==to, due to 2-step isolation. - tcoins, _ := tb.coinTable[to] - tsum := tcoins.Add(amt) - tb.coinTable[to] = tsum -} - -func (tb *testBanker) TotalCoin(denom string) int64 { - panic("not yet implemented") -} - -func (tb *testBanker) IssueCoin(addr crypto.Bech32Address, denom string, amt int64) { - coins, _ := tb.coinTable[addr] - sum := coins.Add(std.Coins{{Denom: denom, Amount: amt}}) - tb.coinTable[addr] = sum -} - -func (tb *testBanker) RemoveCoin(addr crypto.Bech32Address, denom string, amt int64) { - coins, _ := tb.coinTable[addr] - rest := coins.Sub(std.Coins{{Denom: denom, Amount: amt}}) - tb.coinTable[addr] = rest -} diff --git a/gnovm/tests/file_test.go b/gnovm/tests/file_test.go deleted file mode 100644 index 4313fd88645..00000000000 --- a/gnovm/tests/file_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package tests - -import ( - "flag" - "io/fs" - "os" - "path" - "path/filepath" - "strings" - "testing" - - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" -) - -var withSync = flag.Bool("update-golden-tests", false, "rewrite tests updating Realm: and Output: with new values where changed") - -func TestFileStr(t *testing.T) { - filePath := filepath.Join(".", "files", "str.gno") - runFileTest(t, filePath, WithNativeLibs()) -} - -// Run tests in the `files` directory using shims from stdlib -// to native go standard library. -func TestFilesNative(t *testing.T) { - baseDir := filepath.Join(".", "files") - runFileTests(t, baseDir, []string{"*_stdlibs*"}, WithNativeLibs()) -} - -// Test files using standard library in stdlibs/. -func TestFiles(t *testing.T) { - baseDir := filepath.Join(".", "files") - runFileTests(t, baseDir, []string{"*_native*"}) -} - -func TestChallenges(t *testing.T) { - t.Skip("Challenge tests, skipping.") - baseDir := filepath.Join(".", "challenges") - runFileTests(t, baseDir, nil) -} - -type testFile struct { - path string - fs.DirEntry -} - -// ignore are glob patterns to ignore -func runFileTests(t *testing.T, baseDir string, ignore []string, opts ...RunFileTestOption) { - t.Helper() - - opts = append([]RunFileTestOption{WithSyncWanted(*withSync)}, opts...) - - files, err := readFiles(t, baseDir) - if err != nil { - t.Fatal(err) - } - - files = filterFileTests(t, files, ignore) - var path string - var name string - for _, file := range files { - path = file.path - name = strings.TrimPrefix(file.path, baseDir+string(os.PathSeparator)) - t.Run(name, func(t *testing.T) { - runFileTest(t, path, opts...) - }) - } -} - -// it reads all files recursively in the directory -func readFiles(t *testing.T, dir string) ([]testFile, error) { - t.Helper() - var files []testFile - - err := filepath.WalkDir(dir, func(path string, de fs.DirEntry, err error) error { - if err != nil { - return err - } - if de.IsDir() && de.Name() == "extern" { - return filepath.SkipDir - } - f := testFile{path: path, DirEntry: de} - - files = append(files, f) - return nil - }) - return files, err -} - -func filterFileTests(t *testing.T, files []testFile, ignore []string) []testFile { - t.Helper() - filtered := make([]testFile, 0, 1000) - var name string - - for _, f := range files { - // skip none .gno files - name = f.DirEntry.Name() - if filepath.Ext(name) != ".gno" { - continue - } - // skip ignored files - if isIgnored(t, name, ignore) { - continue - } - // skip _long file if we only want to test regular file. - if testing.Short() && strings.Contains(name, "_long") { - t.Logf("skipping test %s in short mode.", name) - continue - } - filtered = append(filtered, f) - } - return filtered -} - -func isIgnored(t *testing.T, name string, ignore []string) bool { - t.Helper() - isIgnore := false - for _, is := range ignore { - match, err := path.Match(is, name) - if err != nil { - t.Fatalf("error parsing glob pattern %q: %v", is, err) - } - if match { - isIgnore = true - break - } - } - return isIgnore -} - -func runFileTest(t *testing.T, path string, opts ...RunFileTestOption) { - t.Helper() - - opts = append([]RunFileTestOption{WithSyncWanted(*withSync)}, opts...) - - var logger loggerFunc - if gno.IsDebug() && testing.Verbose() { - logger = t.Log - } - rootDir := filepath.Join("..", "..") - err := RunFileTest(rootDir, path, append(opts, WithLoggerFunc(logger))...) - if err != nil { - t.Fatalf("got error: %v", err) - } -} diff --git a/gnovm/tests/files/access0_stdlibs.gno b/gnovm/tests/files/access0.gno similarity index 100% rename from gnovm/tests/files/access0_stdlibs.gno rename to gnovm/tests/files/access0.gno diff --git a/gnovm/tests/files/access1_stdlibs.gno b/gnovm/tests/files/access1.gno similarity index 52% rename from gnovm/tests/files/access1_stdlibs.gno rename to gnovm/tests/files/access1.gno index 5a1bf4cc12e..bcbfdb2829c 100644 --- a/gnovm/tests/files/access1_stdlibs.gno +++ b/gnovm/tests/files/access1.gno @@ -9,4 +9,4 @@ func main() { } // Error: -// main/files/access1_stdlibs.gno:8:10: cannot access gno.land/p/demo/testutils.testVar2 from main +// main/files/access1.gno:8:10: cannot access gno.land/p/demo/testutils.testVar2 from main diff --git a/gnovm/tests/files/access2_stdlibs.gno b/gnovm/tests/files/access2.gno similarity index 100% rename from gnovm/tests/files/access2_stdlibs.gno rename to gnovm/tests/files/access2.gno diff --git a/gnovm/tests/files/access3_stdlibs.gno b/gnovm/tests/files/access3.gno similarity index 100% rename from gnovm/tests/files/access3_stdlibs.gno rename to gnovm/tests/files/access3.gno diff --git a/gnovm/tests/files/access4_stdlibs.gno b/gnovm/tests/files/access4.gno similarity index 56% rename from gnovm/tests/files/access4_stdlibs.gno rename to gnovm/tests/files/access4.gno index e38a6d2ea4a..72c4f926ce4 100644 --- a/gnovm/tests/files/access4_stdlibs.gno +++ b/gnovm/tests/files/access4.gno @@ -10,4 +10,4 @@ func main() { } // Error: -// main/files/access4_stdlibs.gno:9:10: cannot access gno.land/p/demo/testutils.TestAccessStruct.privateField from main +// main/files/access4.gno:9:10: cannot access gno.land/p/demo/testutils.TestAccessStruct.privateField from main diff --git a/gnovm/tests/files/access5_stdlibs.gno b/gnovm/tests/files/access5.gno similarity index 100% rename from gnovm/tests/files/access5_stdlibs.gno rename to gnovm/tests/files/access5.gno diff --git a/gnovm/tests/files/access6_stdlibs.gno b/gnovm/tests/files/access6.gno similarity index 61% rename from gnovm/tests/files/access6_stdlibs.gno rename to gnovm/tests/files/access6.gno index 443f2f5291d..04778a8f5bb 100644 --- a/gnovm/tests/files/access6_stdlibs.gno +++ b/gnovm/tests/files/access6.gno @@ -16,4 +16,4 @@ func main() { } // Error: -// main/files/access6_stdlibs.gno:15:2: main.mystruct does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod) +// main/files/access6.gno:15:2: main.mystruct does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod) diff --git a/gnovm/tests/files/access7_stdlibs.gno b/gnovm/tests/files/access7.gno similarity index 67% rename from gnovm/tests/files/access7_stdlibs.gno rename to gnovm/tests/files/access7.gno index 01c9ed83fa0..3874ad98971 100644 --- a/gnovm/tests/files/access7_stdlibs.gno +++ b/gnovm/tests/files/access7.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/access7_stdlibs.gno:19:2: main.PrivateInterface2 does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod) +// main/files/access7.gno:19:2: main.PrivateInterface2 does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod) diff --git a/gnovm/tests/files/addr0b_stdlibs.gno b/gnovm/tests/files/addr0b.gno similarity index 100% rename from gnovm/tests/files/addr0b_stdlibs.gno rename to gnovm/tests/files/addr0b.gno diff --git a/gnovm/tests/files/addr0b_native.gno b/gnovm/tests/files/addr0b_native.gno deleted file mode 100644 index 86846500e42..00000000000 --- a/gnovm/tests/files/addr0b_native.gno +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/gnolang/gno/_test/net/http" -) - -type extendedRequest struct { - Request http.Request - - Data string -} - -func main() { - r := extendedRequest{} - req := &r.Request - - fmt.Println(r) - fmt.Println(req) -} - -// Output: -// {{ 0 0 map[] 0 [] false map[] map[] map[] } } -// &{ 0 0 map[] 0 [] false map[] map[] map[] } diff --git a/gnovm/tests/files/addr2b.gno b/gnovm/tests/files/addr2b.gno index 04342c00574..59a18904bea 100644 --- a/gnovm/tests/files/addr2b.gno +++ b/gnovm/tests/files/addr2b.gno @@ -1,24 +1,22 @@ package main import ( - "encoding/xml" + "encoding/json" "fmt" ) type Email struct { - Where string `xml:"where,attr"` + Where string Addr string } func f(s string, r interface{}) interface{} { - return xml.Unmarshal([]byte(s), &r) + return json.Unmarshal([]byte(s), &r) } func main() { data := ` - - bob@work.com - + {"Where": "work", "Addr": "bob@work.com"} ` v := Email{} err := f(data, &v) diff --git a/gnovm/tests/files/assign0b_stdlibs.gno b/gnovm/tests/files/assign0b.gno similarity index 100% rename from gnovm/tests/files/assign0b_stdlibs.gno rename to gnovm/tests/files/assign0b.gno diff --git a/gnovm/tests/files/assign0b_native.gno b/gnovm/tests/files/assign0b_native.gno deleted file mode 100644 index 42faa57634d..00000000000 --- a/gnovm/tests/files/assign0b_native.gno +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/gnolang/gno/_test/net/http" -) - -func main() { - http.DefaultClient.Timeout = time.Second * 10 - fmt.Println(http.DefaultClient) - http.DefaultClient = &http.Client{} - fmt.Println(http.DefaultClient) -} - -// Output: -// &{ 10s} -// &{ 0s} diff --git a/gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest_stdlibs.gno b/gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest.gno similarity index 100% rename from gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest_stdlibs.gno rename to gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest.gno diff --git a/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno b/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno index d61170334d7..45f83bade5f 100644 --- a/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno +++ b/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno @@ -210,7 +210,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "files/assign_unnamed_type/more/realm_compositelit.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -221,7 +221,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "files/assign_unnamed_type/more/realm_compositelit.gno", // "Line": "16", // "PkgPath": "gno.land/r/test" // } diff --git a/gnovm/tests/files/bin1.gno b/gnovm/tests/files/bin1.gno index 792651f60bf..e0e5a6d663a 100644 --- a/gnovm/tests/files/bin1.gno +++ b/gnovm/tests/files/bin1.gno @@ -1,16 +1,14 @@ package main import ( - "crypto/sha1" + "crypto/sha256" "fmt" ) func main() { - d := sha1.New() - d.Write([]byte("password")) - a := d.Sum(nil) + a := sha256.Sum256([]byte("password")) fmt.Println(a) } // Output: -// [91 170 97 228 201 185 63 63 6 130 37 11 108 248 51 27 126 230 143 216] +// [94 136 72 152 218 40 4 113 81 208 229 111 141 198 41 39 115 96 61 13 106 171 189 214 42 17 239 114 29 21 66 216] diff --git a/gnovm/tests/files/bin5.gno b/gnovm/tests/files/bin5.gno deleted file mode 100644 index d471d4e0fd2..00000000000 --- a/gnovm/tests/files/bin5.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "net" -) - -func main() { - addr := net.TCPAddr{IP: net.IPv4(1, 1, 1, 1), Port: 80} - var s fmt.Stringer = &addr - fmt.Println(s.String()) -} - -// Output: -// 1.1.1.1:80 diff --git a/gnovm/tests/files/binstruct_ptr_map0.gno b/gnovm/tests/files/binstruct_ptr_map0.gno index 329ece209e4..5eddca44f6e 100644 --- a/gnovm/tests/files/binstruct_ptr_map0.gno +++ b/gnovm/tests/files/binstruct_ptr_map0.gno @@ -2,11 +2,12 @@ package main import ( "fmt" - "image" ) +type Point struct{ X, Y int } + func main() { - v := map[string]*image.Point{ + v := map[string]*Point{ "foo": {X: 3, Y: 2}, "bar": {X: 4, Y: 5}, } @@ -14,4 +15,4 @@ func main() { } // Output: -// (3,2) (4,5) +// &{3 2} &{4 5} diff --git a/gnovm/tests/files/binstruct_ptr_slice0.gno b/gnovm/tests/files/binstruct_ptr_slice0.gno deleted file mode 100644 index 1ceea6cab70..00000000000 --- a/gnovm/tests/files/binstruct_ptr_slice0.gno +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "fmt" - "image" -) - -func main() { - v := []*image.Point{ - {X: 3, Y: 2}, - {X: 4, Y: 5}, - } - fmt.Println(v) -} - -// Output: -// [(3,2) (4,5)] diff --git a/gnovm/tests/files/binstruct_slice0.gno b/gnovm/tests/files/binstruct_slice0.gno index 211f60faf01..3cdc455a66f 100644 --- a/gnovm/tests/files/binstruct_slice0.gno +++ b/gnovm/tests/files/binstruct_slice0.gno @@ -2,15 +2,16 @@ package main import ( "fmt" - "image" ) +type Point struct{ X, Y int } + func main() { - v := []image.Point{ + v := []Point{ {X: 3, Y: 2}, } fmt.Println(v) } // Output: -// [(3,2)] +// [{3 2}] diff --git a/gnovm/tests/files/composite11.gno b/gnovm/tests/files/composite11.gno index 85f71018202..2d989022f25 100644 --- a/gnovm/tests/files/composite11.gno +++ b/gnovm/tests/files/composite11.gno @@ -2,11 +2,14 @@ package main import ( "fmt" - "image/color" ) +type NRGBA64 struct { + R, G, B, A uint16 +} + func main() { - c := color.NRGBA64{1, 1, 1, 1} + c := NRGBA64{1, 1, 1, 1} fmt.Println(c) } diff --git a/gnovm/tests/files/const14.gno b/gnovm/tests/files/const14.gno index 835858f712d..93d7975e20f 100644 --- a/gnovm/tests/files/const14.gno +++ b/gnovm/tests/files/const14.gno @@ -1,13 +1,13 @@ package main -import "compress/flate" +import "math" -func f1(i int) { println("i:", i) } +func f1(i float64) { println("i:", i) } func main() { - i := flate.BestSpeed + i := math.Pi f1(i) } // Output: -// i: 1 +// i: 3.141592653589793 diff --git a/gnovm/tests/files/const22.gno b/gnovm/tests/files/const22.gno index 42842066265..f92fcd4d910 100644 --- a/gnovm/tests/files/const22.gno +++ b/gnovm/tests/files/const22.gno @@ -31,7 +31,7 @@ func main() { fmt.Printf("%x", ha) fmt.Printf("%x", hb) - fmt.Printf("%x", ho) + fmt.Printf("%x\n", ho) } // Output: diff --git a/gnovm/tests/files/context.gno b/gnovm/tests/files/context.gno deleted file mode 100644 index 0dcd8d73a22..00000000000 --- a/gnovm/tests/files/context.gno +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import "context" - -func get(ctx context.Context, k string) string { - var r string - if v := ctx.Value(k); v != nil { - r = v.(string) - } - return r -} - -func main() { - ctx := context.WithValue(context.Background(), "hello", "world") - println(get(ctx, "hello")) -} - -// Output: -// world diff --git a/gnovm/tests/files/context2.gno b/gnovm/tests/files/context2.gno deleted file mode 100644 index 457fb03b735..00000000000 --- a/gnovm/tests/files/context2.gno +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import "context" - -func get(ctx context.Context, k string) string { - var r string - var ok bool - if v := ctx.Value(k); v != nil { - r, ok = v.(string) - println(ok) - } - return r -} - -func main() { - ctx := context.WithValue(context.Background(), "hello", "world") - println(get(ctx, "hello")) -} - -// Output: -// true -// world diff --git a/gnovm/tests/files/defer4.gno b/gnovm/tests/files/defer4.gno deleted file mode 100644 index e9baa5ac250..00000000000 --- a/gnovm/tests/files/defer4.gno +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import "sync" - -type T struct { - mu sync.RWMutex - name string -} - -func (t *T) get() string { - t.mu.RLock() - defer t.mu.RUnlock() - return t.name -} - -var d = T{name: "test"} - -func main() { - println(d.get()) -} - -// Output: -// test diff --git a/gnovm/tests/files/extern/p1/s1.gno b/gnovm/tests/files/extern/p1/s1.gno deleted file mode 100644 index ff2d89d4462..00000000000 --- a/gnovm/tests/files/extern/p1/s1.gno +++ /dev/null @@ -1,5 +0,0 @@ -package p1 - -import "crypto/rand" - -var Prime = rand.Prime diff --git a/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno b/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno index 2411bd2081f..13f94950dfa 100644 --- a/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno +++ b/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno @@ -2,7 +2,7 @@ package types import ( "bytes" - "hash/fnv" + "crypto/sha256" ) type ( @@ -36,9 +36,7 @@ func (self String) Less(other Sortable) bool { } func (self String) Hash() int { - h := fnv.New32a() - h.Write([]byte(string(self))) - return int(h.Sum32()) + return int(hash([]byte(self))) } func (self *ByteSlice) MarshalBinary() ([]byte, error) { @@ -67,7 +65,13 @@ func (self ByteSlice) Less(other Sortable) bool { } func (self ByteSlice) Hash() int { - h := fnv.New32a() - h.Write([]byte(self)) - return int(h.Sum32()) + return int(hash([]byte(self))) +} + +func hash(s []byte) int { + res := sha256.Sum256(s) + return int(s[0]) | + int(s[1]<<8) | + int(s[2]<<16) | + int(s[3]<<24) } diff --git a/gnovm/tests/files/float5_stdlibs.gno b/gnovm/tests/files/float5.gno similarity index 100% rename from gnovm/tests/files/float5_stdlibs.gno rename to gnovm/tests/files/float5.gno diff --git a/gnovm/tests/files/fun6.gno b/gnovm/tests/files/fun6.gno deleted file mode 100644 index c5ec644afd5..00000000000 --- a/gnovm/tests/files/fun6.gno +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -func NewPool() Pool { return Pool{} } - -type Pool struct { - P *sync.Pool -} - -var _pool = NewPool() - -func main() { - fmt.Println(_pool) -} - -// Output: -// {} diff --git a/gnovm/tests/files/fun6b.gno b/gnovm/tests/files/fun6b.gno deleted file mode 100644 index 17b0473b33b..00000000000 --- a/gnovm/tests/files/fun6b.gno +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "sync" -) - -func NewPool() Pool { return Pool{} } - -type Pool struct { - p *sync.Pool -} - -var _pool = NewPool() - -func main() { - println(_pool) -} - -// Output: -// (struct{(gonative{} gonative{*sync.Pool})} main.Pool) diff --git a/gnovm/tests/files/fun7.gno b/gnovm/tests/files/fun7.gno deleted file mode 100644 index ee8f813a527..00000000000 --- a/gnovm/tests/files/fun7.gno +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - goflag "flag" - "fmt" -) - -func Foo(goflag *goflag.Flag) { - fmt.Println(goflag) -} - -func main() { - g := &goflag.Flag{} - Foo(g) -} - -// Output: -// &{ } diff --git a/gnovm/tests/files/heap_alloc_forloop9_1.gno b/gnovm/tests/files/heap_alloc_forloop9_1.gno index 5e3b9af74f6..2576b9b4da6 100644 --- a/gnovm/tests/files/heap_alloc_forloop9_1.gno +++ b/gnovm/tests/files/heap_alloc_forloop9_1.gno @@ -19,7 +19,7 @@ func main() { // file{ package main; func Search(n (const-type int), f func(.arg_0 (const-type int)) (const-type bool)) (const-type int) { f((const (1 int))); return (const (0 int)) }; func main() { for x := (const (0 int)); x<~VPBlock(1,0)> < (const (2 int)); x<~VPBlock(1,0)>++ { count := (const (0 int)); (const (println func(xs ...interface{})()))((const (" first: count: " string)), count<~VPBlock(1,1)>); Search((const (1 int)), func func(i (const-type int)) (const-type bool){ count<~VPBlock(1,2)>++; return (const-type bool)(i >= x<~VPBlock(1,3)>) }, x<()~VPBlock(1,0)>>); (const (println func(xs ...interface{})()))((const ("second: count: " string)), count<~VPBlock(1,1)>) } } } // Output: -// first: count: 0 +// first: count: 0 // second: count: 1 // first: count: 0 // second: count: 1 diff --git a/gnovm/tests/files/heap_item_value.gno b/gnovm/tests/files/heap_item_value.gno index 40ec05d3ba1..80bf702bec2 100644 --- a/gnovm/tests/files/heap_item_value.gno +++ b/gnovm/tests/files/heap_item_value.gno @@ -151,7 +151,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "files/heap_item_value.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -162,7 +162,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "files/heap_item_value.gno", // "Line": "10", // "PkgPath": "gno.land/r/test" // } diff --git a/gnovm/tests/files/heap_item_value_init.gno b/gnovm/tests/files/heap_item_value_init.gno index 72f065326f1..2722cce8675 100644 --- a/gnovm/tests/files/heap_item_value_init.gno +++ b/gnovm/tests/files/heap_item_value_init.gno @@ -122,7 +122,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "files/heap_item_value_init.gno", // "IsMethod": false, // "Name": "init.3", // "NativeName": "", @@ -133,7 +133,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "files/heap_item_value_init.gno", // "Line": "10", // "PkgPath": "gno.land/r/test" // } @@ -158,7 +158,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "files/heap_item_value_init.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -169,7 +169,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "files/heap_item_value_init.gno", // "Line": "16", // "PkgPath": "gno.land/r/test" // } diff --git a/gnovm/tests/files/import3.gno b/gnovm/tests/files/import3.gno index c16ac626299..c63ed8a055c 100644 --- a/gnovm/tests/files/import3.gno +++ b/gnovm/tests/files/import3.gno @@ -4,7 +4,8 @@ import "github.com/gnolang/gno/_test/foo" func main() { println(foo.Bar, foo.Boo) } +// Init functions of dependencies are executed separatedly from the test itself, +// so they don't print with the test proper. + // Output: -// init boo -// init foo // BARR Boo diff --git a/gnovm/tests/files/import5.gno b/gnovm/tests/files/import5.gno index 609364d85b1..b270d0b0d3c 100644 --- a/gnovm/tests/files/import5.gno +++ b/gnovm/tests/files/import5.gno @@ -5,6 +5,4 @@ import boo "github.com/gnolang/gno/_test/foo" func main() { println(boo.Bar, boo.Boo, boo.Bir) } // Output: -// init boo -// init foo // BARR Boo Boo22 diff --git a/gnovm/tests/files/interp.gi b/gnovm/tests/files/interp.gi deleted file mode 100644 index ace895a356c..00000000000 --- a/gnovm/tests/files/interp.gi +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "github.com/gnolang/gno/interp" -) - -func main() { - i := interp.New(interp.Opt{}) - i.Eval(`println("Hello")`) -} - -// Output: -// Hello diff --git a/gnovm/tests/files/interp2.gi b/gnovm/tests/files/interp2.gi deleted file mode 100644 index af3ffd75f18..00000000000 --- a/gnovm/tests/files/interp2.gi +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "github.com/gnolang/gno/interp" -) - -func main() { - i := interp.New(interp.Opt{}) - i.Use(interp.ExportValue, interp.ExportType) - i.Eval(`import "github.com/gnolang/gno/interp"`) - i.Eval(`i := interp.New(interp.Opt{})`) - i.Eval(`i.Eval("println(42)")`) -} - -// Output: -// 42 diff --git a/gnovm/tests/files/io0_stdlibs.gno b/gnovm/tests/files/io0.gno similarity index 100% rename from gnovm/tests/files/io0_stdlibs.gno rename to gnovm/tests/files/io0.gno diff --git a/gnovm/tests/files/io0_native.gno b/gnovm/tests/files/io0_native.gno deleted file mode 100644 index 6486a9ba558..00000000000 --- a/gnovm/tests/files/io0_native.gno +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "crypto/rand" - "fmt" - "io" -) - -func main() { - var buf [16]byte - fmt.Println(buf) - io.ReadFull(rand.Reader, buf[:]) - fmt.Println(buf) -} - -// Output: -// [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] -// [100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115] diff --git a/gnovm/tests/files/io2.gno b/gnovm/tests/files/io2.gno index 24655f5040c..d0637c44e16 100644 --- a/gnovm/tests/files/io2.gno +++ b/gnovm/tests/files/io2.gno @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "log" "strings" ) @@ -12,9 +11,9 @@ func main() { b, err := io.ReadAll(r) if err != nil { - log.Fatal(err) + panic(err) } - fmt.Printf("%s", b) + fmt.Printf("%s\n", b) } // Output: diff --git a/gnovm/tests/files/issue_558b_stdlibs.gno b/gnovm/tests/files/issue_558b.gno similarity index 97% rename from gnovm/tests/files/issue_558b_stdlibs.gno rename to gnovm/tests/files/issue_558b.gno index 55eba88c985..51ddee56e0a 100644 --- a/gnovm/tests/files/issue_558b_stdlibs.gno +++ b/gnovm/tests/files/issue_558b.gno @@ -3,8 +3,6 @@ package main import ( "fmt" "io" - "io" - "log" "strings" ) @@ -68,7 +66,7 @@ func main() { p.Reader = newReadAutoCloser(strings.NewReader("test")) b, err := ReadAll(p.Reader) if err != nil { - log.Fatal(err) + panic(err) } fmt.Println(string(b)) } diff --git a/gnovm/tests/files/issue_782.gno b/gnovm/tests/files/issue_782.gno index 9d89a90bd30..4dc938ceaec 100644 --- a/gnovm/tests/files/issue_782.gno +++ b/gnovm/tests/files/issue_782.gno @@ -7,7 +7,7 @@ func main() { from := uint32(2) to := uint32(4) b := a[from:to] - fmt.Print(b) + fmt.Println(b) } // Output: diff --git a/gnovm/tests/files/l3_long.gno b/gnovm/tests/files/l3_long.gno deleted file mode 100644 index 64e75f522b2..00000000000 --- a/gnovm/tests/files/l3_long.gno +++ /dev/null @@ -1,163 +0,0 @@ -package main - -func main() { - for a := 0; a < 20000000; a++ { - if a&0x8ffff == 0x80000 { - println(a) - } - } -} - -// Output: -// 524288 -// 589824 -// 655360 -// 720896 -// 786432 -// 851968 -// 917504 -// 983040 -// 1572864 -// 1638400 -// 1703936 -// 1769472 -// 1835008 -// 1900544 -// 1966080 -// 2031616 -// 2621440 -// 2686976 -// 2752512 -// 2818048 -// 2883584 -// 2949120 -// 3014656 -// 3080192 -// 3670016 -// 3735552 -// 3801088 -// 3866624 -// 3932160 -// 3997696 -// 4063232 -// 4128768 -// 4718592 -// 4784128 -// 4849664 -// 4915200 -// 4980736 -// 5046272 -// 5111808 -// 5177344 -// 5767168 -// 5832704 -// 5898240 -// 5963776 -// 6029312 -// 6094848 -// 6160384 -// 6225920 -// 6815744 -// 6881280 -// 6946816 -// 7012352 -// 7077888 -// 7143424 -// 7208960 -// 7274496 -// 7864320 -// 7929856 -// 7995392 -// 8060928 -// 8126464 -// 8192000 -// 8257536 -// 8323072 -// 8912896 -// 8978432 -// 9043968 -// 9109504 -// 9175040 -// 9240576 -// 9306112 -// 9371648 -// 9961472 -// 10027008 -// 10092544 -// 10158080 -// 10223616 -// 10289152 -// 10354688 -// 10420224 -// 11010048 -// 11075584 -// 11141120 -// 11206656 -// 11272192 -// 11337728 -// 11403264 -// 11468800 -// 12058624 -// 12124160 -// 12189696 -// 12255232 -// 12320768 -// 12386304 -// 12451840 -// 12517376 -// 13107200 -// 13172736 -// 13238272 -// 13303808 -// 13369344 -// 13434880 -// 13500416 -// 13565952 -// 14155776 -// 14221312 -// 14286848 -// 14352384 -// 14417920 -// 14483456 -// 14548992 -// 14614528 -// 15204352 -// 15269888 -// 15335424 -// 15400960 -// 15466496 -// 15532032 -// 15597568 -// 15663104 -// 16252928 -// 16318464 -// 16384000 -// 16449536 -// 16515072 -// 16580608 -// 16646144 -// 16711680 -// 17301504 -// 17367040 -// 17432576 -// 17498112 -// 17563648 -// 17629184 -// 17694720 -// 17760256 -// 18350080 -// 18415616 -// 18481152 -// 18546688 -// 18612224 -// 18677760 -// 18743296 -// 18808832 -// 19398656 -// 19464192 -// 19529728 -// 19595264 -// 19660800 -// 19726336 -// 19791872 -// 19857408 diff --git a/gnovm/tests/files/l4_long.gno b/gnovm/tests/files/l4_long.gno deleted file mode 100644 index f91542999b3..00000000000 --- a/gnovm/tests/files/l4_long.gno +++ /dev/null @@ -1,7 +0,0 @@ -package main - -func main() { println(f(5)) } -func f(i int) int { return i + 1 } - -// Output: -// 6 diff --git a/gnovm/tests/files/l5_long.gno b/gnovm/tests/files/l5_long.gno deleted file mode 100644 index c72357d2e15..00000000000 --- a/gnovm/tests/files/l5_long.gno +++ /dev/null @@ -1,164 +0,0 @@ -package main - -func main() { - for a := 0; a < 20000000; { - if a&0x8ffff == 0x80000 { - println(a) - } - a = a + 1 - } -} - -// Output: -// 524288 -// 589824 -// 655360 -// 720896 -// 786432 -// 851968 -// 917504 -// 983040 -// 1572864 -// 1638400 -// 1703936 -// 1769472 -// 1835008 -// 1900544 -// 1966080 -// 2031616 -// 2621440 -// 2686976 -// 2752512 -// 2818048 -// 2883584 -// 2949120 -// 3014656 -// 3080192 -// 3670016 -// 3735552 -// 3801088 -// 3866624 -// 3932160 -// 3997696 -// 4063232 -// 4128768 -// 4718592 -// 4784128 -// 4849664 -// 4915200 -// 4980736 -// 5046272 -// 5111808 -// 5177344 -// 5767168 -// 5832704 -// 5898240 -// 5963776 -// 6029312 -// 6094848 -// 6160384 -// 6225920 -// 6815744 -// 6881280 -// 6946816 -// 7012352 -// 7077888 -// 7143424 -// 7208960 -// 7274496 -// 7864320 -// 7929856 -// 7995392 -// 8060928 -// 8126464 -// 8192000 -// 8257536 -// 8323072 -// 8912896 -// 8978432 -// 9043968 -// 9109504 -// 9175040 -// 9240576 -// 9306112 -// 9371648 -// 9961472 -// 10027008 -// 10092544 -// 10158080 -// 10223616 -// 10289152 -// 10354688 -// 10420224 -// 11010048 -// 11075584 -// 11141120 -// 11206656 -// 11272192 -// 11337728 -// 11403264 -// 11468800 -// 12058624 -// 12124160 -// 12189696 -// 12255232 -// 12320768 -// 12386304 -// 12451840 -// 12517376 -// 13107200 -// 13172736 -// 13238272 -// 13303808 -// 13369344 -// 13434880 -// 13500416 -// 13565952 -// 14155776 -// 14221312 -// 14286848 -// 14352384 -// 14417920 -// 14483456 -// 14548992 -// 14614528 -// 15204352 -// 15269888 -// 15335424 -// 15400960 -// 15466496 -// 15532032 -// 15597568 -// 15663104 -// 16252928 -// 16318464 -// 16384000 -// 16449536 -// 16515072 -// 16580608 -// 16646144 -// 16711680 -// 17301504 -// 17367040 -// 17432576 -// 17498112 -// 17563648 -// 17629184 -// 17694720 -// 17760256 -// 18350080 -// 18415616 -// 18481152 -// 18546688 -// 18612224 -// 18677760 -// 18743296 -// 18808832 -// 19398656 -// 19464192 -// 19529728 -// 19595264 -// 19660800 -// 19726336 -// 19791872 -// 19857408 diff --git a/gnovm/tests/files/l2_long.gno b/gnovm/tests/files/loop0.gno similarity index 100% rename from gnovm/tests/files/l2_long.gno rename to gnovm/tests/files/loop0.gno diff --git a/gnovm/tests/files/loop1.gno b/gnovm/tests/files/loop1.gno new file mode 100644 index 00000000000..4a61fbfba5b --- /dev/null +++ b/gnovm/tests/files/loop1.gno @@ -0,0 +1,51 @@ +package main + +func main() { + for a := 0; a < 20000; { + if (a & 0x8ff) == 0x800 { + println(a) + } + a = a + 1 + } +} + +// Output: +// 2048 +// 2304 +// 2560 +// 2816 +// 3072 +// 3328 +// 3584 +// 3840 +// 6144 +// 6400 +// 6656 +// 6912 +// 7168 +// 7424 +// 7680 +// 7936 +// 10240 +// 10496 +// 10752 +// 11008 +// 11264 +// 11520 +// 11776 +// 12032 +// 14336 +// 14592 +// 14848 +// 15104 +// 15360 +// 15616 +// 15872 +// 16128 +// 18432 +// 18688 +// 18944 +// 19200 +// 19456 +// 19712 +// 19968 diff --git a/gnovm/tests/files/map27.gno b/gnovm/tests/files/map27.gno index 5d76ffc21c7..578788d144e 100644 --- a/gnovm/tests/files/map27.gno +++ b/gnovm/tests/files/map27.gno @@ -2,7 +2,6 @@ package main import ( "fmt" - "text/template" ) type fm map[string]interface{} @@ -14,7 +13,7 @@ func main() { a["foo"] = &foo{} fmt.Println(a["foo"]) - b := make(template.FuncMap) // type FuncMap map[string]interface{} + b := make(map[string]interface{}) b["foo"] = &foo{} fmt.Println(b["foo"]) } diff --git a/gnovm/tests/files/map29_stdlibs.gno b/gnovm/tests/files/map29.gno similarity index 100% rename from gnovm/tests/files/map29_stdlibs.gno rename to gnovm/tests/files/map29.gno diff --git a/gnovm/tests/files/map29_native.gno b/gnovm/tests/files/map29_native.gno deleted file mode 100644 index b4a4129cd39..00000000000 --- a/gnovm/tests/files/map29_native.gno +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -type Item struct { - Object interface{} - Expiry time.Duration -} - -func main() { - items := map[string]Item{} - - items["test"] = Item{ - Object: "test", - Expiry: time.Second, - } - - item := items["test"] - fmt.Println(item) -} - -// Output: -// {test 1s} diff --git a/gnovm/tests/files/math0_stdlibs.gno b/gnovm/tests/files/math0.gno similarity index 100% rename from gnovm/tests/files/math0_stdlibs.gno rename to gnovm/tests/files/math0.gno diff --git a/gnovm/tests/files/math3.gno b/gnovm/tests/files/math3.gno index 592af0aa89d..a0ed2e1aa1e 100644 --- a/gnovm/tests/files/math3.gno +++ b/gnovm/tests/files/math3.gno @@ -1,32 +1,27 @@ package main import ( - "crypto/md5" + "crypto/sha256" "fmt" ) -func md5Crypt(password, salt, magic []byte) []byte { - d := md5.New() - d.Write(password) - d.Write(magic) - d.Write(salt) +func sha256Crypt(password, salt, magic string) []byte { + toHash := password + magic + salt + mixin := sha256.Sum256([]byte(password + salt)) - d2 := md5.New() - d2.Write(password) - d2.Write(salt) - - for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { - d.Write([]byte{mixin[i%16]}) + for i := 0; i < len(password); i++ { + toHash += string(mixin[i%32]) } - return ([]byte)(d.Sum(nil)) // gonative{[]byte} -> []byte + res := sha256.Sum256([]byte(toHash)) + return res[:] } func main() { - b := md5Crypt([]byte("1"), []byte("2"), []byte("3")) + b := sha256Crypt("1", "2", "3") fmt.Println(b) } // Output: -// [187 141 73 89 101 229 33 106 226 63 117 234 117 149 230 21] +// [172 65 148 29 23 72 77 86 46 80 184 188 192 158 154 11 145 11 197 253 206 210 141 253 188 27 157 126 89 142 179 143] diff --git a/gnovm/tests/files/math_native.gno b/gnovm/tests/files/math5.gno similarity index 100% rename from gnovm/tests/files/math_native.gno rename to gnovm/tests/files/math5.gno diff --git a/gnovm/tests/files/method16b.gno b/gnovm/tests/files/method16b.gno index 421a9f44e7b..4f36f48aa37 100644 --- a/gnovm/tests/files/method16b.gno +++ b/gnovm/tests/files/method16b.gno @@ -9,7 +9,7 @@ type Cheese struct { } func (t *Cheese) Hello(param string) { - fmt.Printf("%+v %+v", t, param) + fmt.Printf("%+v %+v\n", t, param) } func main() { diff --git a/gnovm/tests/files/method18.gno b/gnovm/tests/files/method18.gno deleted file mode 100644 index 3da9580dc02..00000000000 --- a/gnovm/tests/files/method18.gno +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "compress/gzip" - "fmt" - - "github.com/gnolang/gno/_test/net/http" -) - -type GzipResponseWriter struct { - http.ResponseWriter - index int - gw *gzip.Writer -} - -type GzipResponseWriterWithCloseNotify struct { - *GzipResponseWriter -} - -func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool { - return w.ResponseWriter.(http.CloseNotifier).CloseNotify() -} - -func main() { - fmt.Println("hello") -} - -// Output: -// hello diff --git a/gnovm/tests/files/method20.gno b/gnovm/tests/files/method20.gno index 7561451b699..7f3bce4c806 100644 --- a/gnovm/tests/files/method20.gno +++ b/gnovm/tests/files/method20.gno @@ -2,16 +2,11 @@ package main import ( "fmt" - "sync" ) -type Hello struct { - mu sync.Mutex -} +type Hello struct{} func (h *Hello) Hi() string { - h.mu.Lock() - h.mu.Unlock() return "hi" } diff --git a/gnovm/tests/files/method24.gno b/gnovm/tests/files/method24.gno deleted file mode 100644 index 624f4397b68..00000000000 --- a/gnovm/tests/files/method24.gno +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type Pool struct { - P *sync.Pool -} - -func (p Pool) Get() *Buffer { return &Buffer{} } - -func NewPool() Pool { return Pool{} } - -type Buffer struct { - Bs []byte - Pool Pool -} - -var ( - _pool = NewPool() - Get = _pool.Get -) - -func main() { - fmt.Println(_pool) - fmt.Println(Get()) -} - -// Output: -// {} -// &{[] {}} diff --git a/gnovm/tests/files/method25.gno b/gnovm/tests/files/method25.gno deleted file mode 100644 index a9dff18b6fb..00000000000 --- a/gnovm/tests/files/method25.gno +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -func (p Pool) Get() *Buffer { return &Buffer{} } - -func NewPool() Pool { return Pool{} } - -type Buffer struct { - Bs []byte - Pool Pool -} - -type Pool struct { - P *sync.Pool -} - -var ( - _pool = NewPool() - Get = _pool.Get -) - -func main() { - fmt.Println(_pool) - fmt.Println(Get()) -} - -// Output: -// {} -// &{[] {}} diff --git a/gnovm/tests/files/op0.gno b/gnovm/tests/files/op0.gno index 860f525a3bd..3c599928be6 100644 --- a/gnovm/tests/files/op0.gno +++ b/gnovm/tests/files/op0.gno @@ -7,7 +7,7 @@ func main() { a = 64 b = 64 c = a * b - fmt.Printf("c: %v %T", c, c) + fmt.Printf("c: %v %T\n", c, c) } // Output: diff --git a/gnovm/tests/files/print0.gno b/gnovm/tests/files/print0.gno index 43cdcf19d39..cab6a7943d1 100644 --- a/gnovm/tests/files/print0.gno +++ b/gnovm/tests/files/print0.gno @@ -2,6 +2,7 @@ package main func main() { print("hello") + println() } // Output: diff --git a/gnovm/tests/files/sample.plugin b/gnovm/tests/files/sample.plugin deleted file mode 100644 index cbe637b73af..00000000000 --- a/gnovm/tests/files/sample.plugin +++ /dev/null @@ -1,19 +0,0 @@ -package sample - -import ( - "fmt" - "net/http" -) - -type Sample struct{} - -func (s *Sample) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - r.Header.Set("X-sample-test", "Hello") - if next != nil { - next(w, r) - } -} - -func Test() { - fmt.Println("Hello from toto.Test()") -} diff --git a/gnovm/tests/files/secure.gi b/gnovm/tests/files/secure.gi deleted file mode 100644 index 3ac731a85ff..00000000000 --- a/gnovm/tests/files/secure.gi +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/unrolled/secure" // or "gopkg.in/unrolled/secure.v1" -) - -var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("hello world")) -}) - -func main() { - secureMiddleware := secure.New(secure.Options{ - AllowedHosts: []string{"example.com", "ssl.example.com"}, - HostsProxyHeaders: []string{"X-Forwarded-Host"}, - SSLRedirect: true, - SSLHost: "ssl.example.com", - SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, - STSSeconds: 315360000, - STSIncludeSubdomains: true, - STSPreload: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXssFilter: true, - ContentSecurityPolicy: "script-src $NONCE", - PublicKey: `pin-sha256="base64+primary=="; pin-sha256="base64+backup=="; max-age=5184000; includeSubdomains; report-uri="https://www.example.com/hpkp-report"`, - IsDevelopment: false, - }) - - app := secureMiddleware.Handler(myHandler) - http.ListenAndServe("127.0.0.1:3000", app) -} diff --git a/gnovm/tests/files/std0_stdlibs.gno b/gnovm/tests/files/std0.gno similarity index 100% rename from gnovm/tests/files/std0_stdlibs.gno rename to gnovm/tests/files/std0.gno diff --git a/gnovm/tests/files/std10_stdlibs.gno b/gnovm/tests/files/std10.gno similarity index 100% rename from gnovm/tests/files/std10_stdlibs.gno rename to gnovm/tests/files/std10.gno diff --git a/gnovm/tests/files/std11_stdlibs.gno b/gnovm/tests/files/std11.gno similarity index 100% rename from gnovm/tests/files/std11_stdlibs.gno rename to gnovm/tests/files/std11.gno diff --git a/gnovm/tests/files/std2_stdlibs.gno b/gnovm/tests/files/std2.gno similarity index 100% rename from gnovm/tests/files/std2_stdlibs.gno rename to gnovm/tests/files/std2.gno diff --git a/gnovm/tests/files/std3_stdlibs.gno b/gnovm/tests/files/std3.gno similarity index 100% rename from gnovm/tests/files/std3_stdlibs.gno rename to gnovm/tests/files/std3.gno diff --git a/gnovm/tests/files/std4_stdlibs.gno b/gnovm/tests/files/std4.gno similarity index 100% rename from gnovm/tests/files/std4_stdlibs.gno rename to gnovm/tests/files/std4.gno diff --git a/gnovm/tests/files/std5_stdlibs.gno b/gnovm/tests/files/std5.gno similarity index 90% rename from gnovm/tests/files/std5_stdlibs.gno rename to gnovm/tests/files/std5.gno index 4afa09da8d3..54cfb7846ab 100644 --- a/gnovm/tests/files/std5_stdlibs.gno +++ b/gnovm/tests/files/std5.gno @@ -18,7 +18,7 @@ func main() { // std.GetCallerAt(2) // std/native.gno:44 // main() -// main/files/std5_stdlibs.gno:10 +// main/files/std5.gno:10 // Error: // frame not found diff --git a/gnovm/tests/files/std6_stdlibs.gno b/gnovm/tests/files/std6.gno similarity index 100% rename from gnovm/tests/files/std6_stdlibs.gno rename to gnovm/tests/files/std6.gno diff --git a/gnovm/tests/files/std7_stdlibs.gno b/gnovm/tests/files/std7.gno similarity index 100% rename from gnovm/tests/files/std7_stdlibs.gno rename to gnovm/tests/files/std7.gno diff --git a/gnovm/tests/files/std8_stdlibs.gno b/gnovm/tests/files/std8.gno similarity index 89% rename from gnovm/tests/files/std8_stdlibs.gno rename to gnovm/tests/files/std8.gno index ab5e15bd618..27545f267ce 100644 --- a/gnovm/tests/files/std8_stdlibs.gno +++ b/gnovm/tests/files/std8.gno @@ -28,11 +28,11 @@ func main() { // std.GetCallerAt(4) // std/native.gno:44 // fn() -// main/files/std8_stdlibs.gno:16 +// main/files/std8.gno:16 // testutils.WrapCall(inner) // gno.land/p/demo/testutils/misc.gno:5 // main() -// main/files/std8_stdlibs.gno:21 +// main/files/std8.gno:21 // Error: // frame not found diff --git a/gnovm/tests/files/std9_stdlibs.gno b/gnovm/tests/files/std9.gno similarity index 100% rename from gnovm/tests/files/std9_stdlibs.gno rename to gnovm/tests/files/std9.gno diff --git a/gnovm/tests/files/stdbanker_stdlibs.gno b/gnovm/tests/files/stdbanker.gno similarity index 100% rename from gnovm/tests/files/stdbanker_stdlibs.gno rename to gnovm/tests/files/stdbanker.gno diff --git a/gnovm/tests/files/stdlibs_stdlibs.gno b/gnovm/tests/files/stdlibs.gno similarity index 100% rename from gnovm/tests/files/stdlibs_stdlibs.gno rename to gnovm/tests/files/stdlibs.gno diff --git a/gnovm/tests/files/struct13_stdlibs.gno b/gnovm/tests/files/struct13.gno similarity index 100% rename from gnovm/tests/files/struct13_stdlibs.gno rename to gnovm/tests/files/struct13.gno diff --git a/gnovm/tests/files/struct13_native.gno b/gnovm/tests/files/struct13_native.gno deleted file mode 100644 index 85515555f50..00000000000 --- a/gnovm/tests/files/struct13_native.gno +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/gnolang/gno/_test/net/http" -) - -type Fromage struct { - http.Server -} - -func main() { - a := Fromage{} - fmt.Println(a.Server.WriteTimeout) -} - -// Output: -// 0s diff --git a/gnovm/tests/files/switch21.gno b/gnovm/tests/files/switch21.gno index b13867d4512..5dd70e2a188 100644 --- a/gnovm/tests/files/switch21.gno +++ b/gnovm/tests/files/switch21.gno @@ -6,7 +6,7 @@ func main() { var err error switch v := err.(type) { - case fmt.Formatter: + case interface{ Format() string }: println("formatter") default: fmt.Println(v) diff --git a/gnovm/tests/files/time0_stdlibs.gno b/gnovm/tests/files/time0.gno similarity index 100% rename from gnovm/tests/files/time0_stdlibs.gno rename to gnovm/tests/files/time0.gno diff --git a/gnovm/tests/files/time0_native.gno b/gnovm/tests/files/time0_native.gno deleted file mode 100644 index 52c5b4d6727..00000000000 --- a/gnovm/tests/files/time0_native.gno +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - fmt.Println(time.Now()) -} - -// Output: -// 1970-01-01 00:00:00 +0000 UTC diff --git a/gnovm/tests/files/time1_stdlibs.gno b/gnovm/tests/files/time1.gno similarity index 100% rename from gnovm/tests/files/time1_stdlibs.gno rename to gnovm/tests/files/time1.gno diff --git a/gnovm/tests/files/time11_stdlibs.gno b/gnovm/tests/files/time11.gno similarity index 100% rename from gnovm/tests/files/time11_stdlibs.gno rename to gnovm/tests/files/time11.gno diff --git a/gnovm/tests/files/time11_native.gno b/gnovm/tests/files/time11_native.gno deleted file mode 100644 index 641ab4e6e4d..00000000000 --- a/gnovm/tests/files/time11_native.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -const df = time.Minute * 30 - -func main() { - fmt.Printf("df: %v %T\n", df, df) -} - -// Output: -// df: 30m0s time.Duration diff --git a/gnovm/tests/files/time12_stdlibs.gno b/gnovm/tests/files/time12.gno similarity index 100% rename from gnovm/tests/files/time12_stdlibs.gno rename to gnovm/tests/files/time12.gno diff --git a/gnovm/tests/files/time12_native.gno b/gnovm/tests/files/time12_native.gno deleted file mode 100644 index 890e49cd1f0..00000000000 --- a/gnovm/tests/files/time12_native.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -var twentyFourHours = time.Duration(24 * time.Hour) - -func main() { - fmt.Println(twentyFourHours.Hours()) -} - -// Output: -// 24 diff --git a/gnovm/tests/files/time13_stdlibs.gno b/gnovm/tests/files/time13.gno similarity index 100% rename from gnovm/tests/files/time13_stdlibs.gno rename to gnovm/tests/files/time13.gno diff --git a/gnovm/tests/files/time13_native.gno b/gnovm/tests/files/time13_native.gno deleted file mode 100644 index a2eedafe880..00000000000 --- a/gnovm/tests/files/time13_native.gno +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -var dummy = 1 - -var t time.Time = time.Date(2007, time.November, 10, 23, 4, 5, 0, time.UTC) - -func main() { - t = time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC) - fmt.Println(t.Clock()) -} - -// Output: -// 23 4 5 diff --git a/gnovm/tests/files/time14_stdlibs.gno b/gnovm/tests/files/time14.gno similarity index 100% rename from gnovm/tests/files/time14_stdlibs.gno rename to gnovm/tests/files/time14.gno diff --git a/gnovm/tests/files/time14_native.gno b/gnovm/tests/files/time14_native.gno deleted file mode 100644 index 9f28c57d006..00000000000 --- a/gnovm/tests/files/time14_native.gno +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -var t time.Time - -func f() time.Time { - time := t - return time -} - -func main() { - fmt.Println(f()) -} - -// Output: -// 0001-01-01 00:00:00 +0000 UTC diff --git a/gnovm/tests/files/time16_native.gno b/gnovm/tests/files/time16_native.gno deleted file mode 100644 index 4010667b41c..00000000000 --- a/gnovm/tests/files/time16_native.gno +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - var a int64 = 2 - fmt.Println(time.Second * a) -} - -// Error: -// main/files/time16_native.gno:10:14: incompatible operands in binary expression: go:time.Duration MUL int64 diff --git a/gnovm/tests/files/time17_native.gno b/gnovm/tests/files/time17_native.gno deleted file mode 100644 index 6733c1381cb..00000000000 --- a/gnovm/tests/files/time17_native.gno +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - now := time.Now() - now.In(nil) -} - -// Error: -// time: missing Location in call to Time.In - -// Stacktrace: -// now.In(gonative{*time.Location}) -// gofunction:func(*time.Location) time.Time -// main() -// main/files/time17_native.gno:10 diff --git a/gnovm/tests/files/time1_native.gno b/gnovm/tests/files/time1_native.gno deleted file mode 100644 index 9749d472e08..00000000000 --- a/gnovm/tests/files/time1_native.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - t := time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC) - m := t.Minute() - fmt.Println(t, m) -} - -// Output: -// 2009-11-10 23:04:05 +0000 UTC 4 diff --git a/gnovm/tests/files/time2_stdlibs.gno b/gnovm/tests/files/time2.gno similarity index 100% rename from gnovm/tests/files/time2_stdlibs.gno rename to gnovm/tests/files/time2.gno diff --git a/gnovm/tests/files/time2_native.gno b/gnovm/tests/files/time2_native.gno deleted file mode 100644 index 03ea3a2be96..00000000000 --- a/gnovm/tests/files/time2_native.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - t := time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC) - h, m, s := t.Clock() - fmt.Println(h, m, s) -} - -// Output: -// 23 4 5 diff --git a/gnovm/tests/files/time3_stdlibs.gno b/gnovm/tests/files/time3.gno similarity index 100% rename from gnovm/tests/files/time3_stdlibs.gno rename to gnovm/tests/files/time3.gno diff --git a/gnovm/tests/files/time3_native.gno b/gnovm/tests/files/time3_native.gno deleted file mode 100644 index 0848abd9a13..00000000000 --- a/gnovm/tests/files/time3_native.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -// FIXME related to named returns -func main() { - t := time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC) - fmt.Println(t.Clock()) -} - -// Output: -// 23 4 5 diff --git a/gnovm/tests/files/time4_stdlibs.gno b/gnovm/tests/files/time4.gno similarity index 100% rename from gnovm/tests/files/time4_stdlibs.gno rename to gnovm/tests/files/time4.gno diff --git a/gnovm/tests/files/time4_native.gno b/gnovm/tests/files/time4_native.gno deleted file mode 100644 index 3662e35cb01..00000000000 --- a/gnovm/tests/files/time4_native.gno +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - var m time.Month - m = 9 - fmt.Println(m) -} - -// Output: -// September diff --git a/gnovm/tests/files/time6_stdlibs.gno b/gnovm/tests/files/time6.gno similarity index 100% rename from gnovm/tests/files/time6_stdlibs.gno rename to gnovm/tests/files/time6.gno diff --git a/gnovm/tests/files/time6_native.gno b/gnovm/tests/files/time6_native.gno deleted file mode 100644 index c88d3ab8115..00000000000 --- a/gnovm/tests/files/time6_native.gno +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - t := &time.Time{} - t.UnmarshalText([]byte("1985-04-12T23:20:50.52Z")) - - fmt.Println(t) -} - -// Output: -// 1985-04-12 23:20:50.52 +0000 UTC diff --git a/gnovm/tests/files/time7_stdlibs.gno b/gnovm/tests/files/time7.gno similarity index 100% rename from gnovm/tests/files/time7_stdlibs.gno rename to gnovm/tests/files/time7.gno diff --git a/gnovm/tests/files/time7_native.gno b/gnovm/tests/files/time7_native.gno deleted file mode 100644 index 1e02defc80d..00000000000 --- a/gnovm/tests/files/time7_native.gno +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -var d = 2 * time.Second - -func main() { fmt.Println(d) } - -// Output: -// 2s diff --git a/gnovm/tests/files/time9_stdlibs.gno b/gnovm/tests/files/time9.gno similarity index 100% rename from gnovm/tests/files/time9_stdlibs.gno rename to gnovm/tests/files/time9.gno diff --git a/gnovm/tests/files/time9_native.gno b/gnovm/tests/files/time9_native.gno deleted file mode 100644 index a87b4560d1a..00000000000 --- a/gnovm/tests/files/time9_native.gno +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - fmt.Println((5 * time.Minute).Seconds()) -} - -// Output: -// 300 diff --git a/gnovm/tests/files/type11.gno b/gnovm/tests/files/type11.gno index a95be962a59..28aaee838dc 100644 --- a/gnovm/tests/files/type11.gno +++ b/gnovm/tests/files/type11.gno @@ -1,16 +1,17 @@ package main import ( - "compress/gzip" "fmt" - "sync" + "time" ) -var gzipWriterPools [gzip.BestCompression - gzip.BestSpeed + 2]*sync.Pool +const i1 = int(time.Nanosecond) + +var weirdArray [i1 + len("123456789")]time.Duration func main() { - fmt.Printf("%T\n", gzipWriterPools) + fmt.Printf("%T\n", weirdArray) } // Output: -// [10]*sync.Pool +// [10]int64 diff --git a/gnovm/tests/files/type2_stdlibs.gno b/gnovm/tests/files/type2.gno similarity index 100% rename from gnovm/tests/files/type2_stdlibs.gno rename to gnovm/tests/files/type2.gno diff --git a/gnovm/tests/files/type2_native.gno b/gnovm/tests/files/type2_native.gno deleted file mode 100644 index 453fd4b64e7..00000000000 --- a/gnovm/tests/files/type2_native.gno +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -type Options struct { - debug bool -} - -type T1 struct { - opt Options - time time.Time -} - -func main() { - t := T1{} - t.time = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - fmt.Println(t.time) -} - -// Output: -// 2009-11-10 23:00:00 +0000 UTC diff --git a/gnovm/tests/files/typeassert7_native.gno b/gnovm/tests/files/typeassert7.gno similarity index 100% rename from gnovm/tests/files/typeassert7_native.gno rename to gnovm/tests/files/typeassert7.gno diff --git a/gnovm/tests/files/typeassert7a_native.gno b/gnovm/tests/files/typeassert7a.gno similarity index 87% rename from gnovm/tests/files/typeassert7a_native.gno rename to gnovm/tests/files/typeassert7a.gno index cafb27b6a6b..6e0aa0e8dca 100644 --- a/gnovm/tests/files/typeassert7a_native.gno +++ b/gnovm/tests/files/typeassert7a.gno @@ -39,5 +39,5 @@ func main() { // Output: // ok -// interface conversion: interface is nil, not gonative{io.Reader} +// interface conversion: interface is nil, not io.Reader // ok diff --git a/gnovm/tests/files/types/add_assign_f0_stdlibs.gno b/gnovm/tests/files/types/add_assign_f0.gno similarity index 71% rename from gnovm/tests/files/types/add_assign_f0_stdlibs.gno rename to gnovm/tests/files/types/add_assign_f0.gno index 67c6777d085..a3df217aa7d 100644 --- a/gnovm/tests/files/types/add_assign_f0_stdlibs.gno +++ b/gnovm/tests/files/types/add_assign_f0.gno @@ -22,4 +22,4 @@ func main() { } // Error: -// main/files/types/add_assign_f0_stdlibs.gno:20:2: invalid operation: mismatched types int and .uverse.error +// main/files/types/add_assign_f0.gno:20:2: invalid operation: mismatched types int and .uverse.error diff --git a/gnovm/tests/files/types/add_assign_f1_stdlibs.gno b/gnovm/tests/files/types/add_assign_f1.gno similarity index 81% rename from gnovm/tests/files/types/add_assign_f1_stdlibs.gno rename to gnovm/tests/files/types/add_assign_f1.gno index d83a66359c9..195d8ab1c1c 100644 --- a/gnovm/tests/files/types/add_assign_f1_stdlibs.gno +++ b/gnovm/tests/files/types/add_assign_f1.gno @@ -25,4 +25,4 @@ func main() { } // Error: -// main/files/types/add_assign_f1_stdlibs.gno:21:2: invalid operation: mismatched types main.Error and .uverse.error +// main/files/types/add_assign_f1.gno:21:2: invalid operation: mismatched types main.Error and .uverse.error diff --git a/gnovm/tests/files/types/add_assign_f2_stdlibs.gno b/gnovm/tests/files/types/add_assign_f2.gno similarity index 75% rename from gnovm/tests/files/types/add_assign_f2_stdlibs.gno rename to gnovm/tests/files/types/add_assign_f2.gno index 8be6b3cfb7b..2603ee273d6 100644 --- a/gnovm/tests/files/types/add_assign_f2_stdlibs.gno +++ b/gnovm/tests/files/types/add_assign_f2.gno @@ -22,4 +22,4 @@ func main() { } // Error: -// main/files/types/add_assign_f2_stdlibs.gno:20:2: operator += not defined on: InterfaceKind +// main/files/types/add_assign_f2.gno:20:2: operator += not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/add_f0_stdlibs.gno b/gnovm/tests/files/types/add_f0.gno similarity index 74% rename from gnovm/tests/files/types/add_f0_stdlibs.gno rename to gnovm/tests/files/types/add_f0.gno index 33e0346d44f..4497efd41f1 100644 --- a/gnovm/tests/files/types/add_f0_stdlibs.gno +++ b/gnovm/tests/files/types/add_f0.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/types/add_f0_stdlibs.gno:19:10: operator + not defined on: InterfaceKind +// main/files/types/add_f0.gno:19:10: operator + not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/add_f1_stdlibs.gno b/gnovm/tests/files/types/add_f1.gno similarity index 75% rename from gnovm/tests/files/types/add_f1_stdlibs.gno rename to gnovm/tests/files/types/add_f1.gno index e46d67e93d7..0c403aff6a4 100644 --- a/gnovm/tests/files/types/add_f1_stdlibs.gno +++ b/gnovm/tests/files/types/add_f1.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/types/add_f1_stdlibs.gno:19:10: operator + not defined on: InterfaceKind +// main/files/types/add_f1.gno:19:10: operator + not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/and_f0_stdlibs.gno b/gnovm/tests/files/types/and_f0.gno similarity index 74% rename from gnovm/tests/files/types/and_f0_stdlibs.gno rename to gnovm/tests/files/types/and_f0.gno index e80f69332a8..2c82610b932 100644 --- a/gnovm/tests/files/types/and_f0_stdlibs.gno +++ b/gnovm/tests/files/types/and_f0.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/types/and_f0_stdlibs.gno:19:10: operator & not defined on: InterfaceKind +// main/files/types/and_f0.gno:19:10: operator & not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/and_f1_stdlibs.gno b/gnovm/tests/files/types/and_f1.gno similarity index 75% rename from gnovm/tests/files/types/and_f1_stdlibs.gno rename to gnovm/tests/files/types/and_f1.gno index 42a6aa4b466..41a72899ee2 100644 --- a/gnovm/tests/files/types/and_f1_stdlibs.gno +++ b/gnovm/tests/files/types/and_f1.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/types/and_f1_stdlibs.gno:19:10: operator & not defined on: InterfaceKind +// main/files/types/and_f1.gno:19:10: operator & not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/cmp_iface_0_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_0.gno similarity index 100% rename from gnovm/tests/files/types/cmp_iface_0_stdlibs.gno rename to gnovm/tests/files/types/cmp_iface_0.gno diff --git a/gnovm/tests/files/types/cmp_iface_3_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_3.gno similarity index 100% rename from gnovm/tests/files/types/cmp_iface_3_stdlibs.gno rename to gnovm/tests/files/types/cmp_iface_3.gno diff --git a/gnovm/tests/files/types/eql_0f8_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_5.gno similarity index 75% rename from gnovm/tests/files/types/eql_0f8_stdlibs.gno rename to gnovm/tests/files/types/cmp_iface_5.gno index a6e24110432..7d748bacef3 100644 --- a/gnovm/tests/files/types/eql_0f8_stdlibs.gno +++ b/gnovm/tests/files/types/cmp_iface_5.gno @@ -24,4 +24,4 @@ func main() { } // Error: -// main/files/types/eql_0f8_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error) +// main/files/types/cmp_iface_5.gno:19:5: int64 does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/eql_0b4_native.gno b/gnovm/tests/files/types/eql_0b4.gno similarity index 50% rename from gnovm/tests/files/types/eql_0b4_native.gno rename to gnovm/tests/files/types/eql_0b4.gno index 7c7baf01924..14a719c41d0 100644 --- a/gnovm/tests/files/types/eql_0b4_native.gno +++ b/gnovm/tests/files/types/eql_0b4.gno @@ -10,4 +10,4 @@ func main() { } // Error: -// main/files/types/eql_0b4_native.gno:9:10: unexpected type pair: cannot use bigint as gonative{error} +// main/files/types/eql_0b4.gno:9:10: bigint does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/eql_0b4_stdlibs.gno b/gnovm/tests/files/types/eql_0b4_stdlibs.gno deleted file mode 100644 index eac923c6d31..00000000000 --- a/gnovm/tests/files/types/eql_0b4_stdlibs.gno +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "errors" -) - -func main() { - errCmp := errors.New("xxx") - println(5 == errCmp) -} - -// Error: -// main/files/types/eql_0b4_stdlibs.gno:9:10: bigint does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/eql_0f0_native.gno b/gnovm/tests/files/types/eql_0f0.gno similarity index 75% rename from gnovm/tests/files/types/eql_0f0_native.gno rename to gnovm/tests/files/types/eql_0f0.gno index e32325f5cf6..f609c0b5ced 100644 --- a/gnovm/tests/files/types/eql_0f0_native.gno +++ b/gnovm/tests/files/types/eql_0f0.gno @@ -25,4 +25,4 @@ func main() { } // Error: -// main/files/types/eql_0f0_native.gno:19:5: unexpected type pair: cannot use bigint as gonative{error} +// main/files/types/eql_0f0.gno:19:5: bigint does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/eql_0f0_stdlibs.gno b/gnovm/tests/files/types/eql_0f0_stdlibs.gno deleted file mode 100644 index 4947627cba4..00000000000 --- a/gnovm/tests/files/types/eql_0f0_stdlibs.gno +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "errors" - "strconv" -) - -type Error int64 - -func (e Error) Error() string { - return "error: " + strconv.Itoa(int(e)) -} - -var errCmp = errors.New("XXXX") - -// special case: -// one is interface -func main() { - if 1 == errCmp { - //if errCmp == 1 { - println("what the firetruck?") - } else { - println("something else") - } -} - -// Error: -// main/files/types/eql_0f0_stdlibs.gno:19:5: bigint does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/eql_0f1_stdlibs.gno b/gnovm/tests/files/types/eql_0f1.gno similarity index 76% rename from gnovm/tests/files/types/eql_0f1_stdlibs.gno rename to gnovm/tests/files/types/eql_0f1.gno index cab7fcfab33..fd40dfcd29b 100644 --- a/gnovm/tests/files/types/eql_0f1_stdlibs.gno +++ b/gnovm/tests/files/types/eql_0f1.gno @@ -25,4 +25,4 @@ func main() { } // Error: -// main/files/types/eql_0f1_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error) +// main/files/types/eql_0f1.gno:19:5: int64 does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/eql_0f27_stdlibs.gno b/gnovm/tests/files/types/eql_0f27.gno similarity index 75% rename from gnovm/tests/files/types/eql_0f27_stdlibs.gno rename to gnovm/tests/files/types/eql_0f27.gno index 188153aeb51..e90bbab9ca5 100644 --- a/gnovm/tests/files/types/eql_0f27_stdlibs.gno +++ b/gnovm/tests/files/types/eql_0f27.gno @@ -18,4 +18,4 @@ func main() { } // Error: -// main/files/types/eql_0f27_stdlibs.gno:13:5: operator > not defined on: InterfaceKind +// main/files/types/eql_0f27.gno:13:5: operator > not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/eql_0f2b_native.gno b/gnovm/tests/files/types/eql_0f2b.gno similarity index 80% rename from gnovm/tests/files/types/eql_0f2b_native.gno rename to gnovm/tests/files/types/eql_0f2b.gno index 9de6155c5be..14a94dfde9a 100644 --- a/gnovm/tests/files/types/eql_0f2b_native.gno +++ b/gnovm/tests/files/types/eql_0f2b.gno @@ -25,4 +25,4 @@ func main() { } // Error: -// main/files/types/eql_0f2b_native.gno:19:5: operator <= not defined on: InterfaceKind +// main/files/types/eql_0f2b.gno:19:5: operator <= not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/eql_0f2b_stdlibs.gno b/gnovm/tests/files/types/eql_0f2b_stdlibs.gno deleted file mode 100644 index ac3616d163d..00000000000 --- a/gnovm/tests/files/types/eql_0f2b_stdlibs.gno +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "errors" - "strconv" -) - -type Error int64 - -func (e Error) Error() string { - return "error: " + strconv.Itoa(int(e)) -} - -var errCmp = errors.New("XXXX") - -// special case: -// one is interface -func main() { - if Error(0) <= errCmp { - //if errCmp == 1 { - println("what the firetruck?") - } else { - println("something else") - } -} - -// Error: -// main/files/types/eql_0f2b_stdlibs.gno:19:5: operator <= not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/eql_0f2c_native.gno b/gnovm/tests/files/types/eql_0f2c.gno similarity index 80% rename from gnovm/tests/files/types/eql_0f2c_native.gno rename to gnovm/tests/files/types/eql_0f2c.gno index edd5ac3f23a..3374357a145 100644 --- a/gnovm/tests/files/types/eql_0f2c_native.gno +++ b/gnovm/tests/files/types/eql_0f2c.gno @@ -25,4 +25,4 @@ func main() { } // Error: -// main/files/types/eql_0f2c_native.gno:19:5: operator < not defined on: InterfaceKind +// main/files/types/eql_0f2c.gno:19:5: operator < not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/eql_0f2c_stdlibs.gno b/gnovm/tests/files/types/eql_0f2c_stdlibs.gno deleted file mode 100644 index 3a6ac3395b6..00000000000 --- a/gnovm/tests/files/types/eql_0f2c_stdlibs.gno +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "errors" - "strconv" -) - -type Error int64 - -func (e Error) Error() string { - return "error: " + strconv.Itoa(int(e)) -} - -var errCmp = errors.New("XXXX") - -// special case: -// one is interface -func main() { - if Error(0) < errCmp { - //if errCmp == 1 { - println("what the firetruck?") - } else { - println("something else") - } -} - -// Error: -// main/files/types/eql_0f2c_stdlibs.gno:19:5: operator < not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/eql_0f40_stdlibs.gno b/gnovm/tests/files/types/eql_0f40.gno similarity index 100% rename from gnovm/tests/files/types/eql_0f40_stdlibs.gno rename to gnovm/tests/files/types/eql_0f40.gno diff --git a/gnovm/tests/files/types/eql_0f41_stdlibs.gno b/gnovm/tests/files/types/eql_0f41.gno similarity index 77% rename from gnovm/tests/files/types/eql_0f41_stdlibs.gno rename to gnovm/tests/files/types/eql_0f41.gno index be78ea6ed79..162586e2977 100644 --- a/gnovm/tests/files/types/eql_0f41_stdlibs.gno +++ b/gnovm/tests/files/types/eql_0f41.gno @@ -32,4 +32,4 @@ func main() { } // Error: -// main/files/types/eql_0f41_stdlibs.gno:27:5: main.animal does not implement .uverse.error (missing method Error) +// main/files/types/eql_0f41.gno:27:5: main.animal does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno b/gnovm/tests/files/types/eql_0f8.gno similarity index 75% rename from gnovm/tests/files/types/cmp_iface_5_stdlibs.gno rename to gnovm/tests/files/types/eql_0f8.gno index e706c74808e..17bb5f9002d 100644 --- a/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno +++ b/gnovm/tests/files/types/eql_0f8.gno @@ -24,4 +24,4 @@ func main() { } // Error: -// main/files/types/cmp_iface_5_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error) +// main/files/types/eql_0f8.gno:19:5: int64 does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/explicit_conversion_0.gno b/gnovm/tests/files/types/explicit_conversion_0.gno index ac5e8c2eb94..be7800590e5 100644 --- a/gnovm/tests/files/types/explicit_conversion_0.gno +++ b/gnovm/tests/files/types/explicit_conversion_0.gno @@ -5,7 +5,7 @@ import "fmt" func main() { r := int(uint(1)) println(r) - fmt.Printf("%T \n", r) + fmt.Printf("%T\n", r) } // Output: diff --git a/gnovm/tests/files/types/explicit_conversion_1.gno b/gnovm/tests/files/types/explicit_conversion_1.gno index 60fc7b95b64..472a430fdc5 100644 --- a/gnovm/tests/files/types/explicit_conversion_1.gno +++ b/gnovm/tests/files/types/explicit_conversion_1.gno @@ -6,7 +6,7 @@ import "fmt" func main() { r := int(uint(string("hello"))) println(r) - fmt.Printf("%T \n", r) + fmt.Printf("%T\n", r) } // Error: diff --git a/gnovm/tests/files/types/explicit_conversion_2.gno b/gnovm/tests/files/types/explicit_conversion_2.gno index f932a970a9a..30da1d31154 100644 --- a/gnovm/tests/files/types/explicit_conversion_2.gno +++ b/gnovm/tests/files/types/explicit_conversion_2.gno @@ -6,7 +6,7 @@ func main() { x := 1 r := uint(+x) println(r) - fmt.Printf("%T \n", r) + fmt.Printf("%T\n", r) } // Output: diff --git a/gnovm/tests/files/types/or_f0_stdlibs.gno b/gnovm/tests/files/types/or_f0.gno similarity index 75% rename from gnovm/tests/files/types/or_f0_stdlibs.gno rename to gnovm/tests/files/types/or_f0.gno index 8e6fb54772a..34ffdaa87fe 100644 --- a/gnovm/tests/files/types/or_f0_stdlibs.gno +++ b/gnovm/tests/files/types/or_f0.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/types/or_f0_stdlibs.gno:19:10: operator | not defined on: InterfaceKind +// main/files/types/or_f0.gno:19:10: operator | not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/or_f1_stdlibs.gno b/gnovm/tests/files/types/or_f1.gno similarity index 75% rename from gnovm/tests/files/types/or_f1_stdlibs.gno rename to gnovm/tests/files/types/or_f1.gno index 5013126c9fa..96a68632320 100644 --- a/gnovm/tests/files/types/or_f1_stdlibs.gno +++ b/gnovm/tests/files/types/or_f1.gno @@ -20,4 +20,4 @@ func main() { } // Error: -// main/files/types/or_f1_stdlibs.gno:19:10: operator | not defined on: InterfaceKind +// main/files/types/or_f1.gno:19:10: operator | not defined on: InterfaceKind diff --git a/gnovm/tests/files/types/shift_b0.gno b/gnovm/tests/files/types/shift_b0.gno index fa9ee4ed2a0..9717f04a56d 100644 --- a/gnovm/tests/files/types/shift_b0.gno +++ b/gnovm/tests/files/types/shift_b0.gno @@ -6,7 +6,7 @@ func main() { x := 2 r := uint64(1 << x) println(r) - fmt.Printf("%T \n", r) + fmt.Printf("%T\n", r) } // Output: diff --git a/gnovm/tests/files/types/shift_b1.gno b/gnovm/tests/files/types/shift_b1.gno index 403887269c0..8f2615ba93d 100644 --- a/gnovm/tests/files/types/shift_b1.gno +++ b/gnovm/tests/files/types/shift_b1.gno @@ -6,7 +6,7 @@ func main() { x := 2 r := uint64(1<.Panic() -// gno.land/r/test/main.gno:7 +// gno.land/r/test/files/zrealm_panic.gno:7 // main() -// gno.land/r/test/main.gno:12 +// gno.land/r/test/files/zrealm_panic.gno:12 diff --git a/gnovm/tests/files/zrealm_std0_stdlibs.gno b/gnovm/tests/files/zrealm_std0.gno similarity index 100% rename from gnovm/tests/files/zrealm_std0_stdlibs.gno rename to gnovm/tests/files/zrealm_std0.gno diff --git a/gnovm/tests/files/zrealm_std1_stdlibs.gno b/gnovm/tests/files/zrealm_std1.gno similarity index 100% rename from gnovm/tests/files/zrealm_std1_stdlibs.gno rename to gnovm/tests/files/zrealm_std1.gno diff --git a/gnovm/tests/files/zrealm_std2_stdlibs.gno b/gnovm/tests/files/zrealm_std2.gno similarity index 100% rename from gnovm/tests/files/zrealm_std2_stdlibs.gno rename to gnovm/tests/files/zrealm_std2.gno diff --git a/gnovm/tests/files/zrealm_std3_stdlibs.gno b/gnovm/tests/files/zrealm_std3.gno similarity index 100% rename from gnovm/tests/files/zrealm_std3_stdlibs.gno rename to gnovm/tests/files/zrealm_std3.gno diff --git a/gnovm/tests/files/zrealm_std4_stdlibs.gno b/gnovm/tests/files/zrealm_std4.gno similarity index 100% rename from gnovm/tests/files/zrealm_std4_stdlibs.gno rename to gnovm/tests/files/zrealm_std4.gno diff --git a/gnovm/tests/files/zrealm_std5_stdlibs.gno b/gnovm/tests/files/zrealm_std5.gno similarity index 100% rename from gnovm/tests/files/zrealm_std5_stdlibs.gno rename to gnovm/tests/files/zrealm_std5.gno diff --git a/gnovm/tests/files/zrealm_std6_stdlibs.gno b/gnovm/tests/files/zrealm_std6.gno similarity index 100% rename from gnovm/tests/files/zrealm_std6_stdlibs.gno rename to gnovm/tests/files/zrealm_std6.gno diff --git a/gnovm/tests/files/zrealm_tests0_stdlibs.gno b/gnovm/tests/files/zrealm_tests0.gno similarity index 99% rename from gnovm/tests/files/zrealm_tests0_stdlibs.gno rename to gnovm/tests/files/zrealm_tests0.gno index d11701505e5..82e4d418217 100644 --- a/gnovm/tests/files/zrealm_tests0_stdlibs.gno +++ b/gnovm/tests/files/zrealm_tests0.gno @@ -14,12 +14,15 @@ func init() { func main() { tests_foo.AddFooStringer("three") println(tests.Render("")) + println("end") } // Output: // 0: &FooStringer{one} // 1: &FooStringer{two} // 2: &FooStringer{three} +// +// end // Realm: // switchrealm["gno.land/r/demo/tests"] diff --git a/gnovm/tests/files/zrealm_testutils0_stdlibs.gno b/gnovm/tests/files/zrealm_testutils0.gno similarity index 100% rename from gnovm/tests/files/zrealm_testutils0_stdlibs.gno rename to gnovm/tests/files/zrealm_testutils0.gno diff --git a/gnovm/tests/files/zregexp_stdlibs.gno b/gnovm/tests/files/zregexp.gno similarity index 100% rename from gnovm/tests/files/zregexp_stdlibs.gno rename to gnovm/tests/files/zregexp.gno diff --git a/gnovm/tests/imports.go b/gnovm/tests/imports.go deleted file mode 100644 index 66398ba5f50..00000000000 --- a/gnovm/tests/imports.go +++ /dev/null @@ -1,492 +0,0 @@ -package tests - -import ( - "bufio" - "bytes" - "compress/flate" - "compress/gzip" - "context" - "crypto/md5" //nolint:gosec - crand "crypto/rand" - "crypto/sha1" //nolint:gosec - "encoding/base64" - "encoding/binary" - "encoding/json" - "encoding/xml" - "errors" - "flag" - "fmt" - "hash/fnv" - "image" - "image/color" - "io" - "log" - "math" - "math/big" - "math/rand/v2" - "net" - "net/url" - "os" - "path/filepath" - "reflect" - "strconv" - "strings" - "sync" - "sync/atomic" - "text/template" - "time" - "unicode/utf8" - - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - teststdlibs "github.com/gnolang/gno/gnovm/tests/stdlibs" - teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" - "github.com/gnolang/gno/tm2/pkg/db/memdb" - osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/store/dbadapter" - "github.com/gnolang/gno/tm2/pkg/store/iavl" - stypes "github.com/gnolang/gno/tm2/pkg/store/types" -) - -type importMode uint64 - -// Import modes to control the import behaviour of TestStore. -const ( - // use stdlibs/* only (except a few exceptions). for stdlibs/* and examples/* testing. - ImportModeStdlibsOnly importMode = iota - // use stdlibs/* if present, otherwise use native. used in files/tests, excluded for *_native.go - ImportModeStdlibsPreferred - // do not use stdlibs/* if native registered. used in files/tests, excluded for *_stdlibs.go - ImportModeNativePreferred -) - -// NOTE: this isn't safe, should only be used for testing. -func TestStore(rootDir, filesPath string, stdin io.Reader, stdout, stderr io.Writer, mode importMode) (resStore gno.Store) { - getPackage := func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { - if pkgPath == "" { - panic(fmt.Sprintf("invalid zero package path in testStore().pkgGetter")) - } - if mode != ImportModeStdlibsOnly && - mode != ImportModeStdlibsPreferred && - mode != ImportModeNativePreferred { - panic(fmt.Sprintf("unrecognized import mode")) - } - - if filesPath != "" { - // if _test package... - const testPath = "github.com/gnolang/gno/_test/" - if strings.HasPrefix(pkgPath, testPath) { - baseDir := filepath.Join(filesPath, "extern", pkgPath[len(testPath):]) - memPkg := gno.ReadMemPackage(baseDir, pkgPath) - send := std.Coins{} - ctx := TestContext(pkgPath, send) - m2 := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "test", - Output: stdout, - Store: store, - Context: ctx, - }) - // pkg := gno.NewPackageNode(gno.Name(memPkg.Name), memPkg.Path, nil) - // pv := pkg.NewPackage() - // m2.SetActivePackage(pv) - // XXX remove second arg 'false' and remove all gonative stuff. - return m2.RunMemPackage(memPkg, false) - } - } - - // if stdlibs package is preferred , try to load it first. - if mode == ImportModeStdlibsOnly || - mode == ImportModeStdlibsPreferred { - pn, pv = loadStdlib(rootDir, pkgPath, store, stdout) - if pn != nil { - return - } - } - - // if native package is allowed, return it. - if pkgPath == "os" || // special cases even when StdlibsOnly (for tests). - pkgPath == "fmt" || // TODO: try to minimize these exceptions over time. - pkgPath == "log" || - pkgPath == "crypto/rand" || - pkgPath == "crypto/md5" || - pkgPath == "crypto/sha1" || - pkgPath == "encoding/binary" || - pkgPath == "encoding/json" || - pkgPath == "encoding/xml" || - pkgPath == "internal/os_test" || - pkgPath == "math/big" || - mode == ImportModeStdlibsPreferred || - mode == ImportModeNativePreferred { - switch pkgPath { - case "os": - pkg := gno.NewPackageNode("os", pkgPath, nil) - pkg.DefineGoNativeValue("Stdin", stdin) - pkg.DefineGoNativeValue("Stdout", stdout) - pkg.DefineGoNativeValue("Stderr", stderr) - return pkg, pkg.NewPackage() - case "fmt": - pkg := gno.NewPackageNode("fmt", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf((*fmt.Stringer)(nil)).Elem()) - pkg.DefineGoNativeType(reflect.TypeOf((*fmt.Formatter)(nil)).Elem()) - pkg.DefineGoNativeValue("Println", func(a ...interface{}) (n int, err error) { - // NOTE: uncomment to debug long running tests - // fmt.Println(a...) - res := fmt.Sprintln(a...) - return stdout.Write([]byte(res)) - }) - pkg.DefineGoNativeValue("Printf", func(format string, a ...interface{}) (n int, err error) { - res := fmt.Sprintf(format, a...) - return stdout.Write([]byte(res)) - }) - pkg.DefineGoNativeValue("Print", func(a ...interface{}) (n int, err error) { - res := fmt.Sprint(a...) - return stdout.Write([]byte(res)) - }) - pkg.DefineGoNativeValue("Sprint", fmt.Sprint) - pkg.DefineGoNativeValue("Sprintf", fmt.Sprintf) - pkg.DefineGoNativeValue("Sprintln", fmt.Sprintln) - pkg.DefineGoNativeValue("Sscanf", fmt.Sscanf) - pkg.DefineGoNativeValue("Errorf", fmt.Errorf) - pkg.DefineGoNativeValue("Fprintln", fmt.Fprintln) - pkg.DefineGoNativeValue("Fprintf", fmt.Fprintf) - pkg.DefineGoNativeValue("Fprint", fmt.Fprint) - return pkg, pkg.NewPackage() - case "encoding/base64": - pkg := gno.NewPackageNode("base64", pkgPath, nil) - pkg.DefineGoNativeValue("RawStdEncoding", base64.RawStdEncoding) - pkg.DefineGoNativeValue("StdEncoding", base64.StdEncoding) - pkg.DefineGoNativeValue("NewDecoder", base64.NewDecoder) - return pkg, pkg.NewPackage() - case "encoding/binary": - pkg := gno.NewPackageNode("binary", pkgPath, nil) - pkg.DefineGoNativeValue("LittleEndian", binary.LittleEndian) - pkg.DefineGoNativeValue("BigEndian", binary.BigEndian) - pkg.DefineGoNativeValue("Write", binary.BigEndian) // warn: use reflection - return pkg, pkg.NewPackage() - case "encoding/json": - pkg := gno.NewPackageNode("json", pkgPath, nil) - pkg.DefineGoNativeValue("Unmarshal", json.Unmarshal) - pkg.DefineGoNativeValue("Marshal", json.Marshal) - return pkg, pkg.NewPackage() - case "encoding/xml": - pkg := gno.NewPackageNode("xml", pkgPath, nil) - pkg.DefineGoNativeValue("Unmarshal", xml.Unmarshal) - return pkg, pkg.NewPackage() - case "internal/os_test": - pkg := gno.NewPackageNode("os_test", pkgPath, nil) - pkg.DefineNative("Sleep", - gno.Flds( // params - "d", gno.AnyT(), // NOTE: should be time.Duration - ), - gno.Flds( // results - ), - func(m *gno.Machine) { - // For testing purposes here, nanoseconds are separately kept track. - arg0 := m.LastBlock().GetParams1().TV - d := arg0.GetInt64() - sec := d / int64(time.Second) - nano := d % int64(time.Second) - ctx := m.Context.(*teststd.TestExecContext) - ctx.Timestamp += sec - ctx.TimestampNano += nano - if ctx.TimestampNano >= int64(time.Second) { - ctx.Timestamp += 1 - ctx.TimestampNano -= int64(time.Second) - } - m.Context = ctx - }, - ) - return pkg, pkg.NewPackage() - case "net": - pkg := gno.NewPackageNode("net", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(net.TCPAddr{})) - pkg.DefineGoNativeValue("IPv4", net.IPv4) - return pkg, pkg.NewPackage() - case "net/url": - pkg := gno.NewPackageNode("url", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(url.Values{})) - return pkg, pkg.NewPackage() - case "bufio": - pkg := gno.NewPackageNode("bufio", pkgPath, nil) - pkg.DefineGoNativeValue("NewScanner", bufio.NewScanner) - pkg.DefineGoNativeType(reflect.TypeOf(bufio.SplitFunc(nil))) - return pkg, pkg.NewPackage() - case "bytes": - pkg := gno.NewPackageNode("bytes", pkgPath, nil) - pkg.DefineGoNativeValue("Equal", bytes.Equal) - pkg.DefineGoNativeValue("Compare", bytes.Compare) - pkg.DefineGoNativeValue("NewReader", bytes.NewReader) - pkg.DefineGoNativeValue("NewBuffer", bytes.NewBuffer) - pkg.DefineGoNativeValue("Repeat", bytes.Repeat) - pkg.DefineGoNativeType(reflect.TypeOf(bytes.Buffer{})) - return pkg, pkg.NewPackage() - case "time": - pkg := gno.NewPackageNode("time", pkgPath, nil) - pkg.DefineGoNativeConstValue("Millisecond", time.Millisecond) - pkg.DefineGoNativeConstValue("Second", time.Second) - pkg.DefineGoNativeConstValue("Minute", time.Minute) - pkg.DefineGoNativeConstValue("Hour", time.Hour) - pkg.DefineGoNativeConstValue("Date", time.Date) - pkg.DefineGoNativeConstValue("Now", func() time.Time { return time.Unix(0, 0).UTC() }) // deterministic - pkg.DefineGoNativeConstValue("January", time.January) - pkg.DefineGoNativeConstValue("February", time.February) - pkg.DefineGoNativeConstValue("March", time.March) - pkg.DefineGoNativeConstValue("April", time.April) - pkg.DefineGoNativeConstValue("May", time.May) - pkg.DefineGoNativeConstValue("June", time.June) - pkg.DefineGoNativeConstValue("July", time.July) - pkg.DefineGoNativeConstValue("August", time.August) - pkg.DefineGoNativeConstValue("September", time.September) - pkg.DefineGoNativeConstValue("November", time.November) - pkg.DefineGoNativeConstValue("December", time.December) - pkg.DefineGoNativeValue("UTC", time.UTC) - pkg.DefineGoNativeValue("Unix", time.Unix) - pkg.DefineGoNativeType(reflect.TypeOf(time.Time{})) - pkg.DefineGoNativeType(reflect.TypeOf(time.Duration(0))) - pkg.DefineGoNativeType(reflect.TypeOf(time.Month(0))) - pkg.DefineGoNativeValue("LoadLocation", time.LoadLocation) - return pkg, pkg.NewPackage() - case "strconv": - pkg := gno.NewPackageNode("strconv", pkgPath, nil) - pkg.DefineGoNativeValue("Itoa", strconv.Itoa) - pkg.DefineGoNativeValue("Atoi", strconv.Atoi) - pkg.DefineGoNativeValue("ParseInt", strconv.ParseInt) - pkg.DefineGoNativeValue("Quote", strconv.Quote) - pkg.DefineGoNativeValue("FormatUint", strconv.FormatUint) - pkg.DefineGoNativeType(reflect.TypeOf(strconv.NumError{})) - return pkg, pkg.NewPackage() - case "strings": - pkg := gno.NewPackageNode("strings", pkgPath, nil) - pkg.DefineGoNativeValue("Split", strings.Split) - pkg.DefineGoNativeValue("SplitN", strings.SplitN) - pkg.DefineGoNativeValue("Contains", strings.Contains) - pkg.DefineGoNativeValue("TrimSpace", strings.TrimSpace) - pkg.DefineGoNativeValue("HasPrefix", strings.HasPrefix) - pkg.DefineGoNativeValue("NewReader", strings.NewReader) - pkg.DefineGoNativeValue("Index", strings.Index) - pkg.DefineGoNativeValue("IndexRune", strings.IndexRune) - pkg.DefineGoNativeValue("Join", strings.Join) - pkg.DefineGoNativeType(reflect.TypeOf(strings.Builder{})) - return pkg, pkg.NewPackage() - case "math": - pkg := gno.NewPackageNode("math", pkgPath, nil) - pkg.DefineGoNativeValue("Abs", math.Abs) - pkg.DefineGoNativeValue("Cos", math.Cos) - pkg.DefineGoNativeConstValue("Pi", math.Pi) - pkg.DefineGoNativeValue("Float64bits", math.Float64bits) - pkg.DefineGoNativeConstValue("MaxFloat32", math.MaxFloat32) - pkg.DefineGoNativeConstValue("MaxFloat64", math.MaxFloat64) - pkg.DefineGoNativeConstValue("MaxUint32", uint32(math.MaxUint32)) - pkg.DefineGoNativeConstValue("MaxUint64", uint64(math.MaxUint64)) - pkg.DefineGoNativeConstValue("MinInt8", math.MinInt8) - pkg.DefineGoNativeConstValue("MinInt16", math.MinInt16) - pkg.DefineGoNativeConstValue("MinInt32", math.MinInt32) - pkg.DefineGoNativeConstValue("MinInt64", int64(math.MinInt64)) - pkg.DefineGoNativeConstValue("MaxInt8", math.MaxInt8) - pkg.DefineGoNativeConstValue("MaxInt16", math.MaxInt16) - pkg.DefineGoNativeConstValue("MaxInt32", math.MaxInt32) - pkg.DefineGoNativeConstValue("MaxInt64", int64(math.MaxInt64)) - return pkg, pkg.NewPackage() - case "math/rand": - // XXX only expose for tests. - pkg := gno.NewPackageNode("rand", pkgPath, nil) - // make native rand same as gno rand. - rnd := rand.New(rand.NewPCG(0, 0)) //nolint:gosec - pkg.DefineGoNativeValue("IntN", rnd.IntN) - pkg.DefineGoNativeValue("Uint32", rnd.Uint32) - return pkg, pkg.NewPackage() - case "crypto/rand": - pkg := gno.NewPackageNode("rand", pkgPath, nil) - pkg.DefineGoNativeValue("Prime", crand.Prime) - // for determinism: - // pkg.DefineGoNativeValue("Reader", crand.Reader) - pkg.DefineGoNativeValue("Reader", &dummyReader{}) - return pkg, pkg.NewPackage() - case "crypto/md5": - pkg := gno.NewPackageNode("md5", pkgPath, nil) - pkg.DefineGoNativeValue("New", md5.New) - return pkg, pkg.NewPackage() - case "crypto/sha1": - pkg := gno.NewPackageNode("sha1", pkgPath, nil) - pkg.DefineGoNativeValue("New", sha1.New) - return pkg, pkg.NewPackage() - case "image": - pkg := gno.NewPackageNode("image", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(image.Point{})) - return pkg, pkg.NewPackage() - case "image/color": - pkg := gno.NewPackageNode("color", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(color.NRGBA64{})) - return pkg, pkg.NewPackage() - case "compress/flate": - pkg := gno.NewPackageNode("flate", pkgPath, nil) - pkg.DefineGoNativeConstValue("BestSpeed", flate.BestSpeed) - return pkg, pkg.NewPackage() - case "compress/gzip": - pkg := gno.NewPackageNode("gzip", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(gzip.Writer{})) - pkg.DefineGoNativeConstValue("BestCompression", gzip.BestCompression) - pkg.DefineGoNativeConstValue("BestSpeed", gzip.BestSpeed) - return pkg, pkg.NewPackage() - case "context": - pkg := gno.NewPackageNode("context", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf((*context.Context)(nil)).Elem()) - pkg.DefineGoNativeValue("WithValue", context.WithValue) - pkg.DefineGoNativeValue("Background", context.Background) - return pkg, pkg.NewPackage() - case "sync": - pkg := gno.NewPackageNode("sync", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(sync.Mutex{})) - pkg.DefineGoNativeType(reflect.TypeOf(sync.RWMutex{})) - pkg.DefineGoNativeType(reflect.TypeOf(sync.Pool{})) - return pkg, pkg.NewPackage() - case "sync/atomic": - pkg := gno.NewPackageNode("atomic", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(atomic.Value{})) - return pkg, pkg.NewPackage() - case "math/big": - pkg := gno.NewPackageNode("big", pkgPath, nil) - pkg.DefineGoNativeValue("NewInt", big.NewInt) - return pkg, pkg.NewPackage() - case "flag": - pkg := gno.NewPackageNode("flag", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(flag.Flag{})) - return pkg, pkg.NewPackage() - case "io": - pkg := gno.NewPackageNode("io", pkgPath, nil) - pkg.DefineGoNativeValue("EOF", io.EOF) - pkg.DefineGoNativeValue("NopCloser", io.NopCloser) - pkg.DefineGoNativeValue("ReadFull", io.ReadFull) - pkg.DefineGoNativeValue("ReadAll", io.ReadAll) - pkg.DefineGoNativeType(reflect.TypeOf((*io.ReadCloser)(nil)).Elem()) - pkg.DefineGoNativeType(reflect.TypeOf((*io.Closer)(nil)).Elem()) - pkg.DefineGoNativeType(reflect.TypeOf((*io.Reader)(nil)).Elem()) - return pkg, pkg.NewPackage() - case "log": - pkg := gno.NewPackageNode("log", pkgPath, nil) - pkg.DefineGoNativeValue("Fatal", log.Fatal) - return pkg, pkg.NewPackage() - case "text/template": - pkg := gno.NewPackageNode("template", pkgPath, nil) - pkg.DefineGoNativeType(reflect.TypeOf(template.FuncMap{})) - return pkg, pkg.NewPackage() - case "unicode/utf8": - pkg := gno.NewPackageNode("utf8", pkgPath, nil) - pkg.DefineGoNativeValue("DecodeRuneInString", utf8.DecodeRuneInString) - tv := gno.TypedValue{T: gno.UntypedRuneType} // TODO dry - tv.SetInt32(utf8.RuneSelf) // .. - pkg.Define("RuneSelf", tv) // .. - return pkg, pkg.NewPackage() - case "errors": - pkg := gno.NewPackageNode("errors", pkgPath, nil) - pkg.DefineGoNativeValue("New", errors.New) - return pkg, pkg.NewPackage() - case "hash/fnv": - pkg := gno.NewPackageNode("fnv", pkgPath, nil) - pkg.DefineGoNativeValue("New32a", fnv.New32a) - return pkg, pkg.NewPackage() - default: - // continue on... - } - } - - // if native package is preferred, try to load stdlibs/* as backup. - if mode == ImportModeNativePreferred { - pn, pv = loadStdlib(rootDir, pkgPath, store, stdout) - if pn != nil { - return - } - } - - // if examples package... - examplePath := filepath.Join(rootDir, "examples", pkgPath) - if osm.DirExists(examplePath) { - memPkg := gno.ReadMemPackage(examplePath, pkgPath) - if memPkg.IsEmpty() { - panic(fmt.Sprintf("found an empty package %q", pkgPath)) - } - - send := std.Coins{} - ctx := TestContext(pkgPath, send) - m2 := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "test", - Output: stdout, - Store: store, - Context: ctx, - }) - pn, pv = m2.RunMemPackage(memPkg, true) - return - } - return nil, nil - } - db := memdb.NewMemDB() - baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{}) - iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{}) - // make a new store - resStore = gno.NewStore(nil, baseStore, iavlStore) - resStore.SetPackageGetter(getPackage) - resStore.SetNativeStore(teststdlibs.NativeStore) - resStore.SetStrictGo2GnoMapping(false) - return -} - -func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) { - dirs := [...]string{ - // normal stdlib path. - filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath), - // override path. definitions here override the previous if duplicate. - filepath.Join(rootDir, "gnovm", "tests", "stdlibs", pkgPath), - } - files := make([]string, 0, 32) // pre-alloc 32 as a likely high number of files - for _, path := range dirs { - dl, err := os.ReadDir(path) - if err != nil { - if os.IsNotExist(err) { - continue - } - panic(fmt.Errorf("could not access dir %q: %w", path, err)) - } - - for _, f := range dl { - // NOTE: RunMemPackage has other rules; those should be mostly useful - // for on-chain packages (ie. include README and gno.mod). - if !f.IsDir() && strings.HasSuffix(f.Name(), ".gno") { - files = append(files, filepath.Join(path, f.Name())) - } - } - } - if len(files) == 0 { - return nil, nil - } - - memPkg := gno.ReadMemPackageFromList(files, pkgPath) - m2 := gno.NewMachineWithOptions(gno.MachineOptions{ - // NOTE: see also pkgs/sdk/vm/builtins.go - // Needs PkgPath != its name because TestStore.getPackage is the package - // getter for the store, which calls loadStdlib, so it would be recursively called. - PkgPath: "stdlibload", - Output: stdout, - Store: store, - }) - save := pkgPath != "testing" // never save the "testing" package - return m2.RunMemPackageWithOverrides(memPkg, save) -} - -type dummyReader struct{} - -func (*dummyReader) Read(b []byte) (n int, err error) { - for i := 0; i < len(b); i++ { - b[i] = byte((100 + i) % 256) - } - return len(b), nil -} - -// ---------------------------------------- - -type TestReport struct { - Name string - Verbose bool - Failed bool - Skipped bool - Output string -} diff --git a/gnovm/tests/machine_test.go b/gnovm/tests/machine_test.go deleted file mode 100644 index a67d67f1ff2..00000000000 --- a/gnovm/tests/machine_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package tests - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" -) - -func TestMachineTestMemPackage(t *testing.T) { - matchFunc := func(pat, str string) (bool, error) { return true, nil } - - tests := []struct { - name string - path string - shouldSucceed bool - }{ - { - name: "TestSuccess", - path: "testdata/TestMemPackage/success", - shouldSucceed: true, - }, - { - name: "TestFail", - path: "testdata/TestMemPackage/fail", - shouldSucceed: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // NOTE: Because the purpose of this test is to ensure testing.T.Failed() - // returns true if a gno test is failing, and because we don't want this - // to affect the current testing.T, we are creating an other one thanks - // to testing.RunTests() function. - testing.RunTests(matchFunc, []testing.InternalTest{ - { - Name: tt.name, - F: func(t2 *testing.T) { //nolint:thelper - rootDir := filepath.Join("..", "..") - store := TestStore(rootDir, "test", os.Stdin, os.Stdout, os.Stderr, ImportModeStdlibsOnly) - store.SetLogStoreOps(true) - m := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "test", - Output: os.Stdout, - Store: store, - Context: nil, - }) - memPkg := gno.ReadMemPackage(tt.path, "test") - - m.TestMemPackage(t2, memPkg) - - if tt.shouldSucceed { - assert.False(t, t2.Failed(), "test %q should have succeed", tt.name) - } else { - assert.True(t, t2.Failed(), "test %q should have failed", tt.name) - } - }, - }, - }) - }) - } -} diff --git a/gnovm/tests/package_test.go b/gnovm/tests/package_test.go deleted file mode 100644 index d4ddfc9a4f0..00000000000 --- a/gnovm/tests/package_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package tests - -import ( - "bytes" - "fmt" - "io/fs" - "log" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" -) - -func TestStdlibs(t *testing.T) { - t.Parallel() - - // NOTE: this test only works using _test.gno files; - // filetests are not meant to be used for testing standard libraries. - // The examples directory is tested directly using `gno test`u - - // find all packages with *_test.gno files. - rootDirs := []string{ - filepath.Join("..", "stdlibs"), - } - testDirs := map[string]string{} // aggregate here, pkgPath -> dir - pkgPaths := []string{} - for _, rootDir := range rootDirs { - fileSystem := os.DirFS(rootDir) - fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - log.Fatal(err) - } - if d.IsDir() { - return nil - } - if strings.HasSuffix(path, "_test.gno") { - dirPath := filepath.Dir(path) - if _, exists := testDirs[dirPath]; exists { - // already exists. - } else { - testDirs[dirPath] = filepath.Join(rootDir, dirPath) - pkgPaths = append(pkgPaths, dirPath) - } - } - return nil - }) - } - // For each package with testfiles (in testDirs), call Machine.TestMemPackage. - for _, pkgPath := range pkgPaths { - testDir := testDirs[pkgPath] - t.Run(pkgPath, func(t *testing.T) { - pkgPath := pkgPath - t.Parallel() - runPackageTest(t, testDir, pkgPath) - }) - } -} - -func runPackageTest(t *testing.T, dir string, path string) { - t.Helper() - - memPkg := gno.ReadMemPackage(dir, path) - require.False(t, memPkg.IsEmpty()) - - stdin := new(bytes.Buffer) - // stdout := new(bytes.Buffer) - stdout := os.Stdout - stderr := new(bytes.Buffer) - rootDir := filepath.Join("..", "..") - store := TestStore(rootDir, path, stdin, stdout, stderr, ImportModeStdlibsOnly) - store.SetLogStoreOps(true) - m := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "test", - Output: stdout, - Store: store, - Context: nil, - }) - m.TestMemPackage(t, memPkg) - - // Check that machine is empty. - err := m.CheckEmpty() - if err != nil { - t.Log("last state: \n", m.String()) - panic(fmt.Sprintf("machine not empty after main: %v", err)) - } -} diff --git a/gnovm/tests/selector_test.go b/gnovm/tests/selector_test.go deleted file mode 100644 index 1f0b400555b..00000000000 --- a/gnovm/tests/selector_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package tests - -import ( - "fmt" - "reflect" - "testing" -) - -/* -This attempts to show a sufficiently exhaustive list of ValuePaths for -different types of selectors. As can be seen, even a simple selector -expression can represent a number of different types of selectors. -*/ - -// S1 struct -type S1 struct { - F0 int -} - -func (S1) Hello() { -} - -func (*S1) Bye() { -} - -// Pointer to S1 -type S1P *S1 - -// Like S1 but pointer struct -type PS1 *struct { - F0 int -} - -type S7 struct { - S1 -} - -type S9 struct { - *S1 -} - -type S10PD *struct { - S1 -} - -func _printValue(x interface{}) { - if reflect.TypeOf(x).Kind() == reflect.Func { - fmt.Println("function") - } else { - fmt.Println(x) - } -} - -func TestSelectors(t *testing.T) { - t.Parallel() - - x0 := struct{ F0 int }{1} - _printValue(x0.F0) // *ST.F0 - // F:0 - // VPField{depth:0,index:0} - x1 := S1{1} - _printValue(x1.F0) // *DT(S1)>*ST.F0 - // +1 F:0 - // VPField{depth:1,index:0} - _printValue(x1.Hello) // *DT(S1).Hello - // +1 M:0 - // VPValMethod{index:0} - _printValue(x1.Bye) // *PT(implied)>*DT(S1).Bye - // +D +1 *M:1 - // VPDerefPtrMethod{index:1} - x2 := &x0 - _printValue(x2.F0) // *PT>*ST.F0 - // +D F:0 - // VPDerefField{depth:0,index:0} - var x3 PS1 = &struct{ F0 int }{1} - _printValue(x3.F0) // *DT(S1P)>*PT>*ST.F0 - // +1 +D F:0 - // VPDerefField{depth:1,index:0} - x4 := &S1{1} - _printValue(x4.F0) // *PT>*DT(S1P)>*ST.F0 - // +D +1 F:0 - // VPDerefField{depth:2,index:0} - var x5 S1P = &S1{1} - _printValue(x5.F0) // *DT(S1P)>*PT>*DT(S1)>*ST.F0 - // +1 +D +1 F:0 - // VPDerefField{depth:3,index:0} - x6 := &x5 - _printValue(x6) - // _printValue(x6.F0) *PT>*DT(S1P)??? > *PT>*DT(S1)>*ST.F0 - // +D +1 +D +1 F:0 - // VPDerefField{depth:1,index:0}(WRONG!!!) > VPDerefField{depth:1,index:0} XXX ERROR - x7 := S7{S1{1}} - _printValue(x7.F0) // *DT(S7)>*ST.S1 > *DT(S1)>*ST.F0 - // +1 F:0 +1 F:0 - // VPField{depth:1,index:0} > VPField{depth:1,index:0} - x8 := &x7 - _printValue(x8.F0) // *PT>*DT(S7)>*ST.S1 > *DT(S1)>*ST.F0 - // +D +1 F:0 +1 F:0 - // VPDerefField{depth:1,index:0} > VPField{depth:1,index:0} - x9 := S9{x5} - _printValue(x9.F0) // *DT(S9)>*ST.S1 > *PT>*DT(S1)>*ST.F0 - // +1 F:0 +D +1 F:0 - // VPField{depth:1,index:0} > VPDerefField{depth:1,index:0} - x10 := struct{ S1 }{S1{1}} - _printValue(x10.F0) // *ST.S1 > *DT(S1)>*ST.F0 - // F:0 +1 F:0 - // VPField{depth:0,index:0} > VPField{depth:1,index:0} - _printValue(x10.Hello) // *ST.S1 > *DT(S1).Hello - // F:0 +1 M:0 - // VPField{depth:0,index:0} > VPValMethod{index:0} - _printValue(x10.Bye) // (*PT>)*ST.S1 > *DT(S1).Bye - // +S F:0 +1 *M:1 - // VPSubrefField{depth:0,index:0} > VPDerefPtrMethod{index:1} - x10p := &x10 - _printValue(x10p.F0) // *PT>*ST.S1 > *DT(S1)>*ST.F0 - // +D F:0 +1 F:0 - // VPDerefField{depth:0,index:0} > VPField{depth:1,index:0} - _printValue(x10p.Hello) // *PT>*ST.S1 > *DT(S1).Hello - // +D F:0 +1 M:0 - // VPDerefField{depth:0,index:0} > VPValMethod{index:0} - _printValue(x10p.Bye) // *PT>*ST.S1 > *DT(S1).Bye - // +D F:0 +1 *M:1 - // VPSubrefField{depth:0,index:0} > VPDerefPtrMethod{index:1} - var x10pd S10PD = &struct{ S1 }{S1{1}} - _printValue(x10pd.F0) // *DT(S10PD)>*PT>*ST.S1 > *DT(S1)>*ST.F0 - // +1 +D F:0 +1 F:0 - // VPDerefField{depth:1,index:0} > VPField{depth:1,index:0} - // _printValue(x10pd.Hello) *DT(S10PD)>*PT>*ST.S1 > *DT(S1).Hello XXX weird, doesn't work. - // +1 +D F:0 +1 M:0 - // VPDerefField{depth:1,index:0} > VPValMethod{index:0} - _printValue(x10p.Bye) // *DT(S10PD)>*PT>*ST.S1 > *DT(S1).Bye - // +1 +D F:0 +1 *M:1 - // VPSubrefField{depth:1,index:0} > VPDerefPtrMethod{index:1} - x11 := S7{S1{1}} - _printValue(x11.F0) // *DT(S7)>*ST.S1 > *DT(S1)>*ST.F0 NOTE same as x7. - // +1 F:0 +1 F:0 - // VPField{depth:1,index:0} > VPField{depth:1,index:0} - _printValue(x11.Hello) // *DT(S7)>*ST.S1 > *DT(S1)>*ST.Hello - // +1 F:0 +1 M:0 - // VPField{depth:1,index:0} > VPValMethod{index:0} - _printValue(x11.Bye) // (*PT>)*DT(S7)>*ST.S1 > *DT(S1).Bye - // +S +1 F:0 +1 *M:1 - // VPSubrefField{depth:2,index:0} > VPDerefPtrMethod{index:1} - x11p := &S7{S1{1}} - _printValue(x11p.F0) // *PT>*DT(S7)>*ST.S1 > *DT(S1)>*ST.F0 - // +1 F:0 +1 F:0 - // VPDerefField{depth:2,index:0} > VPField{depth:1,index:0} - _printValue(x11p.Hello) // *PT>*DT(S7)>*ST.S1 > *DT(S1).Hello - // +1 F:0 +1 M:0 - // VPDerefField{depth:2,index:0} > VPValMethod{index:0} - _printValue(x11p.Bye) // *PT>*DT(S7)>*ST.S1 > *DT(S1).Bye - // +1 F:0 +1 *M:1 - // VPSubrefField{depth:2,index:0} > VPDerefPtrMethod{index:1} - x12 := struct{ *S1 }{&S1{1}} - _printValue(x12.F0) // *ST.S1 > *PT>*DT(S1)>*ST.F0 - // F:0 +D +1 F:0 - // VPField{depth:0,index:0} > VPDerefField{depth:1,index:0} - _printValue(x12.Hello) // *ST.S1 > *PT>*DT(S1).Hello - // F:0 +D +1 M:0 - // VPField{depth:0,index:0} > VPDerefValMethod{index:0} - _printValue(x12.Bye) // *ST.S1 > *PT>*DT(S1).Bye - // F:0 +D +1 *M:1 - // VPField{depth:0,index:0} > VPDerefPtrMethod{index:1} - x13 := &x12 - _printValue(x13.F0) // *PT>*ST.S1 > *PT>*DT(S1)>*ST.F0 - // +D F:0 +D +1 F:0 - // VPDerefField{depth:0,index:0} > VPDerefField{depth:1,index:0} - _printValue(x13.Hello) // *PT>*ST.S1 > *PT>*DT(S1).Hello - // +D F:0 +D +1 M:0 - // VPDerefField{depth:0,index:0} > VPDerefValMethod{index:0} - _printValue(x13.Bye) // *PT>*ST.S1 > *PT>*DT(S1).Bye - // +D F:0 +D +1 *M:1 - // VPDerefField{depth:0,index:0} > VPDerefPtrMethod{index:1} -} diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go index f3d74e214eb..2cc904a9170 100644 --- a/gnovm/tests/stdlibs/generated.go +++ b/gnovm/tests/stdlibs/generated.go @@ -84,18 +84,6 @@ var nativeFuncs = [...]NativeFunc{ p0) }, }, - { - "std", - "ClearStoreCache", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{}, - true, - func(m *gno.Machine) { - testlibs_std.ClearStoreCache( - m, - ) - }, - }, { "std", "callerAt", diff --git a/gnovm/tests/stdlibs/std/std.gno b/gnovm/tests/stdlibs/std/std.gno index 3a56ecc1c47..dcb5a64dbb3 100644 --- a/gnovm/tests/stdlibs/std/std.gno +++ b/gnovm/tests/stdlibs/std/std.gno @@ -3,7 +3,6 @@ package std func AssertOriginCall() // injected func IsOriginCall() bool // injected func TestSkipHeights(count int64) // injected -func ClearStoreCache() // injected func TestSetOrigCaller(addr Address) { testSetOrigCaller(string(addr)) } func TestSetOrigPkgAddr(addr Address) { testSetOrigPkgAddr(string(addr)) } diff --git a/gnovm/tests/stdlibs/std/std.go b/gnovm/tests/stdlibs/std/std.go index d580572e9c5..675194b252f 100644 --- a/gnovm/tests/stdlibs/std/std.go +++ b/gnovm/tests/stdlibs/std/std.go @@ -3,11 +3,11 @@ package std import ( "fmt" "strings" - "testing" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs/std" "github.com/gnolang/gno/tm2/pkg/crypto" + tm2std "github.com/gnolang/gno/tm2/pkg/std" ) // TestExecContext is the testing extension of the exec context. @@ -41,9 +41,17 @@ func IsOriginCall(m *gno.Machine) bool { tname := m.Frames[0].Func.Name switch tname { case "main": // test is a _filetest + // 0. main + // 1. $RealmFuncName + // 2. std.IsOriginCall return len(m.Frames) == 3 - case "runtest": // test is a _test - return len(m.Frames) == 7 + case "RunTest": // test is a _test + // 0. testing.RunTest + // 1. tRunner + // 2. $TestFuncName + // 3. $RealmFuncName + // 4. std.IsOriginCall + return len(m.Frames) == 5 } // support init() in _filetest // XXX do we need to distinguish from 'runtest'/_test? @@ -61,23 +69,6 @@ func TestSkipHeights(m *gno.Machine, count int64) { m.Context = ctx } -func ClearStoreCache(m *gno.Machine) { - if gno.IsDebug() && testing.Verbose() { - m.Store.Print() - fmt.Println("========================================") - fmt.Println("CLEAR CACHE (RUNTIME)") - fmt.Println("========================================") - } - m.Store.ClearCache() - m.PreprocessAllFilesAndSaveBlockNodes() - if gno.IsDebug() && testing.Verbose() { - m.Store.Print() - fmt.Println("========================================") - fmt.Println("CLEAR CACHE DONE") - fmt.Println("========================================") - } -} - func X_callerAt(m *gno.Machine, n int) string { if n <= 0 { m.Panic(typedString("GetCallerAt requires positive arg")) @@ -188,6 +179,60 @@ func X_testSetOrigSend(m *gno.Machine, m.Context = ctx } +// TestBanker is a banker that can be used as a mock banker in test contexts. +type TestBanker struct { + CoinTable map[crypto.Bech32Address]tm2std.Coins +} + +var _ std.BankerInterface = &TestBanker{} + +// GetCoins implements the Banker interface. +func (tb *TestBanker) GetCoins(addr crypto.Bech32Address) (dst tm2std.Coins) { + return tb.CoinTable[addr] +} + +// SendCoins implements the Banker interface. +func (tb *TestBanker) SendCoins(from, to crypto.Bech32Address, amt tm2std.Coins) { + fcoins, fexists := tb.CoinTable[from] + if !fexists { + panic(fmt.Sprintf( + "source address %s does not exist", + from.String())) + } + if !fcoins.IsAllGTE(amt) { + panic(fmt.Sprintf( + "source address %s has %s; cannot send %s", + from.String(), fcoins, amt)) + } + // First, subtract from 'from'. + frest := fcoins.Sub(amt) + tb.CoinTable[from] = frest + // Second, add to 'to'. + // NOTE: even works when from==to, due to 2-step isolation. + tcoins, _ := tb.CoinTable[to] + tsum := tcoins.Add(amt) + tb.CoinTable[to] = tsum +} + +// TotalCoin implements the Banker interface. +func (tb *TestBanker) TotalCoin(denom string) int64 { + panic("not yet implemented") +} + +// IssueCoin implements the Banker interface. +func (tb *TestBanker) IssueCoin(addr crypto.Bech32Address, denom string, amt int64) { + coins, _ := tb.CoinTable[addr] + sum := coins.Add(tm2std.Coins{{Denom: denom, Amount: amt}}) + tb.CoinTable[addr] = sum +} + +// RemoveCoin implements the Banker interface. +func (tb *TestBanker) RemoveCoin(addr crypto.Bech32Address, denom string, amt int64) { + coins, _ := tb.CoinTable[addr] + rest := coins.Sub(tm2std.Coins{{Denom: denom, Amount: amt}}) + tb.CoinTable[addr] = rest +} + func X_testIssueCoins(m *gno.Machine, addr string, denom []string, amt []int64) { ctx := m.Context.(*TestExecContext) banker := ctx.Banker diff --git a/gnovm/tests/testdata/TestMemPackage/fail/file_test.gno b/gnovm/tests/testdata/TestMemPackage/fail/file_test.gno deleted file mode 100644 index b202c40bc46..00000000000 --- a/gnovm/tests/testdata/TestMemPackage/fail/file_test.gno +++ /dev/null @@ -1,7 +0,0 @@ -package test - -import "testing" - -func TestFail(t *testing.T) { - t.Errorf("OUPS") -} diff --git a/gnovm/tests/testdata/TestMemPackage/success/file_test.gno b/gnovm/tests/testdata/TestMemPackage/success/file_test.gno deleted file mode 100644 index 0fc1d898199..00000000000 --- a/gnovm/tests/testdata/TestMemPackage/success/file_test.gno +++ /dev/null @@ -1,5 +0,0 @@ -package test - -import "testing" - -func TestSucess(t *testing.T) {} From 4004ba139ab1fccb2987faea9c244f8eb46f181a Mon Sep 17 00:00:00 2001 From: Mikael VALLENET Date: Tue, 26 Nov 2024 16:06:40 +0100 Subject: [PATCH 261/345] feat(gnovm): forbid importing realms in packages (#3042) Closes #3040 50% of the work comes from @harry-hov's PR #1393 (let's repay to Caesar what belongs to Caesar) :rocket: Notable additions: - handle different domains (e.g github.com/p/demo/...) - skip non ``.gno`` files (LICENSE, README, ...) or empty files
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: n0izn0iz Co-authored-by: Morgan Co-authored-by: Morgan --- examples/gno.land/p/demo/groups/gno.mod | 5 +--- examples/gno.land/p/demo/groups/groups.gno | 8 ----- examples/gno.land/p/demo/tests/gno.mod | 1 - examples/gno.land/p/demo/tests/tests.gno | 13 --------- .../gno.land/p/demo/tests/z0_filetest.gno | 16 ---------- .../gnoland/testdata/assertorigincall.txtar | 29 ++++++++++--------- gno.land/cmd/gnoland/testdata/prevrealm.txtar | 22 +++++++------- gnovm/pkg/gnolang/helpers.go | 12 +++++++- gnovm/pkg/gnolang/preprocess.go | 7 +++++ gnovm/tests/files/import11.gno | 13 +++++++++ gnovm/tests/files/zrealm_crossrealm11.gno | 11 ++----- 11 files changed, 60 insertions(+), 77 deletions(-) delete mode 100644 examples/gno.land/p/demo/groups/groups.gno delete mode 100644 examples/gno.land/p/demo/tests/z0_filetest.gno create mode 100644 gnovm/tests/files/import11.gno diff --git a/examples/gno.land/p/demo/groups/gno.mod b/examples/gno.land/p/demo/groups/gno.mod index f0749e3f411..cf33d0ce74b 100644 --- a/examples/gno.land/p/demo/groups/gno.mod +++ b/examples/gno.land/p/demo/groups/gno.mod @@ -1,6 +1,3 @@ module gno.land/p/demo/groups -require ( - gno.land/p/demo/rat v0.0.0-latest - gno.land/r/demo/boards v0.0.0-latest -) +require gno.land/p/demo/rat v0.0.0-latest diff --git a/examples/gno.land/p/demo/groups/groups.gno b/examples/gno.land/p/demo/groups/groups.gno deleted file mode 100644 index fcf77dd2a74..00000000000 --- a/examples/gno.land/p/demo/groups/groups.gno +++ /dev/null @@ -1,8 +0,0 @@ -package groups - -import "gno.land/r/demo/boards" - -// TODO implement something and test. -type Group struct { - Board *boards.Board -} diff --git a/examples/gno.land/p/demo/tests/gno.mod b/examples/gno.land/p/demo/tests/gno.mod index d3d796f76f8..8a19acdbb18 100644 --- a/examples/gno.land/p/demo/tests/gno.mod +++ b/examples/gno.land/p/demo/tests/gno.mod @@ -3,5 +3,4 @@ module gno.land/p/demo/tests require ( gno.land/p/demo/tests/subtests v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest - gno.land/r/demo/tests v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/tests/tests.gno b/examples/gno.land/p/demo/tests/tests.gno index 43732d82dac..ffad5b8c8cd 100644 --- a/examples/gno.land/p/demo/tests/tests.gno +++ b/examples/gno.land/p/demo/tests/tests.gno @@ -4,19 +4,10 @@ import ( "std" psubtests "gno.land/p/demo/tests/subtests" - "gno.land/r/demo/tests" - rtests "gno.land/r/demo/tests" ) const World = "world" -// IncCounter demonstrates that it's possible to call a realm function from -// a package. So a package can potentially write into the store, by calling -// an other realm. -func IncCounter() { - tests.IncCounter() -} - func CurrentRealmPath() string { return std.CurrentRealm().PkgPath() } @@ -64,10 +55,6 @@ func GetPSubtestsPrevRealm() std.Realm { return psubtests.GetPrevRealm() } -func GetRTestsGetPrevRealm() std.Realm { - return rtests.GetPrevRealm() -} - // Warning: unsafe pattern. func Exec(fn func()) { fn() diff --git a/examples/gno.land/p/demo/tests/z0_filetest.gno b/examples/gno.land/p/demo/tests/z0_filetest.gno deleted file mode 100644 index b788eaf398f..00000000000 --- a/examples/gno.land/p/demo/tests/z0_filetest.gno +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - ptests "gno.land/p/demo/tests" - rtests "gno.land/r/demo/tests" -) - -func main() { - println(rtests.Counter()) - ptests.IncCounter() - println(rtests.Counter()) -} - -// Output: -// 0 -// 1 diff --git a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar index 1315f23cc95..62d660a9215 100644 --- a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar +++ b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar @@ -9,18 +9,18 @@ # | 4 | | through /r/foo | myrealm.A() | PANIC | # | 5 | | | myrealm.B() | pass | # | 6 | | | myrealm.C() | PANIC | -# | 7 | | through /p/demo/bar | myrealm.A() | PANIC | -# | 8 | | | myrealm.B() | pass | -# | 9 | | | myrealm.C() | PANIC | +# | 7 | | through /p/demo/bar | bar.A() | PANIC | +# | 8 | | | bar.B() | pass | +# | 9 | | | bar.C() | PANIC | # | 10 | MsgRun | wallet direct | myrealm.A() | PANIC | # | 11 | | | myrealm.B() | pass | # | 12 | | | myrealm.C() | PANIC | # | 13 | | through /r/foo | myrealm.A() | PANIC | # | 14 | | | myrealm.B() | pass | # | 15 | | | myrealm.C() | PANIC | -# | 16 | | through /p/demo/bar | myrealm.A() | PANIC | -# | 17 | | | myrealm.B() | pass | -# | 18 | | | myrealm.C() | PANIC | +# | 16 | | through /p/demo/bar | bar.A() | PANIC | +# | 17 | | | bar.B() | pass | +# | 18 | | | bar.C() | PANIC | # | 19 | MsgCall | wallet direct | std.AssertOriginCall() | pass | # | 20 | MsgRun | wallet direct | std.AssertOriginCall() | PANIC | @@ -57,15 +57,15 @@ stdout 'OK!' stderr 'invalid non-origin call' ## remove due to update to maketx call can only call realm (case 7,8,9) -## 7. MsgCall -> p/demo/bar.A -> myrlm.A: PANIC +## 7. MsgCall -> p/demo/bar.A: PANIC ## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' -## 8. MsgCall -> p/demo/bar.B -> myrlm.B: PASS +## 8. MsgCall -> p/demo/bar.B: PASS ## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' -## 9. MsgCall -> p/demo/bar.C -> myrlm.C: PANIC +## 9. MsgCall -> p/demo/bar.C: PANIC ## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' @@ -152,18 +152,19 @@ func C() { -- p/demo/bar/bar.gno -- package bar -import "gno.land/r/myrlm" +import "std" func A() { - myrlm.A() + C() } func B() { - myrlm.B() + if false { + C() + } } - func C() { - myrlm.C() + std.AssertOriginCall() } -- run/myrlmA.gno -- package main diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar index 7a0d994a686..4a7cece6d62 100644 --- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar +++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar @@ -8,14 +8,14 @@ # | 2 | | | myrlm.B() | user address | # | 3 | | through /r/foo | myrlm.A() | r/foo | # | 4 | | | myrlm.B() | r/foo | -# | 5 | | through /p/demo/bar | myrlm.A() | user address | -# | 6 | | | myrlm.B() | user address | +# | 5 | | through /p/demo/bar | bar.A() | user address | +# | 6 | | | bar.B() | user address | # | 7 | MsgRun | wallet direct | myrlm.A() | user address | # | 8 | | | myrlm.B() | user address | # | 9 | | through /r/foo | myrlm.A() | r/foo | # | 10 | | | myrlm.B() | r/foo | -# | 11 | | through /p/demo/bar | myrlm.A() | user address | -# | 12 | | | myrlm.B() | user address | +# | 11 | | through /p/demo/bar | bar.A() | user address | +# | 12 | | | bar.B() | user address | # | 13 | MsgCall | wallet direct | std.PrevRealm() | user address | # | 14 | MsgRun | wallet direct | std.PrevRealm() | user address | @@ -50,11 +50,11 @@ gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wan stdout ${RFOO_ADDR} ## remove due to update to maketx call can only call realm (case 5, 6, 13) -## 5. MsgCall -> p/demo/bar.A -> myrlm.A: user address +## 5. MsgCall -> p/demo/bar.A: user address ## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 ## stdout ${USER_ADDR_test1} -## 6. MsgCall -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address +## 6. MsgCall -> p/demo/bar.B: user address ## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 ## stdout ${USER_ADDR_test1} @@ -74,11 +74,11 @@ stdout ${RFOO_ADDR} gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout ${RFOO_ADDR} -## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address +## 11. MsgRun -> p/demo/bar.A: user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stdout ${USER_ADDR_test1} -## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address +## 12. MsgRun -> p/demo/bar.B: user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout ${USER_ADDR_test1} @@ -117,14 +117,14 @@ func B() string { -- p/demo/bar/bar.gno -- package bar -import "gno.land/r/myrlm" +import "std" func A() string { - return myrlm.A() + return std.PrevRealm().Addr().String() } func B() string { - return myrlm.B() + return A() } -- run/myrlmA.gno -- package main diff --git a/gnovm/pkg/gnolang/helpers.go b/gnovm/pkg/gnolang/helpers.go index c6f7e696ea4..d3a8485ee17 100644 --- a/gnovm/pkg/gnolang/helpers.go +++ b/gnovm/pkg/gnolang/helpers.go @@ -12,7 +12,10 @@ import ( // RealmPathPrefix is the prefix used to identify pkgpaths which are meant to // be realms and as such to have their state persisted. This is used by [IsRealmPath]. -const RealmPathPrefix = "gno.land/r/" +const ( + RealmPathPrefix = "gno.land/r/" + PackagePathPrefix = "gno.land/p/" +) // ReGnoRunPath is the path used for realms executed in maketx run. // These are not considered realms, as an exception to the RealmPathPrefix rule. @@ -26,6 +29,13 @@ func IsRealmPath(pkgPath string) bool { !ReGnoRunPath.MatchString(pkgPath) } +// IsPurePackagePath determines whether the given pkgpath is for a published Gno package. +// It only considers "pure" those starting with gno.land/p/, so it returns false for +// stdlib packages and MsgRun paths. +func IsPurePackagePath(pkgPath string) bool { + return strings.HasPrefix(pkgPath, PackagePathPrefix) +} + // IsStdlib determines whether s is a pkgpath for a standard library. func IsStdlib(s string) bool { // NOTE(morgan): this is likely to change in the future as we add support for diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 4b556604f0b..53c187342a6 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -174,6 +174,13 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { case *ImportDecl: nx := &n.NameExpr nn := nx.Name + loc := last.GetLocation() + // NOTE: imports from "pure packages" are actually sometimes + // allowed, most notably in MsgRun and filetests; IsPurePackagePath + // returns false in these cases. + if IsPurePackagePath(loc.PkgPath) && IsRealmPath(n.PkgPath) { + panic(fmt.Sprintf("pure package path %q cannot import realm path %q", loc.PkgPath, n.PkgPath)) + } if nn == "." { panic("dot imports not allowed in gno") } diff --git a/gnovm/tests/files/import11.gno b/gnovm/tests/files/import11.gno new file mode 100644 index 00000000000..594e9f10698 --- /dev/null +++ b/gnovm/tests/files/import11.gno @@ -0,0 +1,13 @@ +// PKGPATH: gno.land/p/demo/bar +package bar + +import ( + "gno.land/r/demo/tests" +) + +func main() { + println(tests.Counter()) +} + +// Error: +// gno.land/p/demo/bar/files/import11.gno:5:2: pure package path "gno.land/p/demo/bar" cannot import realm path "gno.land/r/demo/tests" diff --git a/gnovm/tests/files/zrealm_crossrealm11.gno b/gnovm/tests/files/zrealm_crossrealm11.gno index e6f33c50654..5936743ddc6 100644 --- a/gnovm/tests/files/zrealm_crossrealm11.gno +++ b/gnovm/tests/files/zrealm_crossrealm11.gno @@ -2,10 +2,11 @@ package crossrealm_test import ( + "std" + ptests "gno.land/p/demo/tests" "gno.land/p/demo/ufmt" rtests "gno.land/r/demo/tests" - "std" ) func getPrevRealm() std.Realm { @@ -64,10 +65,6 @@ func main() { callStackAdd: " -> r/demo/tests -> r/demo/tests/subtests", callerFn: rtests.GetRSubtestsPrevRealm, }, - { - callStackAdd: " -> p/demo/tests -> r/demo/tests", - callerFn: ptests.GetRTestsGetPrevRealm, - }, } println("---") // needed to have space prefixes @@ -140,7 +137,3 @@ func printColumns(left, right string) { // user1.gno -> r/crossrealm_test.main -> r/crossrealm_test.Exec -> r/demo/tests -> r/demo/tests/subtests = gno.land/r/demo/tests // user1.gno -> r/crossrealm_test.main -> r/demo/tests.Exec -> r/demo/tests -> r/demo/tests/subtests = gno.land/r/demo/tests // user1.gno -> r/crossrealm_test.main -> p/demo/tests.Exec -> r/demo/tests -> r/demo/tests/subtests = gno.land/r/demo/tests -// user1.gno -> r/crossrealm_test.main -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test -// user1.gno -> r/crossrealm_test.main -> r/crossrealm_test.Exec -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test -// user1.gno -> r/crossrealm_test.main -> r/demo/tests.Exec -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test -// user1.gno -> r/crossrealm_test.main -> p/demo/tests.Exec -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test From 2093d8a43c1ec632707f28021860ac52f5a80c26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:25:20 +0100 Subject: [PATCH 262/345] chore(deps): bump golang.org/x/net from 0.0.0-20190813141303-74dc4d7220e7 to 0.23.0 in /contribs/gnomd in the go_modules group across 1 directory (#3154) Bumps the go_modules group with 1 update in the /contribs/gnomd directory: [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/net` from 0.0.0-20190813141303-74dc4d7220e7 to 0.23.0
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/net&package-manager=go_modules&previous-version=0.0.0-20190813141303-74dc4d7220e7&new-version=0.23.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/gnolang/gno/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- contribs/gnomd/go.mod | 4 ++-- contribs/gnomd/go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index 8bc352d4848..423e4414a79 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -22,6 +22,6 @@ require ( github.com/mattn/go-runewidth v0.0.12 // indirect github.com/rivo/uniseg v0.1.0 // indirect golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect - golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect - golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/contribs/gnomd/go.sum b/contribs/gnomd/go.sum index b4ad4f5c9bf..0ff70dd99fb 100644 --- a/contribs/gnomd/go.sum +++ b/contribs/gnomd/go.sum @@ -57,13 +57,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= From d8589b06b14c9b5d4f887faec047813a9d1a1756 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 27 Nov 2024 11:55:39 +0900 Subject: [PATCH 263/345] fix(gnovm): Prevent use of blank identifier as Value or Type (#2699) # Description Closes #1946 The `isNamedConversion` function now includes a safety check to prevent the use of blank identifiers ("_") as values or types. If both `xt` and `t` are nil, the function assumes that a blank identifier is being used inappropriately and panics with an error message that includes the location of the issue. ## Variable Explanations - `xt` (Expression Type): Represents the type of the right-hand side of an assignment or expression. It's the type resulting from evaluating an expression. - `t` (Target Type): Represents the type of the left-hand side of an assignment. It's the variable or field that will receive the value. Checks if a named conversion is needed when assigning a value of type `xt` to a variable of type `t`. ## Preprocess Added some checks to prevent the disallowd usage of blank identifiers in `Preprocess` function level. Theses checks are performed at different stages of the preprocessing: 1. `TRANS_ENTER` for `AssignStmt`: - Checks if both LHS and RHS are blank identifiers in a `DEFINE` statement. 2. `TRANS_LEAVE` for `NameExpr`: - Checks if blank identifier is used as a value in disallowed contexts (excluding `TRANS_ASSIGN_LHS`, `TRANS_RANGE_KEY` and `TRANS_RANGE_VALUE`). 3. `TRANS_LEAVE` for `AssignStmt`: - Checks if RHS is a blank identifier when LHS is not, in a `DEFINE` statement. When any of these conditions are met, the function throws an panics like go message.
Contributors' checklist... - [X] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [X] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- gnovm/pkg/gnolang/nodes.go | 2 +- gnovm/pkg/gnolang/preprocess.go | 24 +++++++++-- gnovm/pkg/gnolang/type_check.go | 3 ++ gnovm/pkg/gnolang/type_check_test.go | 57 ++++++++++++++++++++++++++ gnovm/tests/files/blankidentifier0.gno | 8 ++++ gnovm/tests/files/blankidentifier1.gno | 12 ++++++ gnovm/tests/files/blankidentifier2.gno | 9 ++++ gnovm/tests/files/blankidentifier3.gno | 10 +++++ gnovm/tests/files/blankidentifier4.gno | 9 ++++ gnovm/tests/files/blankidentifier5.gno | 12 ++++++ gnovm/tests/files/blankidentifier6.gno | 24 +++++++++++ gnovm/tests/files/blankidentifier7.gno | 13 ++++++ gnovm/tests/files/typeassert10.gno | 17 ++++++++ 13 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 gnovm/pkg/gnolang/type_check_test.go create mode 100644 gnovm/tests/files/blankidentifier0.gno create mode 100644 gnovm/tests/files/blankidentifier1.gno create mode 100644 gnovm/tests/files/blankidentifier2.gno create mode 100644 gnovm/tests/files/blankidentifier3.gno create mode 100644 gnovm/tests/files/blankidentifier4.gno create mode 100644 gnovm/tests/files/blankidentifier5.gno create mode 100644 gnovm/tests/files/blankidentifier6.gno create mode 100644 gnovm/tests/files/blankidentifier7.gno create mode 100644 gnovm/tests/files/typeassert10.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index dcc1ad41739..3368c7c7bde 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -441,7 +441,7 @@ type IndexExpr struct { // X[Index] Attributes X Expr // expression Index Expr // index expression - HasOK bool // if true, is form: `value, ok := [] + HasOK bool // if true, is form: `value, ok := []` } type SelectorExpr struct { // X.Sel diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 53c187342a6..6e82786b318 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -927,6 +927,14 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { switch n := n.(type) { // TRANS_LEAVE ----------------------- case *NameExpr: + if isBlankIdentifier(n) { + switch ftype { + case TRANS_ASSIGN_LHS, TRANS_RANGE_KEY, TRANS_RANGE_VALUE, TRANS_VAR_NAME: + // can use _ as value or type in these contexts + default: + panic("cannot use _ as value or type") + } + } // Validity: check that name isn't reserved. if isReservedName(n.Name) { panic(fmt.Sprintf( @@ -1645,7 +1653,8 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // Type assertions on the blank identifier are illegal. - if nx, ok := n.X.(*NameExpr); ok && string(nx.Name) == blankIdentifier { + + if isBlankIdentifier(n.X) { panic("cannot use _ as value or type") } @@ -3622,7 +3631,6 @@ func isNamedConversion(xt, t Type) bool { if t == nil { t = xt } - // no conversion case 1: the LHS is an interface _, c1 := t.(*InterfaceType) @@ -3634,7 +3642,6 @@ func isNamedConversion(xt, t Type) bool { _, oktt2 := xt.(*TypeType) c2 := oktt || oktt2 - // if !c1 && !c2 { // carve out above two cases // covert right to the type of left if one side is unnamed type and the other side is not @@ -4216,6 +4223,12 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { }) d.Path = last.GetPathForName(store, d.Name) case *ValueDecl: + // check for blank identifier in type + // e.g., `var x _` + if isBlankIdentifier(d.Type) { + panic("cannot use _ as value or type") + } + un = findUndefined(store, last, d.Type) if un != "" { return @@ -4258,6 +4271,11 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { case *StarExpr: t = &PointerType{} case *NameExpr: + // check for blank identifier in type + // e.g., `type T _` + if isBlankIdentifier(tx) { + panic("cannot use _ as value or type") + } if tv := last.GetValueRef(store, tx.Name, true); tv != nil { t = tv.GetType() if dt, ok := t.(*DeclaredType); ok { diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index e786bed683f..95b1c54ae4b 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -288,6 +288,9 @@ func checkAssignableTo(xt, dt Type, autoNative bool) error { } // case0 if xt == nil { // see test/files/types/eql_0f18 + if dt == nil || dt.Kind() == InterfaceKind { + return nil + } if !maybeNil(dt) { panic(fmt.Sprintf("invalid operation, nil can not be compared to %v", dt)) } diff --git a/gnovm/pkg/gnolang/type_check_test.go b/gnovm/pkg/gnolang/type_check_test.go new file mode 100644 index 00000000000..4b738961e0f --- /dev/null +++ b/gnovm/pkg/gnolang/type_check_test.go @@ -0,0 +1,57 @@ +package gnolang + +import "testing" + +func TestCheckAssignableTo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + xt Type + dt Type + autoNative bool + wantPanic bool + }{ + { + name: "nil to nil", + xt: nil, + dt: nil, + }, + { + name: "nil and interface", + xt: nil, + dt: &InterfaceType{}, + }, + { + name: "interface to nil", + xt: &InterfaceType{}, + dt: nil, + }, + { + name: "nil to non-nillable", + xt: nil, + dt: PrimitiveType(StringKind), + wantPanic: true, + }, + { + name: "interface to interface", + xt: &InterfaceType{}, + dt: &InterfaceType{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if tt.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("checkAssignableTo() did not panic, want panic") + } + }() + } + checkAssignableTo(tt.xt, tt.dt, tt.autoNative) + }) + } +} diff --git a/gnovm/tests/files/blankidentifier0.gno b/gnovm/tests/files/blankidentifier0.gno new file mode 100644 index 00000000000..a7447a22a6a --- /dev/null +++ b/gnovm/tests/files/blankidentifier0.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = _ +} + +// Error: +// main/files/blankidentifier0.gno:4:6: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier1.gno b/gnovm/tests/files/blankidentifier1.gno new file mode 100644 index 00000000000..9c93ff08f10 --- /dev/null +++ b/gnovm/tests/files/blankidentifier1.gno @@ -0,0 +1,12 @@ +package main + +type zilch interface{} + +func main() { + _ = zilch(nil) + + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/blankidentifier2.gno b/gnovm/tests/files/blankidentifier2.gno new file mode 100644 index 00000000000..a8d06cdabdc --- /dev/null +++ b/gnovm/tests/files/blankidentifier2.gno @@ -0,0 +1,9 @@ +package main + +func main() { + var i _ + println(i) +} + +// Error: +// main/files/blankidentifier2.gno:4:6: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier3.gno b/gnovm/tests/files/blankidentifier3.gno new file mode 100644 index 00000000000..aab1388c92d --- /dev/null +++ b/gnovm/tests/files/blankidentifier3.gno @@ -0,0 +1,10 @@ +package main + +type S _ + +func main() { + println("hey") +} + +// Error: +// main/files/blankidentifier3.gno:3:6: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier4.gno b/gnovm/tests/files/blankidentifier4.gno new file mode 100644 index 00000000000..214102e6c98 --- /dev/null +++ b/gnovm/tests/files/blankidentifier4.gno @@ -0,0 +1,9 @@ +package main + +func main() { + _ = 1 + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/blankidentifier5.gno b/gnovm/tests/files/blankidentifier5.gno new file mode 100644 index 00000000000..0de62bb77c3 --- /dev/null +++ b/gnovm/tests/files/blankidentifier5.gno @@ -0,0 +1,12 @@ +package main + +type foo struct{} + +var _ int = 10 + +func main() { + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/blankidentifier6.gno b/gnovm/tests/files/blankidentifier6.gno new file mode 100644 index 00000000000..a59ea246ad6 --- /dev/null +++ b/gnovm/tests/files/blankidentifier6.gno @@ -0,0 +1,24 @@ +package main + +type Animal interface { + Sound() string +} + +type Dog struct { + name string +} + +func (d Dog) Sound() string { + return "Woof!" +} + +func main() { + var a Animal = Dog{name: "Rex"} + + v := a.(_) + + println(v) +} + +// Error: +// main/files/blankidentifier6.gno:18:13: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier7.gno b/gnovm/tests/files/blankidentifier7.gno new file mode 100644 index 00000000000..4b3a50d2135 --- /dev/null +++ b/gnovm/tests/files/blankidentifier7.gno @@ -0,0 +1,13 @@ +package main + +import "strconv" + +var value int = 0 +func Foo(_ string) string { return strconv.Itoa(value) } + +func main() { + println(Foo("")) +} + +// Output: +// 0 diff --git a/gnovm/tests/files/typeassert10.gno b/gnovm/tests/files/typeassert10.gno new file mode 100644 index 00000000000..5876111b324 --- /dev/null +++ b/gnovm/tests/files/typeassert10.gno @@ -0,0 +1,17 @@ +package main + +type ( + nat []word + word int +) + +func main() { + var a nat + b := []word{0} + a = b + + println(a) +} + +// Output: +// (slice[(0 main.word)] main.nat) From 551bd66d9a42d24886e7966a1aabb456ba6b4e06 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:58:49 +0100 Subject: [PATCH 264/345] fix(gnodev): timestamp issue on reload (#2943) - [x] depends on #2941 resolve #1509 This PR addresses the timestamp issue on gnodev by implementing the `MetadataTX` changes from #2941. Timestamps will now be correctly handled for `Reload` and `import`/`export`. For `Reset`, the timestamp will be updated to the current time. cc @zivkovicmilos @thehowl #### ~TODOs~ - [x] test replays (I've only tested it manually)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/setup_node.go | 2 +- contribs/gnodev/pkg/dev/node.go | 22 ++- contribs/gnodev/pkg/dev/node_state.go | 2 +- contribs/gnodev/pkg/dev/node_test.go | 217 ++++++++++++++++++++++- contribs/gnodev/pkg/dev/packages.go | 34 ++-- 5 files changed, 247 insertions(+), 30 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 4b3619b4a7d..a2b1970d0ef 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -43,7 +43,7 @@ func setupDevNode( nodeConfig.InitialTxs[index] = nodeTx } - logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(nodeConfig.InitialTxs)) + logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) } return gnodev.NewDevNode(ctx, nodeConfig) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 9b3f838b8a0..e0ed64aad36 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "sync" + "time" "unicode" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" @@ -84,6 +85,9 @@ type Node struct { // keep track of number of loaded package to be able to skip them on restore loadedPackages int + // track starting time for genesis + startTime time.Time + // state initialState, state []gnoland.TxWithMetadata currentStateIndex int @@ -97,7 +101,8 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { return nil, fmt.Errorf("unable map pkgs list: %w", err) } - pkgsTxs, err := mpkgs.Load(DefaultFee) + startTime := time.Now() + pkgsTxs, err := mpkgs.Load(DefaultFee, startTime) if err != nil { return nil, fmt.Errorf("unable to load genesis packages: %w", err) } @@ -110,6 +115,7 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { pkgs: mpkgs, logger: cfg.Logger, loadedPackages: len(pkgsTxs), + startTime: startTime, state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), @@ -173,9 +179,10 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs)) for i, encodedTx := range b.Block.Data.Txs { + // fallback on std tx var tx std.Tx if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { - return nil, fmt.Errorf("unable to unmarshal amino tx, %w", unmarshalErr) + return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr) } txs[i] = gnoland.TxWithMetadata{ @@ -268,8 +275,11 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to stop the node: %w", err) } + // Reset starting time + startTime := time.Now() + // Generate a new genesis state based on the current packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -289,6 +299,7 @@ func (n *Node) Reset(ctx context.Context) error { n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) + n.startTime = startTime n.emitter.Emit(&events.Reset{}) return nil } @@ -358,7 +369,6 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState) initialTxs := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages - state := append([]gnoland.TxWithMetadata{}, initialTxs...) lastBlock := n.getLatestBlockNumber() @@ -397,7 +407,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - txs, err := n.pkgs.Load(DefaultFee) + txs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -413,7 +423,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 7504580b333..73362a5f1c8 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -84,7 +84,7 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 11b0a2090d7..e05e5a996fa 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -2,9 +2,11 @@ package dev import ( "context" + "encoding/json" "os" "path/filepath" "testing" + "time" mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" @@ -15,8 +17,10 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" + tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -221,6 +225,191 @@ func Render(_ string) string { return str } assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) } +func TestTxTimestampRecover(t *testing.T) { + const ( + // foo package + foobarGnoMod = "module gno.land/r/dev/foo\n" + fooFile = `package foo +import ( + "strconv" + "strings" + "time" +) + +var times = []time.Time{ + time.Now(), // Evaluate at genesis +} + +func SpanTime() { + times = append(times, time.Now()) +} + +func Render(_ string) string { + var strs strings.Builder + + strs.WriteRune('[') + for i, t := range times { + if i > 0 { + strs.WriteRune(',') + } + strs.WriteString(strconv.Itoa(int(t.UnixNano()))) + } + strs.WriteRune(']') + + return strs.String() +} +` + ) + + // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + parseJSONTimesList := func(t *testing.T, render string) []time.Time { + t.Helper() + + var times []time.Time + var nanos []int64 + + err := json.Unmarshal([]byte(render), &nanos) + require.NoError(t, err) + + for _, nano := range nanos { + sec, nsec := nano/int64(time.Second), nano%int64(time.Second) + times = append(times, time.Unix(sec, nsec)) + } + + return times + } + + // Generate package foo + foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + + // Call NewDevNode with no package should work + cfg := createDefaultTestingNodeConfig(foopkg) + + // XXX(gfanton): Setting this to `false` somehow makes the time block + // drift from the time spanned by the VM. + cfg.TMConfig.Consensus.SkipTimeoutCommit = false + cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond + cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond + cfg.TMConfig.Consensus.CreateEmptyBlocks = true + + node, emitter := newTestingDevNodeWithConfig(t, cfg) + + // We need to make sure that blocks are separated by at least 1 second + // (minimal time between blocks). We can ensure this by listening for + // new blocks and comparing timestamps + cc := make(chan types.EventNewBlock) + node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { + newBlock, ok := evt.(types.EventNewBlock) + if !ok { + return + } + + select { + case cc <- newBlock: + default: + } + }) + + // wait for first block for reference + var refHeight, refTimestamp int64 + + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case res := <-cc: + refTimestamp = res.Block.Time.Unix() + refHeight = res.Block.Height + } + + // number of span to process + const nevents = 3 + + // Span multiple time + for i := 0; i < nevents; i++ { + t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) + for { + var block types.EventNewBlock + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case block = <-cc: + } + + t.Logf("got a block height(%d) and unix(%d)", + block.Block.Height, block.Block.Time.Unix()) + + // Ensure we consume every block before tx block + if refHeight >= block.Block.Height { + continue + } + + // Ensure new block timestamp is before previous reference timestamp + if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { + refTimestamp = newRefTimestamp + break // break the loop + } + } + + t.Logf("found a valid block(%d)! continue", refHeight) + + // Span a new time + msg := vm.MsgCall{ + PkgPath: "gno.land/r/dev/foo", + Func: "SpanTime", + } + + res, err := testingCallRealm(t, node, msg) + + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Set the new height from the tx as reference + refHeight = res.Height + } + + // Render JSON times list + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + // Parse times list + timesList1 := parseJSONTimesList(t, render) + t.Logf("list of times: %+v", timesList1) + + // Ensure times are correctly expending. + for i, t2 := range timesList1 { + if i == 0 { + continue + } + + t1 := timesList1[i-1] + require.Greater(t, t2.UnixNano(), t1.UnixNano()) + } + + // Reload the node + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + + // Fetch time list again from render + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + timesList2 := parseJSONTimesList(t, render) + + // Times list should be identical from the orignal list + require.Len(t, timesList2, len(timesList1)) + for i := 0; i < len(timesList1); i++ { + t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() + assert.Equal(t, t1nsec, t2nsec, + "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) + } +} + func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { t.Helper() @@ -285,25 +474,37 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { } } +func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { + cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg.PackagesPathList = pkgslist + return cfg +} + func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) { t.Helper() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - logger := log.NewTestingLogger(t) + cfg := createDefaultTestingNodeConfig(pkgslist...) + return newTestingDevNodeWithConfig(t, cfg) +} + +func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + logger := log.NewTestingLogger(t) emitter := &mock.ServerEmitter{} - // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesPathList = pkgslist cfg.Emitter = emitter cfg.Logger = logger + node, err := NewDevNode(ctx, cfg) require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(pkgslist)) + assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList)) - t.Cleanup(func() { node.Close() }) + t.Cleanup(func() { + node.Close() + cancel() + }) return node, emitter } diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 7ee628ce39e..cccbf316525 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "path/filepath" + "time" "github.com/gnolang/gno/contribs/gnodev/pkg/address" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -119,7 +120,7 @@ func (pm PackagesMap) toList() gnomod.PkgList { return list } -func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { +func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetadata, error) { pkgs := pm.toList() sorted, err := pkgs.Sort() @@ -128,8 +129,8 @@ func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { } nonDraft := sorted.GetNonDraftPkgs() - txs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) + metatxs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) for _, modPkg := range nonDraft { pkg := pm[modPkg.Dir] if pkg.Creator.IsZero() { @@ -143,22 +144,27 @@ func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { } // Create transaction - tx := gnoland.TxWithMetadata{ - Tx: std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: pkg.Creator, - Deposit: pkg.Deposit, - Package: memPkg, - }, + tx := std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: pkg.Creator, + Deposit: pkg.Deposit, + Package: memPkg, }, }, } - tx.Tx.Signatures = make([]std.Signature, len(tx.Tx.GetSigners())) - txs = append(txs, tx) + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + metatx := gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: start.Unix(), + }, + } + + metatxs = append(metatxs, metatx) } - return txs, nil + return metatxs, nil } From c64e0df5a6beaeeab29f60c69537087f91b49527 Mon Sep 17 00:00:00 2001 From: Sergio Maria Matone Date: Wed, 27 Nov 2024 13:15:30 +0100 Subject: [PATCH 265/345] feat: Adding an official Gnocontribs image in release process (#3219) --- .github/goreleaser.yaml | 103 ++++++++++++++++++++++++++++------------ Dockerfile.release | 40 +++++++++------- 2 files changed, 96 insertions(+), 47 deletions(-) diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml index b5fb07d0578..71a8ba98745 100644 --- a/.github/goreleaser.yaml +++ b/.github/goreleaser.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json project_name: gno version: 2 @@ -86,6 +87,8 @@ builds: goarm: - "6" - "7" + # Gno Contribs + # NOTE: Contribs binary will be added in a single docker image below: gnocontribs - id: gnobro dir: ./contribs/gnodev/cmd/gnobro binary: gnobro @@ -101,6 +104,21 @@ builds: goarm: - "6" - "7" + - id: gnogenesis + dir: ./contribs/gnogenesis + binary: gnogenesis + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - "6" + - "7" gomod: proxy: true @@ -300,6 +318,7 @@ dockers: - gno.land/genesis/genesis_txs.jsonl - examples - gnovm/stdlibs + # gnokey - use: buildx dockerfile: Dockerfile.release @@ -504,73 +523,97 @@ dockers: ids: - gnofaucet - # gnobro + # gnocontribs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: amd64 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: arm64 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/arm64/v8" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: arm goarm: 6 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/arm/v6" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: arm goarm: 7 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/arm/v7" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs docker_manifests: # https://goreleaser.com/customization/docker_manifest/ @@ -645,19 +688,19 @@ docker_manifests: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7 - # gnobro - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv7 + # gnocontribs + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7 docker_signs: - cmd: cosign diff --git a/Dockerfile.release b/Dockerfile.release index 481100c85c3..c7bb1b582ed 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -11,11 +11,11 @@ CMD [ "" ] ## ghcr.io/gnolang/gno/gnoland FROM base as gnoland -COPY ./gnoland /usr/bin/gnoland -COPY ./examples /gnoroot/examples/ -COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ -COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt -COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +COPY ./gnoland /usr/bin/gnoland +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl EXPOSE 26656 26657 @@ -44,21 +44,27 @@ COPY ./gnofaucet /usr/bin/gnofaucet EXPOSE 5050 ENTRYPOINT [ "/usr/bin/gnofaucet" ] -# -## ghcr.io/gnolang/gno/gnobro -FROM base as gnobro - -COPY ./gnobro /usr/bin/gnobro -EXPOSE 22 -ENTRYPOINT [ "/usr/bin/gnobro" ] - # ## ghcr.io/gnolang/gno FROM base as gno -COPY ./gno /usr/bin/gno -COPY ./examples /gnoroot/examples/ -COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ -COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ +COPY ./gno /usr/bin/gno +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ ENTRYPOINT [ "/usr/bin/gno" ] + +# +## ghcr.io/gnolang/gnocontribs +FROM base as gnocontribs + +COPY ./gnobro /usr/bin/gnobro +COPY ./gnogenesis /usr/bin/gnogenesis +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +EXPOSE 22 + +ENTRYPOINT [ "/bin/sh", "-c" ] From c1ae90b50a0346316c7f106e96b897f502f02824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Wed, 27 Nov 2024 13:19:24 +0100 Subject: [PATCH 266/345] chore: add balances restore to the portal loop (#3220) ## Description This PR allows for a balances restore for the portal loop.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- misc/loop/cmd/snapshotter.go | 1 + misc/loop/scripts/pull-gh.sh | 18 ++++++++++++++++++ misc/loop/scripts/start.sh | 3 +++ 3 files changed, 22 insertions(+) diff --git a/misc/loop/cmd/snapshotter.go b/misc/loop/cmd/snapshotter.go index 2dda5d568d9..0173f9aad03 100644 --- a/misc/loop/cmd/snapshotter.go +++ b/misc/loop/cmd/snapshotter.go @@ -150,6 +150,7 @@ func (s snapshotter) startPortalLoopContainer(ctx context.Context) (*types.Conta Env: []string{ "MONIKER=the-portal-loop", "GENESIS_BACKUP_FILE=/backups/backup.jsonl", + "GENESIS_BALANCES_FILE=/backups/balances.jsonl", }, Entrypoint: []string{"/scripts/start.sh"}, ExposedPorts: nat.PortSet{ diff --git a/misc/loop/scripts/pull-gh.sh b/misc/loop/scripts/pull-gh.sh index efbb360d551..55ee0f7762c 100755 --- a/misc/loop/scripts/pull-gh.sh +++ b/misc/loop/scripts/pull-gh.sh @@ -6,6 +6,10 @@ TMP_DIR=temp-tx-exports # that the portal loop use when looping (generating the genesis) MASTER_BACKUP_FILE="backup.jsonl" +# The master balances file will contain the ultimate balances +# backup that the portal loop uses when looping (generating the genesis) +MASTER_BALANCES_FILE="balances.jsonl" + # Clones the portal loop backups subdirectory, located in BACKUPS_REPO (tx-exports) pullGHBackups () { BACKUPS_REPO=https://github.com/gnolang/tx-exports.git @@ -32,8 +36,10 @@ pullGHBackups # Combine the pulled backups into a single backup file TXS_BACKUPS_PREFIX="backup_portal_loop_txs_" +BALANCES_BACKUP_NAME="backup_portal_loop_balances.jsonl" find . -type f -name "${TXS_BACKUPS_PREFIX}*.jsonl" | sort | xargs cat > "temp_$MASTER_BACKUP_FILE" +find . -type f -name "${BALANCES_BACKUP_NAME}" | sort | xargs cat > "temp_$MASTER_BALANCES_FILE" BACKUPS_DIR="../backups" TIMESTAMP=$(date +%s) @@ -47,10 +53,22 @@ if [ -e "$BACKUPS_DIR/$MASTER_BACKUP_FILE" ]; then echo "Renamed $MASTER_BACKUP_FILE to ${MASTER_BACKUP_FILE}-legacy-$TIMESTAMP" fi +# Check if the master balances backup file already exists +if [ -e "$BACKUPS_DIR/$MASTER_BALANCES_FILE" ]; then + # Back up the existing master txs file + echo "Master balances backup file exists, backing up..." + mv "$BACKUPS_DIR/$MASTER_BALANCES_FILE" "$BACKUPS_DIR/${MASTER_BALANCES_FILE}-legacy-$TIMESTAMP" + + echo "Renamed $MASTER_BALANCES_FILE to ${MASTER_BALANCES_FILE}-legacy-$TIMESTAMP" +fi + # Use the GitHub state as the canonical backup mv "temp_$MASTER_BACKUP_FILE" "$BACKUPS_DIR/$MASTER_BACKUP_FILE" echo "Moved temp_$MASTER_BACKUP_FILE to $BACKUPS_DIR/$MASTER_BACKUP_FILE" +mv "temp_$MASTER_BALANCES_FILE" "$BACKUPS_DIR/$MASTER_BALANCES_FILE" +echo "Moved temp_$MASTER_BALANCES_FILE to $BACKUPS_DIR/$MASTER_BALANCES_FILE" + # Clean up the temporary directory cd .. rm -rf $TMP_DIR diff --git a/misc/loop/scripts/start.sh b/misc/loop/scripts/start.sh index 6dd57b2c041..db36de39f2a 100755 --- a/misc/loop/scripts/start.sh +++ b/misc/loop/scripts/start.sh @@ -7,12 +7,15 @@ RPC_LADDR=${RPC_LADDR:-"tcp://0.0.0.0:26657"} CHAIN_ID=${CHAIN_ID:-"portal-loop"} GENESIS_BACKUP_FILE=${GENESIS_BACKUP_FILE:-""} +GENESIS_BALANCES_FILE=${GENESIS_BALANCES_FILE:-""} SEEDS=${SEEDS:-""} PERSISTENT_PEERS=${PERSISTENT_PEERS:-""} echo "" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +echo "" >> /gnoroot/gno.land/genesis/genesis_balances.jsonl cat "${GENESIS_BACKUP_FILE}" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +cat "${GENESIS_BALANCES_FILE}" >> /gnoroot/gno.land/genesis/genesis_balances.jsonl # Initialize the secrets gnoland secrets init From e3e59c268407e1e80a2e8e41d28f058936f3e4e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:19:25 +0100 Subject: [PATCH 267/345] chore(deps): bump the actions group across 1 directory with 2 updates (#3214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions group with 2 updates in the / directory: [tj-actions/changed-files](https://github.com/tj-actions/changed-files) and [anchore/sbom-action](https://github.com/anchore/sbom-action). Updates `tj-actions/changed-files` from 41 to 45
Release notes

Sourced from tj-actions/changed-files's releases.

v45

Changes in v45.0.4

What's Changed

Full Changelog: https://github.com/tj-actions/changed-files/compare/v45...v45.0.4


Changes in v45.0.3

What's Changed

... (truncated)

Changelog

Sourced from tj-actions/changed-files's changelog.

Changelog

45.0.4 - (2024-11-05)

🚀 Features

  • Prevent ignore files warning (#2318) (1f772e9) - (Tonye Jack)

🐛 Bug Fixes

  • deps: Update dependency @​actions/core to v1.11.1 (4d0aab9) - (renovate[bot])

➕ Add

  • Added missing changes and modified dist assets. (9d7201d) - (GitHub Action)
  • Added missing changes and modified dist assets. (0104c75) - (GitHub Action)

📝 Other

⚙️ Miscellaneous Tasks

  • deps: Update dependency eslint-plugin-jest to v28.9.0 (4edd678) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.9.0 (f082558) - (renovate[bot])
  • deps: Lock file maintenance (92c02a0) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.7 (b702211) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.6 (435fd74) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.5 (0626fa3) - (renovate[bot])
  • deps: Update dependency @​types/lodash to v4.17.13 (8817a79) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.4 (5417491) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.2 (84ef162) - (renovate[bot])
  • deps: Lock file maintenance (b672a51) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.1 (678cdc2) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.0 (27b7bbb) - (renovate[bot])
  • deps: Update actions/setup-node action to v4.1.0 (8361072) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.9 (21acf46) - (renovate[bot])
  • deps: Update dependency @​types/jest to v29.5.14 (f356b3c) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.8 (66275de) - (renovate[bot])
  • deps: Lock file maintenance (a16702b) - (renovate[bot])
  • deps: Update dependency @​types/lodash to v4.17.12 (aa11897) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.7 (6513fe1) - (renovate[bot])
  • deps: Update dependency @​types/lodash to v4.17.11 (45e0c78) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.6 (a949a83) - (renovate[bot])
  • deps: Lock file maintenance (f93ff33) - (renovate[bot])
  • deps: Update dependency typescript to v5.6.3 (729c704) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.5 (2009d44) - (renovate[bot])
  • deps: Lock file maintenance (b693fc2) - (renovate[bot])

... (truncated)

Commits
  • 4edd678 chore(deps): update dependency eslint-plugin-jest to v28.9.0
  • f082558 chore(deps): update dependency @​types/node to v22.9.0
  • 92c02a0 chore(deps): lock file maintenance
  • b702211 chore(deps): update dependency @​types/node to v22.8.7
  • 435fd74 chore(deps): update dependency @​types/node to v22.8.6
  • 0626fa3 chore(deps): update dependency @​types/node to v22.8.5
  • 8817a79 chore(deps): update dependency @​types/lodash to v4.17.13
  • 5417491 chore(deps): update dependency @​types/node to v22.8.4
  • 84ef162 chore(deps): update dependency @​types/node to v22.8.2
  • b672a51 chore(deps): lock file maintenance
  • Additional commits viewable in compare view

Updates `anchore/sbom-action` from 0.17.7 to 0.17.8
Release notes

Sourced from anchore/sbom-action's releases.

v0.17.8

Changes in v0.17.8

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Morgan Bazalgette --- .github/workflows/genesis-verify.yml | 3 ++- .github/workflows/releaser-master.yml | 2 +- .github/workflows/releaser-nightly.yml | 2 +- .github/workflows/releaser.yml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml index 6c9955b7178..f870cd0658c 100644 --- a/.github/workflows/genesis-verify.yml +++ b/.github/workflows/genesis-verify.yml @@ -6,6 +6,7 @@ on: - master paths: - "misc/deployments/**/genesis.json" + - ".github/workflows/genesis-verify.yml" jobs: verify: @@ -20,7 +21,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v41 + uses: tj-actions/changed-files@v45 with: files: "misc/deployments/${{ matrix.testnet }}/genesis.json" diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index eb5698e9d8f..36a709a242a 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -27,7 +27,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.7 + - uses: anchore/sbom-action/download-syft@v0.17.8 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index aed56526a2f..e9a5c15a22d 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -24,7 +24,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.7 + - uses: anchore/sbom-action/download-syft@v0.17.8 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index aeda7ed2c7e..d33432bd16d 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -24,7 +24,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.7 + - uses: anchore/sbom-action/download-syft@v0.17.8 - uses: docker/login-action@v3 with: From 8125041024044caf5f7f61a4067e700f533e077d Mon Sep 17 00:00:00 2001 From: Kristov Atlas <7227529+kristovatlas@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:20:42 -0600 Subject: [PATCH 268/345] docs: Add security policy (#3217)
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..8380267dacf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +The gno.land community strives to contribute toward the security of our ecosystem through internal security practices, and by working with external security researchers from the community. + +## Reporting a Vulnerability +If you've identified a vulnerability, please report it through one of the following venues: + +* Submit an advisory through GitHub: https://github.com/gnolang/gno/security/advisories/new +* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details. +* A security bug bounty platform for gno.land will be available Soonᵀᴹ. You will need to report via our bug bounty platform in order to be eligible for rewards. + +We will respond within 3 business days to all received reports. + +Thank you for helping to keep our ecosystem safe! From 310938d8a737403f6d4d695cb3342db8f7e6ba9b Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:53:04 +0100 Subject: [PATCH 269/345] ci: add a github bot to support advanced PR review workflows (#3037) This pull request aims to add a bot that extends GitHub's functionalities like codeowners file and other merge protection mechanisms. Interaction with the bot is done via a comment. You can test it on the demo repo here : https://github.com/GnoCheckBot/demo/pull/1 Fixes #1007 Related to #1466, #2788 - The `config.go` file contains all the conditions and requirements in an 'If - Then' format. ```go // Automatic check { Description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", If: c.And( c.FileChanged(gh, "tm2"), c.BaseBranch("main"), ), Then: r.And( r.Or( r.ReviewByTeamMembers(gh, "eu", 1), r.AuthorInTeam(gh, "eu"), ), r.Or( r.ReviewByTeamMembers(gh, "us", 1), r.AuthorInTeam(gh, "us"), ), ), } ``` - There are two types of checks: some are automatic and managed by the bot (like the one above), while others are manual and need to be verified by a specific org team member (like the one below). If no team is specified, anyone with comment editing permission can check it. ```go // Manual check { Description: "The documentation is accurate and relevant", If: c.FileChanged(gh, `.*\.md`), Teams: []string{ "tech-staff", "devrels", }, }, ``` - The conditions (If) allow checking, among other things, who the author is, who is assigned, what labels are applied, the modified files, etc. The list is available in the `condition` folder. - The requirements (Then) allow, among other things, assigning a member, verifying that a review is done by a specific user, applying a label, etc. (List in `requirement` folder). - A PR Check (the icon at the bottom with all the CI checks) will remain orange/pending until all checks are validated, after which it will turn green. Screenshot 2024-11-05 at 18 37 34 - The Github Actions workflow associated with the bot ensures that PRs are processed concurrently, while ensuring that the same PR is not processed by two runners at the same time. - We can manually process a PR by launching the workflow directly from the [GitHub Actions interface](https://github.com/GnoCheckBot/demo/actions/workflows/bot.yml). Screenshot 2024-11-06 at 01 36 42 #### To do - [x] implement base version of the bot - [x] cleanup code / comments - [x] setup a demo repo - [x] add debug printing on dry run - [x] add some tests on requirements and conditions
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- .github/workflows/bot.yml | 79 +++++ contribs/github-bot/README.md | 48 +++ contribs/github-bot/check.go | 246 +++++++++++++++ contribs/github-bot/comment.go | 282 +++++++++++++++++ contribs/github-bot/comment.tmpl | 51 +++ contribs/github-bot/comment_test.go | 164 ++++++++++ contribs/github-bot/config.go | 100 ++++++ contribs/github-bot/go.mod | 28 ++ contribs/github-bot/go.sum | 38 +++ contribs/github-bot/internal/client/client.go | 293 ++++++++++++++++++ .../internal/conditions/assignee.go | 66 ++++ .../internal/conditions/assignee_test.go | 100 ++++++ .../github-bot/internal/conditions/author.go | 60 ++++ .../internal/conditions/author_test.go | 93 ++++++ .../github-bot/internal/conditions/boolean.go | 98 ++++++ .../internal/conditions/boolean_test.go | 96 ++++++ .../github-bot/internal/conditions/branch.go | 49 +++ .../internal/conditions/branch_test.go | 49 +++ .../internal/conditions/condition.go | 12 + .../internal/conditions/constant.go | 34 ++ .../internal/conditions/constant_test.go | 25 ++ .../github-bot/internal/conditions/file.go | 58 ++++ .../internal/conditions/file_test.go | 68 ++++ .../github-bot/internal/conditions/label.go | 34 ++ .../internal/conditions/label_test.go | 48 +++ contribs/github-bot/internal/logger/action.go | 43 +++ contribs/github-bot/internal/logger/logger.go | 40 +++ contribs/github-bot/internal/logger/noop.go | 27 ++ .../github-bot/internal/logger/terminal.go | 55 ++++ contribs/github-bot/internal/params/params.go | 118 +++++++ contribs/github-bot/internal/params/prlist.go | 49 +++ .../internal/requirements/assignee.go | 53 ++++ .../internal/requirements/assignee_test.go | 72 +++++ .../internal/requirements/author.go | 39 +++ .../internal/requirements/author_test.go | 93 ++++++ .../internal/requirements/boolean.go | 98 ++++++ .../internal/requirements/boolean_test.go | 96 ++++++ .../internal/requirements/branch.go | 53 ++++ .../internal/requirements/branch_test.go | 62 ++++ .../internal/requirements/constant.go | 34 ++ .../internal/requirements/constant_test.go | 25 ++ .../github-bot/internal/requirements/label.go | 53 ++++ .../internal/requirements/label_test.go | 79 +++++ .../internal/requirements/maintainer.go | 25 ++ .../internal/requirements/maintener_test.go | 34 ++ .../internal/requirements/requirement.go | 12 + .../internal/requirements/reviewer.go | 156 ++++++++++ .../internal/requirements/reviewer_test.go | 215 +++++++++++++ contribs/github-bot/internal/utils/actions.go | 45 +++ .../github-bot/internal/utils/actions_test.go | 43 +++ .../github-bot/internal/utils/github_const.go | 14 + contribs/github-bot/internal/utils/testing.go | 21 ++ contribs/github-bot/internal/utils/tree.go | 24 ++ contribs/github-bot/main.go | 26 ++ contribs/github-bot/matrix.go | 111 +++++++ contribs/github-bot/matrix_test.go | 248 +++++++++++++++ 56 files changed, 4282 insertions(+) create mode 100644 .github/workflows/bot.yml create mode 100644 contribs/github-bot/README.md create mode 100644 contribs/github-bot/check.go create mode 100644 contribs/github-bot/comment.go create mode 100644 contribs/github-bot/comment.tmpl create mode 100644 contribs/github-bot/comment_test.go create mode 100644 contribs/github-bot/config.go create mode 100644 contribs/github-bot/go.mod create mode 100644 contribs/github-bot/go.sum create mode 100644 contribs/github-bot/internal/client/client.go create mode 100644 contribs/github-bot/internal/conditions/assignee.go create mode 100644 contribs/github-bot/internal/conditions/assignee_test.go create mode 100644 contribs/github-bot/internal/conditions/author.go create mode 100644 contribs/github-bot/internal/conditions/author_test.go create mode 100644 contribs/github-bot/internal/conditions/boolean.go create mode 100644 contribs/github-bot/internal/conditions/boolean_test.go create mode 100644 contribs/github-bot/internal/conditions/branch.go create mode 100644 contribs/github-bot/internal/conditions/branch_test.go create mode 100644 contribs/github-bot/internal/conditions/condition.go create mode 100644 contribs/github-bot/internal/conditions/constant.go create mode 100644 contribs/github-bot/internal/conditions/constant_test.go create mode 100644 contribs/github-bot/internal/conditions/file.go create mode 100644 contribs/github-bot/internal/conditions/file_test.go create mode 100644 contribs/github-bot/internal/conditions/label.go create mode 100644 contribs/github-bot/internal/conditions/label_test.go create mode 100644 contribs/github-bot/internal/logger/action.go create mode 100644 contribs/github-bot/internal/logger/logger.go create mode 100644 contribs/github-bot/internal/logger/noop.go create mode 100644 contribs/github-bot/internal/logger/terminal.go create mode 100644 contribs/github-bot/internal/params/params.go create mode 100644 contribs/github-bot/internal/params/prlist.go create mode 100644 contribs/github-bot/internal/requirements/assignee.go create mode 100644 contribs/github-bot/internal/requirements/assignee_test.go create mode 100644 contribs/github-bot/internal/requirements/author.go create mode 100644 contribs/github-bot/internal/requirements/author_test.go create mode 100644 contribs/github-bot/internal/requirements/boolean.go create mode 100644 contribs/github-bot/internal/requirements/boolean_test.go create mode 100644 contribs/github-bot/internal/requirements/branch.go create mode 100644 contribs/github-bot/internal/requirements/branch_test.go create mode 100644 contribs/github-bot/internal/requirements/constant.go create mode 100644 contribs/github-bot/internal/requirements/constant_test.go create mode 100644 contribs/github-bot/internal/requirements/label.go create mode 100644 contribs/github-bot/internal/requirements/label_test.go create mode 100644 contribs/github-bot/internal/requirements/maintainer.go create mode 100644 contribs/github-bot/internal/requirements/maintener_test.go create mode 100644 contribs/github-bot/internal/requirements/requirement.go create mode 100644 contribs/github-bot/internal/requirements/reviewer.go create mode 100644 contribs/github-bot/internal/requirements/reviewer_test.go create mode 100644 contribs/github-bot/internal/utils/actions.go create mode 100644 contribs/github-bot/internal/utils/actions_test.go create mode 100644 contribs/github-bot/internal/utils/github_const.go create mode 100644 contribs/github-bot/internal/utils/testing.go create mode 100644 contribs/github-bot/internal/utils/tree.go create mode 100644 contribs/github-bot/main.go create mode 100644 contribs/github-bot/matrix.go create mode 100644 contribs/github-bot/matrix_test.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml new file mode 100644 index 00000000000..975f39f29dc --- /dev/null +++ b/.github/workflows/bot.yml @@ -0,0 +1,79 @@ +name: GitHub Bot + +on: + # Watch for changes on PR state, assignees, labels, head branch and draft/ready status + pull_request_target: + types: + - assigned + - unassigned + - labeled + - unlabeled + - opened + - reopened + - synchronize # PR head updated + - converted_to_draft + - ready_for_review + + # Watch for changes on PR comment + issue_comment: + types: [created, edited, deleted] + + # Manual run from GitHub Actions interface + workflow_dispatch: + inputs: + pull-request-list: + description: "PR(s) to process: specify 'all' or a comma separated list of PR numbers, e.g. '42,1337,7890'" + required: true + default: all + type: string + +jobs: + # This job creates a matrix of PR numbers based on the inputs from the various + # events that can trigger this workflow so that the process-pr job below can + # handle the parallel processing of the pull-requests + define-prs-matrix: + name: Define PRs matrix + # Prevent bot from retriggering itself + if: ${{ github.actor != vars.GH_BOT_LOGIN }} + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }} + + steps: + - name: Generate matrix from event + id: pr-numbers + working-directory: contribs/github-bot + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: go run . matrix >> "$GITHUB_OUTPUT" + + # This job processes each pull request in the matrix individually while ensuring + # that a same PR cannot be processed concurrently by mutliple runners + process-pr: + name: Process PR + needs: define-prs-matrix + runs-on: ubuntu-latest + strategy: + matrix: + # Run one job for each PR to process + pr-number: ${{ fromJSON(needs.define-prs-matrix.outputs.pr-numbers) }} + concurrency: + # Prevent running concurrent jobs for a given PR number + group: ${{ matrix.pr-number }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run GitHub Bot + working-directory: contribs/github-bot + env: + GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }} + run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md new file mode 100644 index 00000000000..e3cc12fe01a --- /dev/null +++ b/contribs/github-bot/README.md @@ -0,0 +1,48 @@ +# GitHub Bot + +## Overview + +The GitHub Bot is designed to automate and streamline the process of managing pull requests. It can automate certain tasks such as requesting reviews, assigning users or applying labels, but it also ensures that certain requirements are satisfied before allowing a pull request to be merged. Interaction with the bot occurs through a comment on the pull request, providing all the information to the user and allowing them to check boxes for the manual validation of certain rules. + +## How It Works + +### Configuration + +The bot operates by defining a set of rules that are evaluated against each pull request passed as parameter. These rules are categorized into automatic and manual checks: + +- **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members. +- **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files. + +The bot configuration is defined in Go and is located in the file [config.go](./config.go). + +### GitHub Token + +For the bot to make requests to the GitHub API, it needs a Personal Access Token. The fine-grained permissions to assign to the token for the bot to function are: + +- `pull_requests` scope to read is the bare minimum to run the bot in dry-run mode +- `pull_requests` scope to write to be able to update bot comment, assign user, apply label and request review +- `contents` scope to read to be able to check if the head branch is up to date with another one +- `commit_statuses` scope to write to be able to update pull request bot status check + +## Usage + +```bash +> go install github.com/gnolang/gno/contribs/github-bot@latest +// (go: downloading ...) + +> github-bot --help +USAGE + github-bot [flags] + +This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. +A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. + +FLAGS + -dry-run=false print if pull request requirements are satisfied without updating anything on GitHub + -owner ... owner of the repo to process, if empty, will be retrieved from GitHub Actions context + -pr-all=false process all opened pull requests + -pr-numbers ... pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context + -repo ... repo to process, if empty, will be retrieved from GitHub Actions context + -timeout 0s timeout after which the bot execution is interrupted + -verbose=false set logging level to debug +``` diff --git a/contribs/github-bot/check.go b/contribs/github-bot/check.go new file mode 100644 index 00000000000..8019246d27c --- /dev/null +++ b/contribs/github-bot/check.go @@ -0,0 +1,246 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "sync/atomic" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + p "github.com/gnolang/gno/contribs/github-bot/internal/params" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/google/go-github/v64/github" + "github.com/sethvargo/go-githubactions" + "github.com/xlab/treeprint" +) + +func newCheckCmd() *commands.Command { + params := &p.Params{} + + return commands.NewCommand( + commands.Metadata{ + Name: "check", + ShortUsage: "github-bot check [flags]", + ShortHelp: "checks requirements for a pull request to be merged", + LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.", + }, + params, + func(_ context.Context, _ []string) error { + params.ValidateFlags() + return execCheck(params) + }, + ) +} + +func execCheck(params *p.Params) error { + // Create context with timeout if specified in the parameters. + ctx := context.Background() + if params.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), params.Timeout) + defer cancel() + } + + // Init GitHub API client. + gh, err := client.New(ctx, params) + if err != nil { + return fmt.Errorf("comment update handling failed: %w", err) + } + + // Get GitHub Actions context to retrieve comment update. + actionCtx, err := githubactions.Context() + if err != nil { + gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) + return nil + } + + // Handle comment update, if any. + if err := handleCommentUpdate(gh, actionCtx); errors.Is(err, errTriggeredByBot) { + return nil // Ignore if this run was triggered by a previous run. + } else if err != nil { + return fmt.Errorf("comment update handling failed: %w", err) + } + + // Retrieve a slice of pull requests to process. + var prs []*github.PullRequest + + // If requested, retrieve all open pull requests. + if params.PRAll { + prs, err = gh.ListPR(utils.PRStateOpen) + if err != nil { + return fmt.Errorf("unable to list all PR: %w", err) + } + } else { + // Otherwise, retrieve only specified pull request(s) + // (flag or GitHub Action context). + prs = make([]*github.PullRequest, len(params.PRNums)) + for i, prNum := range params.PRNums { + pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) + if err != nil { + return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) + } + prs[i] = pr + } + } + + return processPRList(gh, prs) +} + +func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { + if len(prs) > 1 { + prNums := make([]int, len(prs)) + for i, pr := range prs { + prNums[i] = pr.GetNumber() + } + + gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums) + } + + // Process all pull requests in parallel. + autoRules, manualRules := config(gh) + var wg sync.WaitGroup + + // Used in dry-run mode to log cleanly from different goroutines. + logMutex := sync.Mutex{} + + // Used in regular-run mode to return an error if one PR processing failed. + var failed atomic.Bool + + for _, pr := range prs { + wg.Add(1) + go func(pr *github.PullRequest) { + defer wg.Done() + commentContent := CommentContent{} + commentContent.allSatisfied = true + + // Iterate over all automatic rules in config. + for _, autoRule := range autoRules { + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) + + // Check if conditions of this rule are met by this PR. + if !autoRule.ifC.IsMet(pr, ifDetails) { + continue + } + + c := AutoContent{Description: autoRule.description, Satisfied: false} + thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail)) + + // Check if requirements of this rule are satisfied by this PR. + if autoRule.thenR.IsSatisfied(pr, thenDetails) { + thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) + c.Satisfied = true + } else { + commentContent.allSatisfied = false + } + + c.ConditionDetails = ifDetails.String() + c.RequirementDetails = thenDetails.String() + commentContent.AutoRules = append(commentContent.AutoRules, c) + } + + // Retrieve manual check states. + checks := make(map[string]manualCheckDetails) + if comment, err := gh.GetBotComment(pr.GetNumber()); err == nil { + checks = getCommentManualChecks(comment.GetBody()) + } + + // Iterate over all manual rules in config. + for _, manualRule := range manualRules { + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) + + // Check if conditions of this rule are met by this PR. + if !manualRule.ifC.IsMet(pr, ifDetails) { + continue + } + + // Get check status from current comment, if any. + checkedBy := "" + check, ok := checks[manualRule.description] + if ok { + checkedBy = check.checkedBy + } + + commentContent.ManualRules = append( + commentContent.ManualRules, + ManualContent{ + Description: manualRule.description, + ConditionDetails: ifDetails.String(), + CheckedBy: checkedBy, + Teams: manualRule.teams, + }, + ) + + if checkedBy == "" { + commentContent.allSatisfied = false + } + } + + // Logs results or write them in bot PR comment. + if gh.DryRun { + logMutex.Lock() + logResults(gh.Logger, pr.GetNumber(), commentContent) + logMutex.Unlock() + } else { + if err := updatePullRequest(gh, pr, commentContent); err != nil { + gh.Logger.Errorf("unable to update pull request: %v", err) + failed.Store(true) + } + } + }(pr) + } + wg.Wait() + + if failed.Load() { + return errors.New("error occurred while processing pull requests") + } + + return nil +} + +// logResults is called in dry-run mode and outputs the status of each check +// and a conclusion. +func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { + logger.Infof("Pull request #%d requirements", prNum) + if len(commentContent.AutoRules) > 0 { + logger.Infof("Automated Checks:") + } + + for _, rule := range commentContent.AutoRules { + status := utils.Fail + if rule.Satisfied { + status = utils.Success + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Then:\n%s", rule.RequirementDetails) + } + + if len(commentContent.ManualRules) > 0 { + logger.Infof("Manual Checks:") + } + + for _, rule := range commentContent.ManualRules { + status := utils.Fail + checker := "any user with comment edit permission" + if rule.CheckedBy != "" { + status = utils.Success + } + if len(rule.Teams) == 0 { + checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", ")) + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Can be checked by %s", checker) + } + + logger.Infof("Conclusion:") + if commentContent.allSatisfied { + logger.Infof("%s All requirements are satisfied\n", utils.Success) + } else { + logger.Infof("%s Not all requirements are satisfied\n", utils.Fail) + } +} diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go new file mode 100644 index 00000000000..8bf4a158745 --- /dev/null +++ b/contribs/github-bot/comment.go @@ -0,0 +1,282 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "text/template" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/sethvargo/go-githubactions" +) + +var errTriggeredByBot = errors.New("event triggered by bot") + +// Compile regex only once. +var ( + // Regex for capturing the entire line of a manual check. + manualCheckLine = regexp.MustCompile(`(?m:^-\s\[([ xX])\]\s+(.+?)\s*(\(checked by @(\w+)\))?$)`) + // Regex for capturing only the checkboxes. + checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) + // Regex used to capture markdown links. + markdownLink = regexp.MustCompile(`\[(.*)\]\(.*\)`) +) + +// These structures contain the necessary information to generate +// the bot's comment from the template file. +type AutoContent struct { + Description string + Satisfied bool + ConditionDetails string + RequirementDetails string +} +type ManualContent struct { + Description string + CheckedBy string + ConditionDetails string + Teams []string +} +type CommentContent struct { + AutoRules []AutoContent + ManualRules []ManualContent + allSatisfied bool +} + +type manualCheckDetails struct { + status string + checkedBy string +} + +// getCommentManualChecks parses the bot comment to get the checkbox status, +// the check description and the username who checked it. +func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { + checks := make(map[string]manualCheckDetails) + + // For each line that matches the "Manual check" regex. + for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { + description := match[2] + status := match[1] + checkedBy := "" + if len(match) > 4 { + checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x. + } + + checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} + } + + return checks +} + +// handleCommentUpdate checks if: +// - the current run was triggered by GitHub Actions +// - the triggering event is an edit of the bot comment +// - the comment was not edited by the bot itself (prevent infinite loop) +// - the comment change is only a checkbox being checked or unckecked (or restore it) +// - the actor / comment editor has permission to modify this checkbox (or restore it) +func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubContext) error { + // Ignore if it's not a comment related event. + if actionCtx.EventName != utils.EventIssueComment { + gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) + return nil + } + + // Ignore if the action type is not deleted or edited. + actionType, ok := actionCtx.Event["action"].(string) + if !ok { + return errors.New("unable to get type on issue comment event") + } + + if actionType != "deleted" && actionType != "edited" { + return nil + } + + // Return if comment was edited by bot (current authenticated user). + authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + return fmt.Errorf("unable to get authenticated user: %w", err) + } + + if actionCtx.Actor == authUser.GetLogin() { + gh.Logger.Debugf("Prevent infinite loop if the bot comment was edited by the bot itself") + return errTriggeredByBot + } + + // Get login of the author of the edited comment. + login, ok := utils.IndexMap(actionCtx.Event, "comment", "user", "login").(string) + if !ok { + return errors.New("unable to get comment user login on issue comment event") + } + + // If the author is not the bot, return. + if login != authUser.GetLogin() { + return nil + } + + // Get comment updated body. + current, ok := utils.IndexMap(actionCtx.Event, "comment", "body").(string) + if !ok { + return errors.New("unable to get comment body on issue comment event") + } + + // Get comment previous body. + previous, ok := utils.IndexMap(actionCtx.Event, "changes", "body", "from").(string) + if !ok { + return errors.New("unable to get changes body content on issue comment event") + } + + // Get PR number from GitHub Actions context. + prNum, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64) + if !ok || prNum <= 0 { + return errors.New("unable to get issue number on issue comment event") + } + + // Check if change is only a checkbox being checked or unckecked. + if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { + // If not, restore previous comment body. + if !gh.DryRun { + gh.SetBotComment(previous, int(prNum)) + } + return errors.New("bot comment edited outside of checkboxes") + } + + // Check if actor / comment editor has permission to modify changed boxes. + currentChecks := getCommentManualChecks(current) + previousChecks := getCommentManualChecks(previous) + edited := "" + for key := range currentChecks { + // If there is no diff for this check, ignore it. + if currentChecks[key].status == previousChecks[key].status { + continue + } + + // Get teams allowed to edit this box from config. + var teams []string + found := false + _, manualRules := config(gh) + + for _, manualRule := range manualRules { + if manualRule.description == key { + found = true + teams = manualRule.teams + } + } + + // If rule were not found, return to reprocess the bot comment entirely + // (maybe bot config was updated since last run?). + if !found { + gh.Logger.Debugf("Updated rule not found in config: %s", key) + return nil + } + + // If teams specified in rule, check if actor is a member of one of them. + if len(teams) > 0 { + if gh.IsUserInTeams(actionCtx.Actor, teams) { + if !gh.DryRun { + gh.SetBotComment(previous, int(prNum)) + } + return errors.New("checkbox edited by a user not allowed to") + } + } + + // This regex capture only the line of the current check. + specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, regexp.QuoteMeta(key))) + + // If the box is checked, append the username of the user who checked it. + if strings.TrimSpace(currentChecks[key].status) == "x" { + replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key].status, key, actionCtx.Actor) + edited = specificManualCheck.ReplaceAllString(current, replacement) + } else { + // Else, remove the username of the user. + replacement := fmt.Sprintf("- [%s] %s", currentChecks[key].status, key) + edited = specificManualCheck.ReplaceAllString(current, replacement) + } + } + + // Update comment with username. + if edited != "" && !gh.DryRun { + gh.SetBotComment(edited, int(prNum)) + gh.Logger.Debugf("Comment manual checks updated successfully") + } + + return nil +} + +// generateComment generates a comment using the template file and the +// content passed as parameter. +func generateComment(content CommentContent) (string, error) { + // Custom function to strip markdown links. + funcMap := template.FuncMap{ + "stripLinks": func(input string) string { + return markdownLink.ReplaceAllString(input, "$1") + }, + } + + // Bind markdown stripping function to template generator. + const tmplFile = "comment.tmpl" + tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) + if err != nil { + return "", fmt.Errorf("unable to init template: %w", err) + } + + // Generate bot comment using template file. + var commentBytes bytes.Buffer + if err := tmpl.Execute(&commentBytes, content); err != nil { + return "", fmt.Errorf("unable to execute template: %w", err) + } + + return commentBytes.String(), nil +} + +// updatePullRequest updates or creates both the bot comment and the commit status. +func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) error { + // Generate comment text content. + commentText, err := generateComment(content) + if err != nil { + return fmt.Errorf("unable to generate comment on PR %d: %w", pr.GetNumber(), err) + } + + // Update comment on pull request. + comment, err := gh.SetBotComment(commentText, pr.GetNumber()) + if err != nil { + return fmt.Errorf("unable to update comment on PR %d: %w", pr.GetNumber(), err) + } else { + gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) + } + + // Prepare commit status content. + var ( + context = "Merge Requirements" + targetURL = comment.GetHTMLURL() + state = "failure" + description = "Some requirements are not satisfied yet. See bot comment." + ) + + if content.allSatisfied { + state = "success" + description = "All requirements are satisfied." + } + + // Update or create commit status. + if _, _, err := gh.Client.Repositories.CreateStatus( + gh.Ctx, + gh.Owner, + gh.Repo, + pr.GetHead().GetSHA(), + &github.RepoStatus{ + Context: &context, + State: &state, + TargetURL: &targetURL, + Description: &description, + }); err != nil { + return fmt.Errorf("unable to create status on PR %d: %w", pr.GetNumber(), err) + } else { + gh.Logger.Infof("Commit status successfully updated on PR %d", pr.GetNumber()) + } + + return nil +} diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl new file mode 100644 index 00000000000..ebd07fdd4b9 --- /dev/null +++ b/contribs/github-bot/comment.tmpl @@ -0,0 +1,51 @@ +# Merge Requirements + +The following requirements must be fulfilled before a pull request can be merged. +Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member. + +These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go). + +## Automated Checks + +{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} +{{ end }} + +{{ if .AutoRules }}
Details
+{{ range .AutoRules }} +
{{ .Description | stripLinks }}
+ +### If +``` +{{ .ConditionDetails | stripLinks }} +``` +### Then +``` +{{ .RequirementDetails | stripLinks }} +``` +
+{{ end }} +
+{{ else }}*No automated checks match this pull request.*{{ end }} + +## Manual Checks + +{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} +{{ end }} + +{{ if .ManualRules }}
Details
+{{ range .ManualRules }} +
{{ .Description | stripLinks }}
+ +### If +``` +{{ .ConditionDetails }} +``` +### Can be checked by +{{range $item := .Teams }} - team {{ $item | stripLinks }} +{{ else }} +- Any user with comment edit permission +{{end}} +
+{{ end }} +
+{{ else }}*No manual checks match this pull request.*{{ end }} diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go new file mode 100644 index 00000000000..fd8790dd9e1 --- /dev/null +++ b/contribs/github-bot/comment_test.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/sethvargo/go-githubactions" + "github.com/stretchr/testify/assert" +) + +func TestGeneratedComment(t *testing.T) { + t.Parallel() + + autoCheckSuccessLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Success)) + autoCheckFailLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Fail)) + + content := CommentContent{} + autoRules := []AutoContent{ + {Description: "Test automatic 1", Satisfied: false}, + {Description: "Test automatic 2", Satisfied: false}, + {Description: "Test automatic 3", Satisfied: true}, + {Description: "Test automatic 4", Satisfied: true}, + {Description: "Test automatic 5", Satisfied: false}, + } + manualRules := []ManualContent{ + {Description: "Test manual 1", CheckedBy: "user_1"}, + {Description: "Test manual 2", CheckedBy: ""}, + {Description: "Test manual 3", CheckedBy: ""}, + {Description: "Test manual 4", CheckedBy: "user_4"}, + {Description: "Test manual 5", CheckedBy: "user_5"}, + } + + commentText, err := generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + + content.AutoRules = autoRules + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + + content.ManualRules = manualRules + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder") + + manualChecks := getCommentManualChecks(commentText) + assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found") + for _, rule := range manualRules { + val, ok := manualChecks[rule.Description] + assert.True(t, ok, "manual check should exist") + if rule.CheckedBy == "" { + assert.Equal(t, " ", val.status, "manual rule should not be checked") + } else { + assert.Equal(t, "x", val.status, "manual rule should be checked") + } + assert.Equal(t, rule.CheckedBy, val.checkedBy, "invalid username found for CheckedBy") + } +} + +func setValue(t *testing.T, m map[string]any, value any, keys ...string) map[string]any { + t.Helper() + + if len(keys) > 1 { + currMap, ok := m[keys[0]].(map[string]any) + if !ok { + currMap = map[string]any{} + } + m[keys[0]] = setValue(t, currMap, value, keys[1:]...) + } else if len(keys) == 1 { + m[keys[0]] = value + } + + return m +} + +func TestCommentUpdateHandler(t *testing.T) { + t.Parallel() + + const ( + user = "user" + bot = "bot" + ) + actionCtx := &githubactions.GitHubContext{ + Event: make(map[string]any), + } + + mockOptions := []mock.MockBackendOption{} + newGHClient := func() *client.GitHub { + return &client.GitHub{ + Client: github.NewClient(mock.NewMockedHTTPClient(mockOptions...)), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + } + gh := newGHClient() + + // Exit without error because EventName is empty + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.EventName = utils.EventIssueComment + + // Exit with error because Event.action is not set + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event["action"] = "" + + // Exit without error because Event.action is set but not 'deleted' + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event["action"] = "deleted" + + // Exit with error because mock not setup to return authUser + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + mockOptions = append(mockOptions, mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/user", + Method: "GET", + }, + github.User{Login: github.String(bot)}, + )) + gh = newGHClient() + actionCtx.Actor = bot + + // Exit with error because authUser and action actor is the same user + assert.ErrorIs(t, handleCommentUpdate(gh, actionCtx), errTriggeredByBot) + actionCtx.Actor = user + + // Exit with error because Event.comment.user.login is not set + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, user, "comment", "user", "login") + + // Exit without error because comment author is not the bot + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, bot, "comment", "user", "login") + + // Exit with error because Event.comment.body is not set + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "comment", "body") + + // Exit with error because Event.changes.body.from is not set + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "updated_body", "changes", "body", "from") + + // Exit with error because Event.issue.number is not set + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number") + + // Exit with error because checkboxes are differents + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from") + + assert.Nil(t, handleCommentUpdate(gh, actionCtx)) +} diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go new file mode 100644 index 00000000000..4504844e289 --- /dev/null +++ b/contribs/github-bot/config.go @@ -0,0 +1,100 @@ +package main + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/client" + c "github.com/gnolang/gno/contribs/github-bot/internal/conditions" + r "github.com/gnolang/gno/contribs/github-bot/internal/requirements" +) + +// Automatic check that will be performed by the bot. +type automaticCheck struct { + description string + ifC c.Condition // If the condition is met, the rule is displayed and the requirement is executed. + thenR r.Requirement // If the requirement is satisfied, the check passes. +} + +// Manual check that will be performed by users. +type manualCheck struct { + description string + ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment. + teams []string // Members of these teams can check the checkbox to make the check pass. +} + +// This function returns the configuration of the bot consisting of automatic and manual checks +// in which the GitHub client is injected. +func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { + auto := []automaticCheck{ + { + description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", + ifC: c.And( + c.FileChanged(gh, "tm2"), + c.BaseBranch("master"), + ), + thenR: r.And( + r.Or( + r.ReviewByTeamMembers(gh, "eu", 1), + r.AuthorInTeam(gh, "eu"), + ), + r.Or( + r.ReviewByTeamMembers(gh, "us", 1), + r.AuthorInTeam(gh, "us"), + ), + ), + }, + { + description: "A maintainer must be able to edit this pull request", + ifC: c.Always(), + thenR: r.MaintainerCanModify(), + }, + { + description: "The pull request head branch must be up-to-date with its base", + ifC: c.Always(), // Or only if c.BaseBranch("main") ? + thenR: r.UpToDateWith(gh, r.PR_BASE), + }, + } + + manual := []manualCheck{ + { + description: "Determine if infra needs to be updated", + ifC: c.And( + c.BaseBranch("master"), + c.Or( + c.FileChanged(gh, "misc/deployments"), + c.FileChanged(gh, `misc/docker-\.*`), + c.FileChanged(gh, "tm2/pkg/p2p"), + ), + ), + teams: []string{"tech-staff"}, + }, + { + description: "Ensure the code style is satisfactory", + ifC: c.And( + c.BaseBranch("master"), + c.Or( + c.FileChanged(gh, `.*\.go`), + c.FileChanged(gh, `.*\.js`), + ), + ), + teams: []string{"tech-staff"}, + }, + { + description: "Ensure the documentation is accurate and relevant", + ifC: c.FileChanged(gh, `.*\.md`), + teams: []string{ + "tech-staff", + "devrels", + }, + }, + } + + // Check for duplicates in manual rule descriptions (needs to be unique for the bot operations). + unique := make(map[string]struct{}) + for _, rule := range manual { + if _, exists := unique[rule.description]; exists { + gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.description) + } + unique[rule.description] = struct{}{} + } + + return auto, manual +} diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod new file mode 100644 index 00000000000..8df55e3f282 --- /dev/null +++ b/contribs/github-bot/go.mod @@ -0,0 +1,28 @@ +module github.com/gnolang/gno/contribs/github-bot + +go 1.22 + +toolchain go1.22.2 + +replace github.com/gnolang/gno => ../.. + +require ( + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/google/go-github/v64 v64.0.0 + github.com/migueleliasweb/go-github-mock v1.0.1 + github.com/sethvargo/go-githubactions v1.3.0 + github.com/stretchr/testify v1.9.0 + github.com/xlab/treeprint v1.2.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/time v0.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum new file mode 100644 index 00000000000..2dae4e83e72 --- /dev/null +++ b/contribs/github-bot/go.sum @@ -0,0 +1,38 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= +github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934= +github.com/migueleliasweb/go-github-mock v1.0.1/go.mod h1:8PJ7MpMoIiCBBNpuNmvndHm0QicjsE+hjex1yMGmjYQ= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= +github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go new file mode 100644 index 00000000000..229c3e90631 --- /dev/null +++ b/contribs/github-bot/internal/client/client.go @@ -0,0 +1,293 @@ +package client + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + p "github.com/gnolang/gno/contribs/github-bot/internal/params" + + "github.com/google/go-github/v64/github" +) + +// PageSize is the number of items to load for each iteration when fetching a list. +const PageSize = 100 + +var ErrBotCommentNotFound = errors.New("bot comment not found") + +// GitHub contains everything necessary to interact with the GitHub API, +// including the client, a context (which must be passed with each request), +// a logger, etc. This object will be passed to each condition or requirement +// that requires fetching additional information or modifying things on GitHub. +// The object also provides methods for performing more complex operations than +// a simple API call. +type GitHub struct { + Client *github.Client + Ctx context.Context + DryRun bool + Logger logger.Logger + Owner string + Repo string +} + +// GetBotComment retrieves the bot's (current user) comment on provided PR number. +func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { + // List existing comments + const ( + sort = "created" + direction = "desc" + ) + + // Get current user (bot) + currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + return nil, fmt.Errorf("unable to get current user: %w", err) + } + + // Pagination option + opts := &github.IssueListCommentsOptions{ + Sort: github.String(sort), + Direction: github.String(direction), + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + + for { + comments, response, err := gh.Client.Issues.ListComments( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err) + } + + // Get the comment created by current user + for _, comment := range comments { + if comment.GetUser().GetLogin() == currentUser.GetLogin() { + return comment, nil + } + } + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return nil, errors.New("bot comment not found") +} + +// SetBotComment creates a bot's comment on the provided PR number +// or updates it if it already exists. +func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) { + // Create bot comment if it does not already exist + comment, err := gh.GetBotComment(prNum) + if errors.Is(err, ErrBotCommentNotFound) { + newComment, _, err := gh.Client.Issues.CreateComment( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + &github.IssueComment{Body: &body}, + ) + if err != nil { + return nil, fmt.Errorf("unable to create bot comment for PR %d: %w", prNum, err) + } + return newComment, nil + } else if err != nil { + return nil, fmt.Errorf("unable to get bot comment: %w", err) + } + + comment.Body = &body + editComment, _, err := gh.Client.Issues.EditComment( + gh.Ctx, + gh.Owner, + gh.Repo, + comment.GetID(), + comment, + ) + if err != nil { + return nil, fmt.Errorf("unable to edit bot comment with ID %d: %w", comment.GetID(), err) + } + + return editComment, nil +} + +// ListTeamMembers lists the members of the specified team. +func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { + var ( + allMembers []*github.User + opts = &github.TeamListTeamMembersOptions{ + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + ) + + for { + members, response, err := gh.Client.Teams.ListTeamMembersBySlug( + gh.Ctx, + gh.Owner, + team, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list members for team %s: %w", team, err) + } + + allMembers = append(allMembers, members...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allMembers, nil +} + +// IsUserInTeams checks if the specified user is a member of any of the +// provided teams. +func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { + for _, team := range teams { + teamMembers, err := gh.ListTeamMembers(team) + if err != nil { + gh.Logger.Errorf("unable to check if user %s in team %s", user, team) + continue + } + + for _, member := range teamMembers { + if member.GetLogin() == user { + return true + } + } + } + + return false +} + +// ListPRReviewers returns the list of reviewers for the specified PR number. +func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) { + var ( + allReviewers = &github.Reviewers{} + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviewers, response, err := gh.Client.PullRequests.ListReviewers( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list reviewers for PR %d: %w", prNum, err) + } + + allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) + allReviewers.Users = append(allReviewers.Users, reviewers.Users...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviewers, nil +} + +// ListPRReviewers returns the list of reviews for the specified PR number. +func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) { + var ( + allReviews []*github.PullRequestReview + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviews, response, err := gh.Client.PullRequests.ListReviews( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list reviews for PR %d: %w", prNum, err) + } + + allReviews = append(allReviews, reviews...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviews, nil +} + +// ListPR returns the list of pull requests in the specified state. +func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) { + var prs []*github.PullRequest + + opts := &github.PullRequestListOptions{ + State: state, + Sort: "updated", + Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + + for { + prsPage, response, err := gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + return nil, fmt.Errorf("unable to list pull requests with state %s: %w", state, err) + } + + prs = append(prs, prsPage...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return prs, nil +} + +// New initializes the API client, the logger, and creates an instance of GitHub. +func New(ctx context.Context, params *p.Params) (*GitHub, error) { + gh := &GitHub{ + Ctx: ctx, + Owner: params.Owner, + Repo: params.Repo, + DryRun: params.DryRun, + } + + // Detect if the current process was launched by a GitHub Action and return + // a logger suitable for terminal output or the GitHub Actions web interface + gh.Logger = logger.NewLogger(params.Verbose) + + // Retrieve GitHub API token from env + token, set := os.LookupEnv("GITHUB_TOKEN") + if !set { + return nil, errors.New("GITHUB_TOKEN is not set in env") + } + + // Init GitHub API client using token + gh.Client = github.NewClient(nil).WithAuthToken(token) + + return gh, nil +} diff --git a/contribs/github-bot/internal/conditions/assignee.go b/contribs/github-bot/internal/conditions/assignee.go new file mode 100644 index 00000000000..7024259909c --- /dev/null +++ b/contribs/github-bot/internal/conditions/assignee.go @@ -0,0 +1,66 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Assignee Condition. +type assignee struct { + user string +} + +var _ Condition = &assignee{} + +func (a *assignee) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A pull request assignee is user: %s", a.user) + + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func Assignee(user string) Condition { + return &assignee{user: user} +} + +// AssigneeInTeam Condition. +type assigneeInTeam struct { + gh *client.GitHub + team string +} + +var _ Condition = &assigneeInTeam{} + +func (a *assigneeInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A pull request assignee is a member of the team: %s", a.team) + + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if assignee is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { + for _, assignee := range pr.Assignees { + if member.GetLogin() == assignee.GetLogin() { + return utils.AddStatusNode(true, fmt.Sprintf("%s (member: %s)", detail, member.GetLogin()), details) + } + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AssigneeInTeam(gh *client.GitHub, team string) Condition { + return &assigneeInTeam{gh: gh, team: team} +} diff --git a/contribs/github-bot/internal/conditions/assignee_test.go b/contribs/github-bot/internal/conditions/assignee_test.go new file mode 100644 index 00000000000..9207e4604b7 --- /dev/null +++ b/contribs/github-bot/internal/conditions/assignee_test.go @@ -0,0 +1,100 @@ +package conditions + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAssignee(t *testing.T) { + t.Parallel() + + assignees := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + assignees []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", assignees, true}, + {"assignee list doesn't contain user", "user2", assignees, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Assignees: testCase.assignees} + details := treeprint.New() + condition := Assignee(testCase.user) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestAssigneeInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", members, true}, + {"assignee list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + Assignees: []*github.User{ + {Login: github.String(testCase.user)}, + }, + } + details := treeprint.New() + condition := AssigneeInTeam(gh, "team") + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/author.go b/contribs/github-bot/internal/conditions/author.go new file mode 100644 index 00000000000..9052f781bd5 --- /dev/null +++ b/contribs/github-bot/internal/conditions/author.go @@ -0,0 +1,60 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Author Condition. +type author struct { + user string +} + +var _ Condition = &author{} + +func (a *author) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + a.user == pr.GetUser().GetLogin(), + fmt.Sprintf("Pull request author is user: %v", a.user), + details, + ) +} + +func Author(user string) Condition { + return &author{user: user} +} + +// AuthorInTeam Condition. +type authorInTeam struct { + gh *client.GitHub + team string +} + +var _ Condition = &authorInTeam{} + +func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) + + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if author is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { + if member.GetLogin() == pr.GetUser().GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AuthorInTeam(gh *client.GitHub, team string) Condition { + return &authorInTeam{gh: gh, team: team} +} diff --git a/contribs/github-bot/internal/conditions/author_test.go b/contribs/github-bot/internal/conditions/author_test.go new file mode 100644 index 00000000000..c5836f1ea76 --- /dev/null +++ b/contribs/github-bot/internal/conditions/author_test.go @@ -0,0 +1,93 @@ +package conditions + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestAuthor(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + user string + author string + isMet bool + }{ + {"author match", "user", "user", true}, + {"author doesn't match", "user", "author", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.author)}, + } + details := treeprint.New() + condition := Author(testCase.user) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestAuthorInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isMet bool + }{ + {"empty member list", "user", []*github.User{}, false}, + {"member list contains user", "user", members, true}, + {"member list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.user)}, + } + details := treeprint.New() + condition := AuthorInTeam(gh, "team") + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/boolean.go b/contribs/github-bot/internal/conditions/boolean.go new file mode 100644 index 00000000000..2fa3a25f7ac --- /dev/null +++ b/contribs/github-bot/internal/conditions/boolean.go @@ -0,0 +1,98 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// And Condition. +type and struct { + conditions []Condition +} + +var _ Condition = &and{} + +func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := utils.Success + branch := details.AddBranch("") + + for _, condition := range a.conditions { + if !condition.IsMet(pr, branch) { + met = utils.Fail + // We don't break here because we need to call IsMet on all conditions + // to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s And", met)) + + return (met == utils.Success) +} + +func And(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to And()") + } + + return &and{conditions} +} + +// Or Condition. +type or struct { + conditions []Condition +} + +var _ Condition = &or{} + +func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := utils.Fail + branch := details.AddBranch("") + + for _, condition := range o.conditions { + if condition.IsMet(pr, branch) { + met = utils.Success + // We don't break here because we need to call IsMet on all conditions + // to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s Or", met)) + + return (met == utils.Success) +} + +func Or(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to Or()") + } + + return &or{conditions} +} + +// Not Condition. +type not struct { + cond Condition +} + +var _ Condition = ¬{} + +func (n *not) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := n.cond.IsMet(pr, details) + node := details.FindLastNode() + + if met { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string))) + } else { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string))) + } + + return !met +} + +func Not(cond Condition) Condition { + return ¬{cond} +} diff --git a/contribs/github-bot/internal/conditions/boolean_test.go b/contribs/github-bot/internal/conditions/boolean_test.go new file mode 100644 index 00000000000..52f028cf2b4 --- /dev/null +++ b/contribs/github-bot/internal/conditions/boolean_test.go @@ -0,0 +1,96 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAnd(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + conditions []Condition + isMet bool + }{ + {"and is true", []Condition{Always(), Always()}, true}, + {"and is false", []Condition{Always(), Always(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := And(testCase.conditions...) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestAndPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided") +} + +func TestOr(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + conditions []Condition + isMet bool + }{ + {"or is true", []Condition{Never(), Always()}, true}, + {"or is false", []Condition{Never(), Never(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := Or(testCase.conditions...) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestOrPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided") +} + +func TestNot(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + condition Condition + isMet bool + }{ + {"not is true", Never(), true}, + {"not is false", Always(), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := Not(testCase.condition) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/branch.go b/contribs/github-bot/internal/conditions/branch.go new file mode 100644 index 00000000000..6977d633d98 --- /dev/null +++ b/contribs/github-bot/internal/conditions/branch.go @@ -0,0 +1,49 @@ +package conditions + +import ( + "fmt" + "regexp" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// BaseBranch Condition. +type baseBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &baseBranch{} + +func (b *baseBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + b.pattern.MatchString(pr.GetBase().GetRef()), + fmt.Sprintf("The base branch matches this pattern: %s", b.pattern.String()), + details, + ) +} + +func BaseBranch(pattern string) Condition { + return &baseBranch{pattern: regexp.MustCompile(pattern)} +} + +// HeadBranch Condition. +type headBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &headBranch{} + +func (h *headBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + h.pattern.MatchString(pr.GetHead().GetRef()), + fmt.Sprintf("The head branch matches this pattern: %s", h.pattern.String()), + details, + ) +} + +func HeadBranch(pattern string) Condition { + return &headBranch{pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github-bot/internal/conditions/branch_test.go b/contribs/github-bot/internal/conditions/branch_test.go new file mode 100644 index 00000000000..3e53ef2db1c --- /dev/null +++ b/contribs/github-bot/internal/conditions/branch_test.go @@ -0,0 +1,49 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestHeadBaseBranch(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + pattern string + base string + isMet bool + }{ + {"perfectly match", "base", "base", true}, + {"prefix match", "^dev/", "dev/test-bot", true}, + {"prefix doesn't match", "dev/$", "dev/test-bot", false}, + {"suffix match", "/test-bot$", "dev/test-bot", true}, + {"suffix doesn't match", "^/test-bot", "dev/test-bot", false}, + {"doesn't match", "base", "notatall", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + Base: &github.PullRequestBranch{Ref: github.String(testCase.base)}, + Head: &github.PullRequestBranch{Ref: github.String(testCase.base)}, + } + conditions := []Condition{ + BaseBranch(testCase.pattern), + HeadBranch(testCase.pattern), + } + + for _, condition := range conditions { + details := treeprint.New() + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + } + }) + } +} diff --git a/contribs/github-bot/internal/conditions/condition.go b/contribs/github-bot/internal/conditions/condition.go new file mode 100644 index 00000000000..8c2fa5a2948 --- /dev/null +++ b/contribs/github-bot/internal/conditions/condition.go @@ -0,0 +1,12 @@ +package conditions + +import ( + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +type Condition interface { + // Check if the Condition is met and add the details + // to the tree passed as a parameter. + IsMet(pr *github.PullRequest, details treeprint.Tree) bool +} diff --git a/contribs/github-bot/internal/conditions/constant.go b/contribs/github-bot/internal/conditions/constant.go new file mode 100644 index 00000000000..26bbe9e8110 --- /dev/null +++ b/contribs/github-bot/internal/conditions/constant.go @@ -0,0 +1,34 @@ +package conditions + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Always Condition. +type always struct{} + +var _ Condition = &always{} + +func (*always) IsMet(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(true, "On every pull request", details) +} + +func Always() Condition { + return &always{} +} + +// Never Condition. +type never struct{} + +var _ Condition = &never{} + +func (*never) IsMet(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(false, "On no pull request", details) +} + +func Never() Condition { + return &never{} +} diff --git a/contribs/github-bot/internal/conditions/constant_test.go b/contribs/github-bot/internal/conditions/constant_test.go new file mode 100644 index 00000000000..92bbe9b318a --- /dev/null +++ b/contribs/github-bot/internal/conditions/constant_test.go @@ -0,0 +1,25 @@ +package conditions + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestAlways(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.True(t, Always().IsMet(nil, details), "condition should have a met status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") +} + +func TestNever(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.False(t, Never().IsMet(nil, details), "condition should have a met status: false") + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") +} diff --git a/contribs/github-bot/internal/conditions/file.go b/contribs/github-bot/internal/conditions/file.go new file mode 100644 index 00000000000..e3854a7734a --- /dev/null +++ b/contribs/github-bot/internal/conditions/file.go @@ -0,0 +1,58 @@ +package conditions + +import ( + "fmt" + "regexp" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// FileChanged Condition. +type fileChanged struct { + gh *client.GitHub + pattern *regexp.Regexp +} + +var _ Condition = &fileChanged{} + +func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A changed file matches this pattern: %s", fc.pattern.String()) + opts := &github.ListOptions{ + PerPage: client.PageSize, + } + + for { + files, response, err := fc.gh.Client.PullRequests.ListFiles( + fc.gh.Ctx, + fc.gh.Owner, + fc.gh.Repo, + pr.GetNumber(), + opts, + ) + if err != nil { + fc.gh.Logger.Errorf("Unable to list changed files for PR %d: %v", pr.GetNumber(), err) + break + } + + for _, file := range files { + if fc.pattern.MatchString(file.GetFilename()) { + return utils.AddStatusNode(true, fmt.Sprintf("%s (filename: %s)", detail, file.GetFilename()), details) + } + } + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return utils.AddStatusNode(false, detail, details) +} + +func FileChanged(gh *client.GitHub, pattern string) Condition { + return &fileChanged{gh: gh, pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github-bot/internal/conditions/file_test.go b/contribs/github-bot/internal/conditions/file_test.go new file mode 100644 index 00000000000..3fd7a33fa4a --- /dev/null +++ b/contribs/github-bot/internal/conditions/file_test.go @@ -0,0 +1,68 @@ +package conditions + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestFileChanged(t *testing.T) { + t.Parallel() + + filenames := []*github.CommitFile{ + {Filename: github.String("foo")}, + {Filename: github.String("bar")}, + {Filename: github.String("baz")}, + } + + for _, testCase := range []struct { + name string + pattern string + files []*github.CommitFile + isMet bool + }{ + {"empty file list", "foo", []*github.CommitFile{}, false}, + {"file list contains exact match", "foo", filenames, true}, + {"file list contains prefix match", "^fo", filenames, true}, + {"file list contains prefix doesn't match", "fo$", filenames, false}, + {"file list contains suffix match", "oo$", filenames, true}, + {"file list contains suffix doesn't match", "^oo", filenames, false}, + {"file list doesn't contains match", "foobar", filenames, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/files", + Method: "GET", + }, + testCase.files, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + condition := FileChanged(gh, testCase.pattern) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/label.go b/contribs/github-bot/internal/conditions/label.go new file mode 100644 index 00000000000..ace94ed436c --- /dev/null +++ b/contribs/github-bot/internal/conditions/label.go @@ -0,0 +1,34 @@ +package conditions + +import ( + "fmt" + "regexp" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Label Condition. +type label struct { + pattern *regexp.Regexp +} + +var _ Condition = &label{} + +func (l *label) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A label matches this pattern: %s", l.pattern.String()) + + for _, label := range pr.Labels { + if l.pattern.MatchString(label.GetName()) { + return utils.AddStatusNode(true, fmt.Sprintf("%s (label: %s)", detail, label.GetName()), details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func Label(pattern string) Condition { + return &label{pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github-bot/internal/conditions/label_test.go b/contribs/github-bot/internal/conditions/label_test.go new file mode 100644 index 00000000000..ea895b28ad1 --- /dev/null +++ b/contribs/github-bot/internal/conditions/label_test.go @@ -0,0 +1,48 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestLabel(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + pattern string + labels []*github.Label + isMet bool + }{ + {"empty label list", "label", []*github.Label{}, false}, + {"label list contains exact match", "label", labels, true}, + {"label list contains prefix match", "^lab", labels, true}, + {"label list contains prefix doesn't match", "lab$", labels, false}, + {"label list contains suffix match", "bel$", labels, true}, + {"label list contains suffix doesn't match", "^bel", labels, false}, + {"label list doesn't contains match", "baleb", labels, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Labels: testCase.labels} + details := treeprint.New() + condition := Label(testCase.pattern) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/logger/action.go b/contribs/github-bot/internal/logger/action.go new file mode 100644 index 00000000000..c6d10429e62 --- /dev/null +++ b/contribs/github-bot/internal/logger/action.go @@ -0,0 +1,43 @@ +package logger + +import ( + "github.com/sethvargo/go-githubactions" +) + +type actionLogger struct{} + +var _ Logger = &actionLogger{} + +// Debugf implements Logger. +func (a *actionLogger) Debugf(msg string, args ...any) { + githubactions.Debugf(msg, args...) +} + +// Errorf implements Logger. +func (a *actionLogger) Errorf(msg string, args ...any) { + githubactions.Errorf(msg, args...) +} + +// Fatalf implements Logger. +func (a *actionLogger) Fatalf(msg string, args ...any) { + githubactions.Fatalf(msg, args...) +} + +// Infof implements Logger. +func (a *actionLogger) Infof(msg string, args ...any) { + githubactions.Infof(msg, args...) +} + +// Noticef implements Logger. +func (a *actionLogger) Noticef(msg string, args ...any) { + githubactions.Noticef(msg, args...) +} + +// Warningf implements Logger. +func (a *actionLogger) Warningf(msg string, args ...any) { + githubactions.Warningf(msg, args...) +} + +func newActionLogger() Logger { + return &actionLogger{} +} diff --git a/contribs/github-bot/internal/logger/logger.go b/contribs/github-bot/internal/logger/logger.go new file mode 100644 index 00000000000..570ca027e5c --- /dev/null +++ b/contribs/github-bot/internal/logger/logger.go @@ -0,0 +1,40 @@ +package logger + +import ( + "os" +) + +// All Logger methods follow the standard fmt.Printf convention. +type Logger interface { + // Debugf prints a debug-level message. + Debugf(msg string, args ...any) + + // Noticef prints a notice-level message. + Noticef(msg string, args ...any) + + // Warningf prints a warning-level message. + Warningf(msg string, args ...any) + + // Errorf prints a error-level message. + Errorf(msg string, args ...any) + + // Fatalf prints a error-level message and exits. + Fatalf(msg string, args ...any) + + // Infof prints message to stdout without any level annotations. + Infof(msg string, args ...any) +} + +// Returns a logger suitable for Github Actions or terminal output. +func NewLogger(verbose bool) Logger { + if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { + return newActionLogger() + } + + return newTermLogger(verbose) +} + +// NewNoopLogger returns a logger that does not log anything. +func NewNoopLogger() Logger { + return newNoopLogger() +} diff --git a/contribs/github-bot/internal/logger/noop.go b/contribs/github-bot/internal/logger/noop.go new file mode 100644 index 00000000000..629ed9d52d9 --- /dev/null +++ b/contribs/github-bot/internal/logger/noop.go @@ -0,0 +1,27 @@ +package logger + +type noopLogger struct{} + +var _ Logger = &noopLogger{} + +// Debugf implements Logger. +func (*noopLogger) Debugf(_ string, _ ...any) {} + +// Errorf implements Logger. +func (*noopLogger) Errorf(_ string, _ ...any) {} + +// Fatalf implements Logger. +func (*noopLogger) Fatalf(_ string, _ ...any) {} + +// Infof implements Logger. +func (*noopLogger) Infof(_ string, _ ...any) {} + +// Noticef implements Logger. +func (*noopLogger) Noticef(_ string, _ ...any) {} + +// Warningf implements Logger. +func (*noopLogger) Warningf(_ string, _ ...any) {} + +func newNoopLogger() Logger { + return &noopLogger{} +} diff --git a/contribs/github-bot/internal/logger/terminal.go b/contribs/github-bot/internal/logger/terminal.go new file mode 100644 index 00000000000..d0e5671a3c8 --- /dev/null +++ b/contribs/github-bot/internal/logger/terminal.go @@ -0,0 +1,55 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +type termLogger struct{} + +var _ Logger = &termLogger{} + +// Debugf implements Logger. +func (s *termLogger) Debugf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Debug(fmt.Sprintf(msg, args...)) +} + +// Errorf implements Logger. +func (s *termLogger) Errorf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Error(fmt.Sprintf(msg, args...)) +} + +// Fatalf implements Logger. +func (s *termLogger) Fatalf(msg string, args ...any) { + s.Errorf(msg, args...) + os.Exit(1) +} + +// Infof implements Logger. +func (s *termLogger) Infof(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Info(fmt.Sprintf(msg, args...)) +} + +// Noticef implements Logger. +func (s *termLogger) Noticef(msg string, args ...any) { + // Alias to info on terminal since notice level only exists on GitHub Actions. + s.Infof(msg, args...) +} + +// Warningf implements Logger. +func (s *termLogger) Warningf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Warn(fmt.Sprintf(msg, args...)) +} + +func newTermLogger(verbose bool) Logger { + if verbose { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + return &termLogger{} +} diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/params/params.go new file mode 100644 index 00000000000..c11d1b62419 --- /dev/null +++ b/contribs/github-bot/internal/params/params.go @@ -0,0 +1,118 @@ +package params + +import ( + "flag" + "fmt" + "os" + "time" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/sethvargo/go-githubactions" +) + +type Params struct { + Owner string + Repo string + PRAll bool + PRNums PRList + Verbose bool + DryRun bool + Timeout time.Duration + flagSet *flag.FlagSet +} + +func (p *Params) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &p.Owner, + "owner", + "", + "owner of the repo to process, if empty, will be retrieved from GitHub Actions context", + ) + + fs.StringVar( + &p.Repo, + "repo", + "", + "repo to process, if empty, will be retrieved from GitHub Actions context", + ) + + fs.BoolVar( + &p.PRAll, + "pr-all", + false, + "process all opened pull requests", + ) + + fs.TextVar( + &p.PRNums, + "pr-numbers", + PRList(nil), + "pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context", + ) + + fs.BoolVar( + &p.Verbose, + "verbose", + false, + "set logging level to debug", + ) + + fs.BoolVar( + &p.DryRun, + "dry-run", + false, + "print if pull request requirements are satisfied without updating anything on GitHub", + ) + + fs.DurationVar( + &p.Timeout, + "timeout", + 0, + "timeout after which the bot execution is interrupted", + ) + + p.flagSet = fs +} + +func (p *Params) ValidateFlags() { + // Helper to display an error + usage message before exiting. + errorUsage := func(err string) { + fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err) + p.flagSet.Usage() + os.Exit(1) + } + + // Check if flags are coherent. + if p.PRAll && len(p.PRNums) != 0 { + errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.") + } + + // If one of these values is empty, it must be retrieved + // from GitHub Actions context. + if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) { + actionCtx, err := githubactions.Context() + if err != nil { + errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err)) + } + + if p.Owner == "" { + if p.Owner, _ = actionCtx.Repo(); p.Owner == "" { + errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.") + } + } + if p.Repo == "" { + if _, p.Repo = actionCtx.Repo(); p.Repo == "" { + errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.") + } + } + + if len(p.PRNums) == 0 && !p.PRAll { + prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) + if err != nil { + errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error())) + } + + p.PRNums = PRList{prNum} + } + } +} diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go new file mode 100644 index 00000000000..51aed8dc457 --- /dev/null +++ b/contribs/github-bot/internal/params/prlist.go @@ -0,0 +1,49 @@ +package params + +import ( + "encoding" + "fmt" + "strconv" + "strings" +) + +type PRList []int + +// PRList is both a TextMarshaler and a TextUnmarshaler. +var ( + _ encoding.TextMarshaler = PRList{} + _ encoding.TextUnmarshaler = &PRList{} +) + +// MarshalText implements encoding.TextMarshaler. +func (p PRList) MarshalText() (text []byte, err error) { + prNumsStr := make([]string, len(p)) + + for i, prNum := range p { + prNumsStr[i] = strconv.Itoa(prNum) + } + + return []byte(strings.Join(prNumsStr, ",")), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (p *PRList) UnmarshalText(text []byte) error { + prNumsStr := strings.Split(string(text), ",") + prNums := make([]int, len(prNumsStr)) + + for i := range prNumsStr { + prNum, err := strconv.Atoi(strings.TrimSpace(prNumsStr[i])) + if err != nil { + return err + } + + if prNum <= 0 { + return fmt.Errorf("invalid pull request number (<= 0): original(%s) parsed(%d)", prNumsStr[i], prNum) + } + + prNums[i] = prNum + } + *p = prNums + + return nil +} diff --git a/contribs/github-bot/internal/requirements/assignee.go b/contribs/github-bot/internal/requirements/assignee.go new file mode 100644 index 00000000000..9a2723ad18f --- /dev/null +++ b/contribs/github-bot/internal/requirements/assignee.go @@ -0,0 +1,53 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Assignee Requirement. +type assignee struct { + gh *client.GitHub + user string +} + +var _ Requirement = &assignee{} + +func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This user is assigned to pull request: %s", a.user) + + // Check if user was already assigned to PR. + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + // If in a dry run, skip assigning the user. + if a.gh.DryRun { + return utils.AddStatusNode(false, detail, details) + } + + // If user not already assigned, assign it. + if _, _, err := a.gh.Client.Issues.AddAssignees( + a.gh.Ctx, + a.gh.Owner, + a.gh.Repo, + pr.GetNumber(), + []string{a.user}, + ); err != nil { + a.gh.Logger.Errorf("Unable to assign user %s to PR %d: %v", a.user, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + return utils.AddStatusNode(true, detail, details) +} + +func Assignee(gh *client.GitHub, user string) Requirement { + return &assignee{gh: gh, user: user} +} diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go new file mode 100644 index 00000000000..df6ffdf0cd3 --- /dev/null +++ b/contribs/github-bot/internal/requirements/assignee_test.go @@ -0,0 +1,72 @@ +package requirements + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAssignee(t *testing.T) { + t.Parallel() + + assignees := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + assignees []*github.User + dryRun bool + exists bool + }{ + {"empty assignee list", "user", []*github.User{}, false, false}, + {"empty assignee list with dry-run", "user", []*github.User{}, true, false}, + {"assignee list contains user", "user", assignees, false, true}, + {"assignee list doesn't contain user", "user2", assignees, false, false}, + {"assignee list doesn't contain user with dry-run", "user2", assignees, true, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/assignees", + Method: "GET", // It looks like this mock package doesn't support mocking POST requests + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, + } + + pr := &github.PullRequest{Assignees: testCase.assignees} + details := treeprint.New() + requirement := Assignee(gh, testCase.user) + + assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true") + assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true") + assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item") + }) + } +} diff --git a/contribs/github-bot/internal/requirements/author.go b/contribs/github-bot/internal/requirements/author.go new file mode 100644 index 00000000000..eed2c510b97 --- /dev/null +++ b/contribs/github-bot/internal/requirements/author.go @@ -0,0 +1,39 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/conditions" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Author Requirement. +type author struct { + c conditions.Condition // Alias Author requirement to identical condition. +} + +var _ Requirement = &author{} + +func (a *author) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return a.c.IsMet(pr, details) +} + +func Author(user string) Requirement { + return &author{conditions.Author(user)} +} + +// AuthorInTeam Requirement. +type authorInTeam struct { + c conditions.Condition // Alias AuthorInTeam requirement to identical condition. +} + +var _ Requirement = &authorInTeam{} + +func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return a.c.IsMet(pr, details) +} + +func AuthorInTeam(gh *client.GitHub, team string) Requirement { + return &authorInTeam{conditions.AuthorInTeam(gh, team)} +} diff --git a/contribs/github-bot/internal/requirements/author_test.go b/contribs/github-bot/internal/requirements/author_test.go new file mode 100644 index 00000000000..768ca44f24e --- /dev/null +++ b/contribs/github-bot/internal/requirements/author_test.go @@ -0,0 +1,93 @@ +package requirements + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestAuthor(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + user string + author string + isSatisfied bool + }{ + {"author match", "user", "user", true}, + {"author doesn't match", "user", "author", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.author)}, + } + details := treeprint.New() + requirement := Author(testCase.user) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} + +func TestAuthorInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isSatisfied bool + }{ + {"empty member list", "user", []*github.User{}, false}, + {"member list contains user", "user", members, true}, + {"member list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.user)}, + } + details := treeprint.New() + requirement := AuthorInTeam(gh, "team") + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go new file mode 100644 index 00000000000..6b441c92f80 --- /dev/null +++ b/contribs/github-bot/internal/requirements/boolean.go @@ -0,0 +1,98 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// And Requirement. +type and struct { + requirements []Requirement +} + +var _ Requirement = &and{} + +func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := utils.Success + branch := details.AddBranch("") + + for _, requirement := range a.requirements { + if !requirement.IsSatisfied(pr, branch) { + satisfied = utils.Fail + // We don't break here because we need to call IsSatisfied on all + // requirements to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s And", satisfied)) + + return (satisfied == utils.Success) +} + +func And(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to And()") + } + + return &and{requirements} +} + +// Or Requirement. +type or struct { + requirements []Requirement +} + +var _ Requirement = &or{} + +func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := utils.Fail + branch := details.AddBranch("") + + for _, requirement := range o.requirements { + if requirement.IsSatisfied(pr, branch) { + satisfied = utils.Success + // We don't break here because we need to call IsSatisfied on all + // requirements to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s Or", satisfied)) + + return (satisfied == utils.Success) +} + +func Or(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to Or()") + } + + return &or{requirements} +} + +// Not Requirement. +type not struct { + req Requirement +} + +var _ Requirement = ¬{} + +func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := n.req.IsSatisfied(pr, details) + node := details.FindLastNode() + + if satisfied { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string))) + } else { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string))) + } + + return !satisfied +} + +func Not(req Requirement) Requirement { + return ¬{req} +} diff --git a/contribs/github-bot/internal/requirements/boolean_test.go b/contribs/github-bot/internal/requirements/boolean_test.go new file mode 100644 index 00000000000..0043a44985c --- /dev/null +++ b/contribs/github-bot/internal/requirements/boolean_test.go @@ -0,0 +1,96 @@ +package requirements + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAnd(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirements []Requirement + isSatisfied bool + }{ + {"and is true", []Requirement{Always(), Always()}, true}, + {"and is false", []Requirement{Always(), Always(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := And(testCase.requirements...) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} + +func TestAndPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided") +} + +func TestOr(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirements []Requirement + isSatisfied bool + }{ + {"or is true", []Requirement{Never(), Always()}, true}, + {"or is false", []Requirement{Never(), Never(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := Or(testCase.requirements...) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} + +func TestOrPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided") +} + +func TestNot(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirement Requirement + isSatisfied bool + }{ + {"not is true", Never(), true}, + {"not is false", Always(), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := Not(testCase.requirement) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go new file mode 100644 index 00000000000..65d00d06ae8 --- /dev/null +++ b/contribs/github-bot/internal/requirements/branch.go @@ -0,0 +1,53 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Pass this to UpToDateWith constructor to check the PR head branch +// against its base branch. +const PR_BASE = "PR_BASE" + +// UpToDateWith Requirement. +type upToDateWith struct { + gh *client.GitHub + base string +} + +var _ Requirement = &author{} + +func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + base := u.base + if u.base == PR_BASE { + base = pr.GetBase().GetRef() + } + head := pr.GetHead().GetRef() + + cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) + if err != nil { + u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s): %v", head, base, err) + return false + } + + return utils.AddStatusNode( + cmp.GetBehindBy() == 0, + fmt.Sprintf( + "Head branch (%s) is up to date with base (%s): behind by %d / ahead by %d", + head, + base, + cmp.GetBehindBy(), + cmp.GetAheadBy(), + ), + details, + ) +} + +func UpToDateWith(gh *client.GitHub, base string) Requirement { + return &upToDateWith{gh, base} +} diff --git a/contribs/github-bot/internal/requirements/branch_test.go b/contribs/github-bot/internal/requirements/branch_test.go new file mode 100644 index 00000000000..54387beb605 --- /dev/null +++ b/contribs/github-bot/internal/requirements/branch_test.go @@ -0,0 +1,62 @@ +package requirements + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestUpToDateWith(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + behind int + ahead int + isSatisfied bool + }{ + {"up-to-date without commit ahead", 0, 0, true}, + {"up-to-date with commits ahead", 0, 3, true}, + {"not up-to-date with commits behind", 3, 0, false}, + {"not up-to-date with commits behind and ahead", 3, 3, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/compare/base...", + Method: "GET", + }, + github.CommitsComparison{ + AheadBy: &testCase.ahead, + BehindBy: &testCase.behind, + }, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := UpToDateWith(gh, "base") + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/constant.go b/contribs/github-bot/internal/requirements/constant.go new file mode 100644 index 00000000000..cbe932da830 --- /dev/null +++ b/contribs/github-bot/internal/requirements/constant.go @@ -0,0 +1,34 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Always Requirement. +type always struct{} + +var _ Requirement = &always{} + +func (*always) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(true, "On every pull request", details) +} + +func Always() Requirement { + return &always{} +} + +// Never Requirement. +type never struct{} + +var _ Requirement = &never{} + +func (*never) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(false, "On no pull request", details) +} + +func Never() Requirement { + return &never{} +} diff --git a/contribs/github-bot/internal/requirements/constant_test.go b/contribs/github-bot/internal/requirements/constant_test.go new file mode 100644 index 00000000000..b04addcb672 --- /dev/null +++ b/contribs/github-bot/internal/requirements/constant_test.go @@ -0,0 +1,25 @@ +package requirements + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestAlways(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.True(t, Always().IsSatisfied(nil, details), "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details), "requirement details should have a status: true") +} + +func TestNever(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.False(t, Never().IsSatisfied(nil, details), "requirement should have a satisfied status: false") + assert.True(t, utils.TestLastNodeStatus(t, false, details), "requirement details should have a status: false") +} diff --git a/contribs/github-bot/internal/requirements/label.go b/contribs/github-bot/internal/requirements/label.go new file mode 100644 index 00000000000..d1ee475db92 --- /dev/null +++ b/contribs/github-bot/internal/requirements/label.go @@ -0,0 +1,53 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Label Requirement. +type label struct { + gh *client.GitHub + name string +} + +var _ Requirement = &label{} + +func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This label is applied to pull request: %s", l.name) + + // Check if label was already applied to PR. + for _, label := range pr.Labels { + if l.name == label.GetName() { + return utils.AddStatusNode(true, detail, details) + } + } + + // If in a dry run, skip applying the label. + if l.gh.DryRun { + return utils.AddStatusNode(false, detail, details) + } + + // If label not already applied, apply it. + if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( + l.gh.Ctx, + l.gh.Owner, + l.gh.Repo, + pr.GetNumber(), + []string{l.name}, + ); err != nil { + l.gh.Logger.Errorf("Unable to add label %s to PR %d: %v", l.name, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + return utils.AddStatusNode(true, detail, details) +} + +func Label(gh *client.GitHub, name string) Requirement { + return &label{gh, name} +} diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go new file mode 100644 index 00000000000..6fbe8ff7f25 --- /dev/null +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -0,0 +1,79 @@ +package requirements + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestLabel(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + pattern string + labels []*github.Label + dryRun bool + exists bool + }{ + {"empty label list", "label", []*github.Label{}, false, false}, + {"empty label list with dry-run", "label", []*github.Label{}, true, false}, + {"label list contains exact match", "label", labels, false, true}, + {"label list contains prefix match", "^lab", labels, false, true}, + {"label list contains prefix doesn't match", "lab$", labels, false, false}, + {"label list contains prefix doesn't match with dry-run", "lab$", labels, true, false}, + {"label list contains suffix match", "bel$", labels, false, true}, + {"label list contains suffix match with dry-run", "bel$", labels, true, true}, + {"label list contains suffix doesn't match", "^bel", labels, false, false}, + {"label list contains suffix doesn't match with dry-run", "^bel", labels, true, false}, + {"label list doesn't contains match", "baleb", labels, false, false}, + {"label list doesn't contains match with dry-run", "baleb", labels, true, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/labels", + Method: "GET", // It looks like this mock package doesn't support mocking POST requests + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, + } + + pr := &github.PullRequest{Labels: testCase.labels} + details := treeprint.New() + requirement := Label(gh, testCase.pattern) + + assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true") + assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true") + assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item") + }) + } +} diff --git a/contribs/github-bot/internal/requirements/maintainer.go b/contribs/github-bot/internal/requirements/maintainer.go new file mode 100644 index 00000000000..8e3f356bebf --- /dev/null +++ b/contribs/github-bot/internal/requirements/maintainer.go @@ -0,0 +1,25 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// MaintainerCanModify Requirement. +type maintainerCanModify struct{} + +var _ Requirement = &maintainerCanModify{} + +func (a *maintainerCanModify) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetMaintainerCanModify(), + "Maintainer can modify this pull request", + details, + ) +} + +func MaintainerCanModify() Requirement { + return &maintainerCanModify{} +} diff --git a/contribs/github-bot/internal/requirements/maintener_test.go b/contribs/github-bot/internal/requirements/maintener_test.go new file mode 100644 index 00000000000..5b71803b468 --- /dev/null +++ b/contribs/github-bot/internal/requirements/maintener_test.go @@ -0,0 +1,34 @@ +package requirements + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestMaintenerCanModify(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + isSatisfied bool + }{ + {"modify is true", true}, + {"modify is false", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{MaintainerCanModify: &testCase.isSatisfied} + details := treeprint.New() + requirement := MaintainerCanModify() + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/requirement.go b/contribs/github-bot/internal/requirements/requirement.go new file mode 100644 index 00000000000..296c4a1461d --- /dev/null +++ b/contribs/github-bot/internal/requirements/requirement.go @@ -0,0 +1,12 @@ +package requirements + +import ( + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +type Requirement interface { + // Check if the Requirement is satisfied and add the detail + // to the tree passed as a parameter. + IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool +} diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go new file mode 100644 index 00000000000..aa3914d4c4a --- /dev/null +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -0,0 +1,156 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Reviewer Requirement. +type reviewByUser struct { + gh *client.GitHub + user string +} + +var _ Requirement = &reviewByUser{} + +func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This user approved pull request: %s", r.user) + + // If not a dry run, make the user a reviewer if he's not already. + if !r.gh.DryRun { + requested := false + reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s review is already requested: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, user := range reviewers.Users { + if user.GetLogin() == r.user { + requested = true + break + } + } + + if requested { + r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + Reviewers: []string{r.user}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from user %s on PR %d: %v", r.user, pr.GetNumber(), err) + } + } + } + + // Check if user already approved this PR. + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { + if review.GetUser().GetLogin() == r.user { + r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) + return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details) + } + } + r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) + + return utils.AddStatusNode(false, detail, details) +} + +func ReviewByUser(gh *client.GitHub, user string) Requirement { + return &reviewByUser{gh, user} +} + +// Reviewer Requirement. +type reviewByTeamMembers struct { + gh *client.GitHub + team string + count uint +} + +var _ Requirement = &reviewByTeamMembers{} + +func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("At least %d user(s) of the team %s approved pull request", r.count, r.team) + + // If not a dry run, make the user a reviewer if he's not already. + if !r.gh.DryRun { + requested := false + reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if team %s review is already requested: %v", r.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, team := range reviewers.Teams { + if team.GetSlug() == r.team { + requested = true + break + } + } + + if requested { + r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + TeamReviewers: []string{r.team}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from team %s on PR %d: %v", r.team, pr.GetNumber(), err) + } + } + } + + // Check how many members of this team already approved this PR. + approved := uint(0) + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if a member of team %s already approved this PR: %v", r.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { + teamMembers, err := r.gh.ListTeamMembers(r.team) + if err != nil { + r.gh.Logger.Errorf(err.Error()) + continue + } + + for _, member := range teamMembers { + if review.GetUser().GetLogin() == member.GetLogin() { + if review.GetState() == "APPROVED" { + approved += 1 + } + r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count) + } + } + } + + return utils.AddStatusNode(approved >= r.count, detail, details) +} + +func ReviewByTeamMembers(gh *client.GitHub, team string, count uint) Requirement { + return &reviewByTeamMembers{gh, team, count} +} diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go new file mode 100644 index 00000000000..16c50e13743 --- /dev/null +++ b/contribs/github-bot/internal/requirements/reviewer_test.go @@ -0,0 +1,215 @@ +package requirements + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestReviewByUser(t *testing.T) { + t.Parallel() + + reviewers := github.Reviewers{ + Users: []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + }, + } + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("notTheRightOne")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("anotherOne")}, + State: github.String("REQUEST_CHANGES"), + }, + } + + for _, testCase := range []struct { + name string + user string + isSatisfied bool + create bool + }{ + {"reviewer matches", "user", true, false}, + {"reviewer matches without approval", "anotherOne", false, false}, + {"reviewer doesn't match", "user2", false, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + firstRequest := true + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/requested_reviewers", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if firstRequest { + w.Write(mock.MustMarshal(reviewers)) + firstRequest = false + } else { + requested = true + } + }), + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByUser(gh, testCase.user) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + assert.Equal(t, testCase.create, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.create)) + }) + } +} + +func TestReviewByTeamMembers(t *testing.T) { + t.Parallel() + + reviewers := github.Reviewers{ + Teams: []*github.Team{ + {Slug: github.String("team1")}, + {Slug: github.String("team2")}, + {Slug: github.String("team3")}, + }, + } + + members := map[string][]*github.User{ + "team1": { + {Login: github.String("user1")}, + {Login: github.String("user2")}, + {Login: github.String("user3")}, + }, + "team2": { + {Login: github.String("user3")}, + {Login: github.String("user4")}, + {Login: github.String("user5")}, + }, + "team3": { + {Login: github.String("user4")}, + {Login: github.String("user5")}, + }, + } + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("user1")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user2")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user3")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user4")}, + State: github.String("REQUEST_CHANGES"), + }, { + User: &github.User{Login: github.String("user5")}, + State: github.String("REQUEST_CHANGES"), + }, + } + + for _, testCase := range []struct { + name string + team string + count uint + isSatisfied bool + testRequest bool + }{ + {"3/3 team members approved;", "team1", 3, true, false}, + {"1/1 team member approved", "team2", 1, true, false}, + {"1/2 team member approved", "team2", 2, false, false}, + {"0/1 team member approved", "team3", 1, false, false}, + {"0/1 team member approved with request", "team3", 1, false, true}, + {"team doesn't exist with request", "team4", 1, false, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + firstRequest := true + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/requested_reviewers", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if firstRequest { + if testCase.testRequest { + w.Write(mock.MustMarshal(github.Reviewers{})) + } else { + w.Write(mock.MustMarshal(reviewers)) + } + firstRequest = false + } else { + requested = true + } + }), + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.team), + Method: "GET", + }, + members[testCase.team], + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByTeamMembers(gh, testCase.team, testCase.count) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + assert.Equal(t, testCase.testRequest, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.testRequest)) + }) + } +} diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go new file mode 100644 index 00000000000..91b8ac7e6b4 --- /dev/null +++ b/contribs/github-bot/internal/utils/actions.go @@ -0,0 +1,45 @@ +package utils + +import ( + "fmt" + + "github.com/sethvargo/go-githubactions" +) + +// Recursively search for nested values using the keys provided. +func IndexMap(m map[string]any, keys ...string) any { + if len(keys) == 0 { + return m + } + + if val, ok := m[keys[0]]; ok { + if keys = keys[1:]; len(keys) == 0 { + return val + } + subMap, _ := val.(map[string]any) + return IndexMap(subMap, keys...) + } + + return nil +} + +// Retrieve PR number from GitHub Actions context +func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) { + firstKey := "" + + switch actionCtx.EventName { + case EventIssueComment: + firstKey = "issue" + case EventPullRequest, EventPullRequestTarget: + firstKey = "pull_request" + default: + return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName) + } + + num, ok := IndexMap(actionCtx.Event, firstKey, "number").(float64) + if !ok || num <= 0 { + return 0, fmt.Errorf("invalid value: %d", int(num)) + } + + return int(num), nil +} diff --git a/contribs/github-bot/internal/utils/actions_test.go b/contribs/github-bot/internal/utils/actions_test.go new file mode 100644 index 00000000000..3114bb8a061 --- /dev/null +++ b/contribs/github-bot/internal/utils/actions_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexMap(t *testing.T) { + t.Parallel() + + m := map[string]any{ + "Key1": map[string]any{ + "Key2": map[string]any{ + "Key3": 1, + }, + }, + } + + test := IndexMap(m) + assert.NotNil(t, test, "should return m") + _, ok := test.(map[string]any) + assert.True(t, ok, "returned m should be a map") + + test = IndexMap(m, "Key1") + assert.NotNil(t, test, "should return Key1 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key1 value type should be a map") + + test = IndexMap(m, "Key1", "Key2") + assert.NotNil(t, test, "should return Key2 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key2 value type should be a map") + + test = IndexMap(m, "Key1", "Key2", "Key3") + assert.NotNil(t, test, "should return Key3 value") + val, ok := test.(int) + assert.True(t, ok, "Key3 value type should be an int") + assert.Equal(t, 1, val, "Key3 value should be a 1") + + test = IndexMap(m, "Key1", "Key2", "Key3", "Key4") + assert.Nil(t, test, "Key4 value should not exist") +} diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go new file mode 100644 index 00000000000..564b7d3fb38 --- /dev/null +++ b/contribs/github-bot/internal/utils/github_const.go @@ -0,0 +1,14 @@ +package utils + +// GitHub const +const ( + // GitHub Actions Event Names + EventIssueComment = "issue_comment" + EventPullRequest = "pull_request" + EventPullRequestTarget = "pull_request_target" + EventWorkflowDispatch = "workflow_dispatch" + + // Pull Request States + PRStateOpen = "open" + PRStateClosed = "closed" +) diff --git a/contribs/github-bot/internal/utils/testing.go b/contribs/github-bot/internal/utils/testing.go new file mode 100644 index 00000000000..3c7f7bfef88 --- /dev/null +++ b/contribs/github-bot/internal/utils/testing.go @@ -0,0 +1,21 @@ +package utils + +import ( + "strings" + "testing" + + "github.com/xlab/treeprint" +) + +func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool { + t.Helper() + + detail := details.FindLastNode().(*treeprint.Node).Value.(string) + status := Fail + + if success { + status = Success + } + + return strings.HasPrefix(detail, string(status)) +} diff --git a/contribs/github-bot/internal/utils/tree.go b/contribs/github-bot/internal/utils/tree.go new file mode 100644 index 00000000000..c6ff57bcd99 --- /dev/null +++ b/contribs/github-bot/internal/utils/tree.go @@ -0,0 +1,24 @@ +package utils + +import ( + "fmt" + + "github.com/xlab/treeprint" +) + +type Status string + +const ( + Success Status = "🟢" + Fail Status = "🔴" +) + +func AddStatusNode(b bool, desc string, details treeprint.Tree) bool { + if b { + details.AddNode(fmt.Sprintf("%s %s", Success, desc)) + } else { + details.AddNode(fmt.Sprintf("%s %s", Fail, desc)) + } + + return b +} diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go new file mode 100644 index 00000000000..9895f44dc70 --- /dev/null +++ b/contribs/github-bot/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func main() { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: "github-bot [flags]", + LongHelp: "Bot that allows for advanced management of GitHub pull requests.", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newCheckCmd(), + newMatrixCmd(), + ) + + cmd.Execute(context.Background(), os.Args[1:]) +} diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go new file mode 100644 index 00000000000..2442a6d94d6 --- /dev/null +++ b/contribs/github-bot/matrix.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/params" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/sethvargo/go-githubactions" +) + +func newMatrixCmd() *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "matrix", + ShortUsage: "github-bot matrix", + ShortHelp: "parses GitHub Actions event and defines matrix accordingly", + LongHelp: "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.", + }, + commands.NewEmptyConfig(), + func(_ context.Context, _ []string) error { + return execMatrix() + }, + ) +} + +func execMatrix() error { + // Get GitHub Actions context to retrieve event. + actionCtx, err := githubactions.Context() + if err != nil { + return fmt.Errorf("unable to get GitHub Actions context: %w", err) + } + + // Init Github client using only GitHub Actions context + owner, repo := actionCtx.Repo() + gh, err := client.New(context.Background(), ¶ms.Params{Owner: owner, Repo: repo}) + if err != nil { + return fmt.Errorf("unable to init GitHub client: %w", err) + } + + // Retrieve PR list from GitHub Actions event + prList, err := getPRListFromEvent(gh, actionCtx) + if err != nil { + return err + } + + fmt.Println(prList) + return nil +} + +func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (params.PRList, error) { + var prList params.PRList + + switch actionCtx.EventName { + // Event triggered from GitHub Actions user interface + case utils.EventWorkflowDispatch: + // Get input entered by the user + rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string) + if !ok { + return nil, errors.New("unable to get workflow dispatch input") + } + input := strings.TrimSpace(rawInput) + + // If all PR are requested, list them from GitHub API + if input == "all" { + prs, err := gh.ListPR(utils.PRStateOpen) + if err != nil { + return nil, fmt.Errorf("unable to list all PR: %w", err) + } + + prList = make(params.PRList, len(prs)) + for i := range prs { + prList[i] = prs[i].GetNumber() + } + } else { + // If a PR list is provided, parse it + if err := prList.UnmarshalText([]byte(input)); err != nil { + return nil, fmt.Errorf("invalid PR list provided as input: %w", err) + } + + // Then check if all provided PR are opened + for _, prNum := range prList { + pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) + if err != nil { + return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) + } else if pr.GetState() != utils.PRStateOpen { + return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState()) + } + } + } + + // Event triggered by an issue / PR comment being created / edited / deleted + // or any update on a PR + case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget: + // For these events, retrieve the number of the associated PR from the context + prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) + if err != nil { + return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err) + } + prList = params.PRList{prNum} + + default: + return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName) + } + + return prList, nil +} diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/matrix_test.go new file mode 100644 index 00000000000..bce4ec1bd8f --- /dev/null +++ b/contribs/github-bot/matrix_test.go @@ -0,0 +1,248 @@ +package main + +import ( + "context" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/params" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/sethvargo/go-githubactions" + "github.com/stretchr/testify/assert" +) + +func TestProcessEvent(t *testing.T) { + t.Parallel() + + prs := []*github.PullRequest{ + {Number: github.Int(1), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(2), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(3), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(4), State: github.String(utils.PRStateClosed)}, + {Number: github.Int(5), State: github.String(utils.PRStateClosed)}, + {Number: github.Int(6), State: github.String(utils.PRStateClosed)}, + } + openPRs := prs[:3] + + for _, testCase := range []struct { + name string + gaCtx *githubactions.GitHubContext + prs []*github.PullRequest + expectedPRList params.PRList + expectedError bool + }{ + { + "valid issue_comment event", + &githubactions.GitHubContext{ + EventName: utils.EventIssueComment, + Event: map[string]any{"issue": map[string]any{"number": 1.}}, + }, + prs, + params.PRList{1}, + false, + }, { + "valid pull_request event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequest, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + params.PRList{1}, + false, + }, { + "valid pull_request_target event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestTarget, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + params.PRList{1}, + false, + }, { + "invalid event (PR number not set)", + &githubactions.GitHubContext{ + EventName: utils.EventIssueComment, + Event: map[string]any{"issue": nil}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid event name", + &githubactions.GitHubContext{ + EventName: "invalid_event", + Event: map[string]any{"issue": map[string]any{"number": 1.}}, + }, + prs, + params.PRList(nil), + true, + }, { + "valid workflow_dispatch all", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, + }, + openPRs, + params.PRList{1, 2, 3}, + false, + }, { + "valid workflow_dispatch all (no prs)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, + }, + nil, + params.PRList{}, + false, + }, { + "valid workflow_dispatch list", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}}, + }, + prs, + params.PRList{1, 2, 3}, + false, + }, { + "valid workflow_dispatch list with spaces", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}}, + }, + prs, + params.PRList{1, 2, 3}, + false, + }, { + "invalid workflow_dispatch list (1 closed)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (1 doesn't exist)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (all closed)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (empty)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (unset)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": ""}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (not a number list)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (number list with invalid elem)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}}, + }, + prs, + params.PRList(nil), + true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if testCase.expectedPRList != nil { + w.Write(mock.MustMarshal(testCase.prs)) + } + }), + ), + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/{number}", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var ( + err error + prNum int + parts = strings.Split(req.RequestURI, "/") + ) + + if len(parts) > 0 { + prNumStr := parts[len(parts)-1] + prNum, err = strconv.Atoi(prNumStr) + if err != nil { + panic(err) // Should never happen + } + } + + for _, pr := range prs { + if pr.GetNumber() == prNum { + w.Write(mock.MustMarshal(pr)) + return + } + } + + w.Write(mock.MustMarshal(nil)) + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + prList, err := getPRListFromEvent(gh, testCase.gaCtx) + if testCase.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.expectedPRList, prList) + }) + } +} From 4da7fdf2bf3643cadbeae8ef8490d0c56df9094d Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 27 Nov 2024 19:09:14 +0100 Subject: [PATCH 270/345] ci: in bot.yml, add checkout code and install go (#3224)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- .github/workflows/bot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 975f39f29dc..5beac27c07e 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -42,6 +42,12 @@ jobs: pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }} steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Generate matrix from event id: pr-numbers working-directory: contribs/github-bot From 6433b86e7b10760fa647910c592d745c30f12b25 Mon Sep 17 00:00:00 2001 From: Kristov Atlas <7227529+kristovatlas@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:25:27 -0600 Subject: [PATCH 271/345] docs: Clarify security policy (#3225) --- SECURITY.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 8380267dacf..409c3867e57 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,12 +1,10 @@ # Security Policy -The gno.land community strives to contribute toward the security of our ecosystem through internal security practices, and by working with external security researchers from the community. - ## Reporting a Vulnerability -If you've identified a vulnerability, please report it through one of the following venues: +If you've identified a vulnerability, please **DO NOT** open a new public issue. Instead, report it through one of the following venues: * Submit an advisory through GitHub: https://github.com/gnolang/gno/security/advisories/new -* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details. +* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details. If you’ve found multiple vulnerabilities, please submit one per email. * A security bug bounty platform for gno.land will be available Soonᵀᴹ. You will need to report via our bug bounty platform in order to be eligible for rewards. We will respond within 3 business days to all received reports. From 97b21590624feadea07ccfe4257b609010e4b4e9 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:28:02 +0100 Subject: [PATCH 272/345] ci: fixes bot workflow and comment update (#3229) This PR should (normally) fix the issues with the bot on this repo. In addition to the fixes, I also replaced the old config with a config that has much simpler rules while we decide what to add later on, once we've verified that everything is working properly. Here is the current config, If nothing seems off to you, we can merge it as it is then improve it incrementaly. ```go auto := []automaticCheck{ { description: "Maintainers must be able to edit this pull request", ifC: c.Always(), thenR: r.MaintainerCanModify(), }, { description: "The pull request head branch must be up-to-date with its base", ifC: c.Always(), thenR: r.UpToDateWith(gh, r.PR_BASE), }, { description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", ifC: c.FileChanged(gh, "^docs/"), thenR: r.Or( r.And( r.AuthorInTeam(gh, "devrels"), r.ReviewByTeamMembers(gh, "tech-staff", 1), ), r.And( r.AuthorInTeam(gh, "tech-staff"), r.ReviewByTeamMembers(gh, "devrels", 1), ), ), }, } manual := []manualCheck{ { description: "The pull request description provides enough details", ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")), teams: Teams{"core-contributors"}, }, { description: "Determine if infra needs to be updated before merging", ifC: c.And( c.BaseBranch("master"), c.Or( c.FileChanged(gh, `Dockerfile`), c.FileChanged(gh, `^misc/deployments`), c.FileChanged(gh, `^misc/docker-`), c.FileChanged(gh, `^.github/workflows/releaser.*\.yml$`), c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`), ), ), teams: Teams{"devops"}, }, } ```
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- .github/workflows/bot.yml | 12 ++-- contribs/github-bot/README.md | 9 +-- contribs/github-bot/comment.go | 4 +- contribs/github-bot/config.go | 71 ++++++++----------- contribs/github-bot/internal/client/client.go | 2 +- .../internal/conditions/branch_test.go | 4 +- .../github-bot/internal/conditions/draft.go | 21 ++++++ .../internal/conditions/draft_test.go | 34 +++++++++ .../internal/conditions/file_test.go | 4 +- .../internal/conditions/label_test.go | 4 +- contribs/github-bot/internal/params/prlist.go | 2 +- .../internal/requirements/assignee_test.go | 6 +- .../internal/requirements/branch.go | 5 ++ .../internal/requirements/label_test.go | 21 ++---- contribs/github-bot/matrix.go | 8 ++- 15 files changed, 126 insertions(+), 81 deletions(-) create mode 100644 contribs/github-bot/internal/conditions/draft.go create mode 100644 contribs/github-bot/internal/conditions/draft_test.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 5beac27c07e..21950459ae8 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -44,16 +44,18 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version-file: contribs/github-bot/go.mod + - name: Generate matrix from event id: pr-numbers working-directory: contribs/github-bot env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: go run . matrix >> "$GITHUB_OUTPUT" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: echo "pr-numbers=$(go run . matrix)" >> "$GITHUB_OUTPUT" # This job processes each pull request in the matrix individually while ensuring # that a same PR cannot be processed concurrently by mutliple runners @@ -76,10 +78,10 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version-file: contribs/github-bot/go.mod - name: Run GitHub Bot working-directory: contribs/github-bot env: GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }} - run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose + run: go run . check -pr-numbers '${{ matrix.pr-number }}' -verbose diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index e3cc12fe01a..78c9c3c01b8 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -27,14 +27,11 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token ## Usage ```bash -> go install github.com/gnolang/gno/contribs/github-bot@latest -// (go: downloading ...) - -> github-bot --help +> github-bot check --help USAGE - github-bot [flags] + github-bot check [flags] -This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. +This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. FLAGS diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 8bf4a158745..f6605ea8554 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -175,9 +175,9 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte // If teams specified in rule, check if actor is a member of one of them. if len(teams) > 0 { - if gh.IsUserInTeams(actionCtx.Actor, teams) { + if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed if !gh.DryRun { - gh.SetBotComment(previous, int(prNum)) + gh.SetBotComment(previous, int(prNum)) // Restore previous state } return errors.New("checkbox edited by a user not allowed to") } diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 4504844e289..4a28565ef7f 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -6,6 +6,8 @@ import ( r "github.com/gnolang/gno/contribs/github-bot/internal/requirements" ) +type Teams []string + // Automatic check that will be performed by the bot. type automaticCheck struct { description string @@ -17,7 +19,7 @@ type automaticCheck struct { type manualCheck struct { description string ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment. - teams []string // Members of these teams can check the checkbox to make the check pass. + teams Teams // Members of these teams can check the checkbox to make the check pass. } // This function returns the configuration of the bot consisting of automatic and manual checks @@ -25,65 +27,50 @@ type manualCheck struct { func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { auto := []automaticCheck{ { - description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", - ifC: c.And( - c.FileChanged(gh, "tm2"), - c.BaseBranch("master"), - ), - thenR: r.And( - r.Or( - r.ReviewByTeamMembers(gh, "eu", 1), - r.AuthorInTeam(gh, "eu"), - ), - r.Or( - r.ReviewByTeamMembers(gh, "us", 1), - r.AuthorInTeam(gh, "us"), - ), - ), - }, - { - description: "A maintainer must be able to edit this pull request", + description: "Maintainers must be able to edit this pull request", ifC: c.Always(), thenR: r.MaintainerCanModify(), }, { description: "The pull request head branch must be up-to-date with its base", - ifC: c.Always(), // Or only if c.BaseBranch("main") ? + ifC: c.Always(), thenR: r.UpToDateWith(gh, r.PR_BASE), }, + { + description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", + ifC: c.FileChanged(gh, "^docs/"), + thenR: r.Or( + r.And( + r.AuthorInTeam(gh, "devrels"), + r.ReviewByTeamMembers(gh, "tech-staff", 1), + ), + r.And( + r.AuthorInTeam(gh, "tech-staff"), + r.ReviewByTeamMembers(gh, "devrels", 1), + ), + ), + }, } manual := []manualCheck{ { - description: "Determine if infra needs to be updated", - ifC: c.And( - c.BaseBranch("master"), - c.Or( - c.FileChanged(gh, "misc/deployments"), - c.FileChanged(gh, `misc/docker-\.*`), - c.FileChanged(gh, "tm2/pkg/p2p"), - ), - ), - teams: []string{"tech-staff"}, + description: "The pull request description provides enough details", + ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")), + teams: Teams{"core-contributors"}, }, { - description: "Ensure the code style is satisfactory", + description: "Determine if infra needs to be updated before merging", ifC: c.And( c.BaseBranch("master"), c.Or( - c.FileChanged(gh, `.*\.go`), - c.FileChanged(gh, `.*\.js`), + c.FileChanged(gh, `Dockerfile`), + c.FileChanged(gh, `^misc/deployments`), + c.FileChanged(gh, `^misc/docker-`), + c.FileChanged(gh, `^.github/workflows/releaser.*\.yml$`), + c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`), ), ), - teams: []string{"tech-staff"}, - }, - { - description: "Ensure the documentation is accurate and relevant", - ifC: c.FileChanged(gh, `.*\.md`), - teams: []string{ - "tech-staff", - "devrels", - }, + teams: Teams{"devops"}, }, } diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go index 229c3e90631..474146ad3da 100644 --- a/contribs/github-bot/internal/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -80,7 +80,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { opts.Page = response.NextPage } - return nil, errors.New("bot comment not found") + return nil, ErrBotCommentNotFound } // SetBotComment creates a bot's comment on the provided PR number diff --git a/contribs/github-bot/internal/conditions/branch_test.go b/contribs/github-bot/internal/conditions/branch_test.go index 3e53ef2db1c..81ed96f8314 100644 --- a/contribs/github-bot/internal/conditions/branch_test.go +++ b/contribs/github-bot/internal/conditions/branch_test.go @@ -22,9 +22,9 @@ func TestHeadBaseBranch(t *testing.T) { }{ {"perfectly match", "base", "base", true}, {"prefix match", "^dev/", "dev/test-bot", true}, - {"prefix doesn't match", "dev/$", "dev/test-bot", false}, + {"prefix doesn't match", "^/test-bot", "dev/test-bot", false}, {"suffix match", "/test-bot$", "dev/test-bot", true}, - {"suffix doesn't match", "^/test-bot", "dev/test-bot", false}, + {"suffix doesn't match", "dev/$", "dev/test-bot", false}, {"doesn't match", "base", "notatall", false}, } { t.Run(testCase.name, func(t *testing.T) { diff --git a/contribs/github-bot/internal/conditions/draft.go b/contribs/github-bot/internal/conditions/draft.go new file mode 100644 index 00000000000..2c263f2ae75 --- /dev/null +++ b/contribs/github-bot/internal/conditions/draft.go @@ -0,0 +1,21 @@ +package conditions + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Draft Condition. +type draft struct{} + +var _ Condition = &baseBranch{} + +func (*draft) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(pr.GetDraft(), "This pull request is a draft", details) +} + +func Draft() Condition { + return &draft{} +} diff --git a/contribs/github-bot/internal/conditions/draft_test.go b/contribs/github-bot/internal/conditions/draft_test.go new file mode 100644 index 00000000000..a31b4eaca4c --- /dev/null +++ b/contribs/github-bot/internal/conditions/draft_test.go @@ -0,0 +1,34 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestDraft(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + isMet bool + }{ + {"draft is true", true}, + {"draft is false", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Draft: &testCase.isMet} + details := treeprint.New() + condition := Draft() + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/file_test.go b/contribs/github-bot/internal/conditions/file_test.go index 3fd7a33fa4a..8571ffea7d0 100644 --- a/contribs/github-bot/internal/conditions/file_test.go +++ b/contribs/github-bot/internal/conditions/file_test.go @@ -33,9 +33,9 @@ func TestFileChanged(t *testing.T) { {"empty file list", "foo", []*github.CommitFile{}, false}, {"file list contains exact match", "foo", filenames, true}, {"file list contains prefix match", "^fo", filenames, true}, - {"file list contains prefix doesn't match", "fo$", filenames, false}, + {"file list contains prefix doesn't match", "^oo", filenames, false}, {"file list contains suffix match", "oo$", filenames, true}, - {"file list contains suffix doesn't match", "^oo", filenames, false}, + {"file list contains suffix doesn't match", "fo$", filenames, false}, {"file list doesn't contains match", "foobar", filenames, false}, } { t.Run(testCase.name, func(t *testing.T) { diff --git a/contribs/github-bot/internal/conditions/label_test.go b/contribs/github-bot/internal/conditions/label_test.go index ea895b28ad1..00a3a8e3457 100644 --- a/contribs/github-bot/internal/conditions/label_test.go +++ b/contribs/github-bot/internal/conditions/label_test.go @@ -29,9 +29,9 @@ func TestLabel(t *testing.T) { {"empty label list", "label", []*github.Label{}, false}, {"label list contains exact match", "label", labels, true}, {"label list contains prefix match", "^lab", labels, true}, - {"label list contains prefix doesn't match", "lab$", labels, false}, + {"label list contains prefix doesn't match", "^bel", labels, false}, {"label list contains suffix match", "bel$", labels, true}, - {"label list contains suffix doesn't match", "^bel", labels, false}, + {"label list contains suffix doesn't match", "lab$", labels, false}, {"label list doesn't contains match", "baleb", labels, false}, } { t.Run(testCase.name, func(t *testing.T) { diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go index 51aed8dc457..ace7bcbe3b6 100644 --- a/contribs/github-bot/internal/params/prlist.go +++ b/contribs/github-bot/internal/params/prlist.go @@ -23,7 +23,7 @@ func (p PRList) MarshalText() (text []byte, err error) { prNumsStr[i] = strconv.Itoa(prNum) } - return []byte(strings.Join(prNumsStr, ",")), nil + return []byte(strings.Join(prNumsStr, ", ")), nil } // UnmarshalText implements encoding.TextUnmarshaler. diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go index df6ffdf0cd3..d72e8ad2a19 100644 --- a/contribs/github-bot/internal/requirements/assignee_test.go +++ b/contribs/github-bot/internal/requirements/assignee_test.go @@ -64,9 +64,9 @@ func TestAssignee(t *testing.T) { details := treeprint.New() requirement := Assignee(gh, testCase.user) - assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true") - assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true") - assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item") + assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true") + assert.True(t, testCase.exists || requested || testCase.dryRun, "requirement should have requested to create item") }) } } diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go index 65d00d06ae8..b686a093015 100644 --- a/contribs/github-bot/internal/requirements/branch.go +++ b/contribs/github-bot/internal/requirements/branch.go @@ -27,7 +27,12 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre if u.base == PR_BASE { base = pr.GetBase().GetRef() } + head := pr.GetHead().GetRef() + // If pull request is open from a fork, prepend head ref with fork owner login + if pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName() { + head = fmt.Sprintf("%s:%s", pr.GetHead().GetRepo().GetOwner().GetLogin(), pr.GetHead().GetRef()) + } cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) if err != nil { diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go index 6fbe8ff7f25..7e991b55756 100644 --- a/contribs/github-bot/internal/requirements/label_test.go +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -32,17 +32,10 @@ func TestLabel(t *testing.T) { exists bool }{ {"empty label list", "label", []*github.Label{}, false, false}, - {"empty label list with dry-run", "label", []*github.Label{}, true, false}, - {"label list contains exact match", "label", labels, false, true}, - {"label list contains prefix match", "^lab", labels, false, true}, - {"label list contains prefix doesn't match", "lab$", labels, false, false}, - {"label list contains prefix doesn't match with dry-run", "lab$", labels, true, false}, - {"label list contains suffix match", "bel$", labels, false, true}, - {"label list contains suffix match with dry-run", "bel$", labels, true, true}, - {"label list contains suffix doesn't match", "^bel", labels, false, false}, - {"label list contains suffix doesn't match with dry-run", "^bel", labels, true, false}, - {"label list doesn't contains match", "baleb", labels, false, false}, - {"label list doesn't contains match with dry-run", "baleb", labels, true, false}, + {"empty label list with dry-run", "user", []*github.Label{}, true, false}, + {"label list contains label", "label", labels, false, true}, + {"label list doesn't contain label", "label2", labels, false, false}, + {"label list doesn't contain label with dry-run", "label", labels, true, false}, } { t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -71,9 +64,9 @@ func TestLabel(t *testing.T) { details := treeprint.New() requirement := Label(gh, testCase.pattern) - assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true") - assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true") - assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item") + assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true") + assert.True(t, testCase.exists || requested || testCase.dryRun, "requirement should have requested to create item") }) } } diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go index 2442a6d94d6..56d6667589a 100644 --- a/contribs/github-bot/matrix.go +++ b/contribs/github-bot/matrix.go @@ -48,7 +48,13 @@ func execMatrix() error { return err } - fmt.Println(prList) + // Print PR list for GitHub Actions matrix definition + bytes, err := prList.MarshalText() + if err != nil { + return fmt.Errorf("unable to marshal PR list: %w", err) + } + fmt.Printf("[%s]", string(bytes)) + return nil } From 2c060704b0225f54975d2a015174b207a3c33221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Fri, 29 Nov 2024 10:05:14 +0100 Subject: [PATCH 273/345] chore: remove leftover `docker-integration` make directive (#3243) --- Makefile | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 2bfbe4e05e2..bd67020f236 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ install_gnokey: install.gnokey install_gno: install.gno .PHONY: test -test: test.components test.docker +test: test.components .PHONY: test.components test.components: @@ -64,14 +64,6 @@ test.components: $(MAKE) --no-print-directory -C examples test $(MAKE) --no-print-directory -C misc test -.PHONY: test.docker -test.docker: - @if hash docker 2>/dev/null; then \ - go test --tags=docker -count=1 -v ./misc/docker-integration; \ - else \ - echo "[-] 'docker' is missing, skipping ./misc/docker-integration tests."; \ - fi - .PHONY: fmt fmt: $(MAKE) --no-print-directory -C tm2 fmt imports From 7b7e7585540b495d5f0052ae280f3e876d91854a Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:31:45 +0100 Subject: [PATCH 274/345] feat(govdao): better rendering (#3096) ## Description Introduces better `gnoweb` rendering for the GovDAO suite, and better help page actions for voting on proposals. Home page before: Screenshot 2024-11-08 at 16 29 49 Home page after (also resolves usernames from `r/demo/users`): Screenshot 2024-11-09 at 13 19 55 Prop page before: Screenshot 2024-11-08 at 16 30 43 Prop page after: Screenshot 2024-11-21 at 13 05 50 The actions bar notifies the user when the proposal is no longer active as well. Continuation of #2579
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/p/demo/dao/dao.gno | 1 + examples/gno.land/p/demo/dao/proposals.gno | 5 +- examples/gno.land/p/demo/simpledao/dao.gno | 8 + .../gno.land/p/demo/simpledao/dao_test.gno | 49 ++++++ .../gno.land/p/demo/simpledao/propstore.gno | 38 +++-- examples/gno.land/r/gov/dao/v2/dao.gno | 54 ------ examples/gno.land/r/gov/dao/v2/gno.mod | 2 + .../gno.land/r/gov/dao/v2/prop1_filetest.gno | 80 +++++++-- .../gno.land/r/gov/dao/v2/prop2_filetest.gno | 80 +++++++-- .../gno.land/r/gov/dao/v2/prop3_filetest.gno | 98 +++++++++-- .../gno.land/r/gov/dao/v2/prop4_filetest.gno | 155 ++++++++---------- examples/gno.land/r/gov/dao/v2/render.gno | 123 ++++++++++++++ 12 files changed, 485 insertions(+), 208 deletions(-) create mode 100644 examples/gno.land/r/gov/dao/v2/render.gno diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno index f8ea433192f..e3a2ba72c5b 100644 --- a/examples/gno.land/p/demo/dao/dao.gno +++ b/examples/gno.land/p/demo/dao/dao.gno @@ -15,6 +15,7 @@ const ( // that contains the necessary information to // log and generate a valid proposal type ProposalRequest struct { + Title string // the title associated with the proposal Description string // the description associated with the proposal Executor Executor // the proposal executor } diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno index 5cad679d006..66abcb248c5 100644 --- a/examples/gno.land/p/demo/dao/proposals.gno +++ b/examples/gno.land/p/demo/dao/proposals.gno @@ -16,7 +16,7 @@ var ( Accepted ProposalStatus = "accepted" // proposal gathered quorum NotAccepted ProposalStatus = "not accepted" // proposal failed to gather quorum ExecutionSuccessful ProposalStatus = "execution successful" // proposal is executed successfully - ExecutionFailed ProposalStatus = "execution failed" // proposal is failed during execution + ExecutionFailed ProposalStatus = "execution failed" // proposal has failed during execution ) func (s ProposalStatus) String() string { @@ -42,6 +42,9 @@ type Proposal interface { // Author returns the author of the proposal Author() std.Address + // Title returns the title of the proposal + Title() string + // Description returns the description of the proposal Description() string diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 7a20237ec3f..837f64a41d6 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -3,6 +3,7 @@ package simpledao import ( "errors" "std" + "strings" "gno.land/p/demo/avl" "gno.land/p/demo/dao" @@ -12,6 +13,7 @@ import ( var ( ErrInvalidExecutor = errors.New("invalid executor provided") + ErrInvalidTitle = errors.New("invalid proposal title provided") ErrInsufficientProposalFunds = errors.New("insufficient funds for proposal") ErrInsufficientExecuteFunds = errors.New("insufficient funds for executing proposal") ErrProposalExecuted = errors.New("proposal already executed") @@ -47,6 +49,11 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { return 0, ErrInvalidExecutor } + // Make sure the title is set + if strings.TrimSpace(request.Title) == "" { + return 0, ErrInvalidTitle + } + var ( caller = getDAOCaller() sentCoins = std.GetOrigSend() // Get the sent coins, if any @@ -61,6 +68,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { // Create the wrapped proposal prop := &proposal{ author: caller, + title: request.Title, description: request.Description, executor: request.Executor, status: dao.Active, diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index fb32895e72f..46251e24dad 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -45,6 +45,50 @@ func TestSimpleDAO_Propose(t *testing.T) { ) }) + t.Run("invalid title", func(t *testing.T) { + t.Parallel() + + var ( + called = false + cb = func() error { + called = true + + return nil + } + ex = &mockExecutor{ + executeFn: cb, + } + + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minProposalFeeValue, + ), + ) + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return false + }, + } + s = New(ms) + ) + + std.TestSetOrigSend(sentCoins, std.Coins{}) + + _, err := s.Propose(dao.ProposalRequest{ + Executor: ex, + Title: "", // Set invalid title + }) + uassert.ErrorIs( + t, + err, + ErrInvalidTitle, + ) + + uassert.False(t, called) + }) + t.Run("caller cannot cover fee", func(t *testing.T) { t.Parallel() @@ -58,6 +102,7 @@ func TestSimpleDAO_Propose(t *testing.T) { ex = &mockExecutor{ executeFn: cb, } + title = "Proposal title" sentCoins = std.NewCoins( std.NewCoin( @@ -80,6 +125,7 @@ func TestSimpleDAO_Propose(t *testing.T) { _, err := s.Propose(dao.ProposalRequest{ Executor: ex, + Title: title, }) uassert.ErrorIs( t, @@ -105,6 +151,7 @@ func TestSimpleDAO_Propose(t *testing.T) { executeFn: cb, } description = "Proposal description" + title = "Proposal title" proposer = testutils.TestAddress("proposer") sentCoins = std.NewCoins( @@ -129,6 +176,7 @@ func TestSimpleDAO_Propose(t *testing.T) { // Make sure the proposal was added id, err := s.Propose(dao.ProposalRequest{ + Title: title, Description: description, Executor: ex, }) @@ -141,6 +189,7 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.Equal(t, proposer.String(), prop.Author().String()) uassert.Equal(t, description, prop.Description()) + uassert.Equal(t, title, prop.Title()) uassert.Equal(t, dao.Active.String(), prop.Status().String()) stats := prop.Stats() diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 06741d397cb..91f2a883047 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -3,6 +3,7 @@ package simpledao import ( "errors" "std" + "strings" "gno.land/p/demo/dao" "gno.land/p/demo/seqid" @@ -18,6 +19,7 @@ const maxRequestProposals = 10 // proposal is the internal simpledao proposal implementation type proposal struct { author std.Address // initiator of the proposal + title string // title of the proposal description string // description of the proposal executor dao.Executor // executor for the proposal @@ -31,6 +33,10 @@ func (p *proposal) Author() std.Address { return p.author } +func (p *proposal) Title() string { + return p.title +} + func (p *proposal) Description() string { return p.description } @@ -63,15 +69,20 @@ func (p *proposal) Render() string { // Fetch the voting stats stats := p.Stats() - output := "" - output += ufmt.Sprintf("Author: %s", p.Author().String()) - output += "\n\n" - output += p.Description() - output += "\n\n" - output += ufmt.Sprintf("Status: %s", p.Status().String()) - output += "\n\n" - output += ufmt.Sprintf( - "Voting stats: YES %d (%d%%), NO %d (%d%%), ABSTAIN %d (%d%%), MISSING VOTE %d (%d%%)", + var out string + + out += "## Description\n\n" + if strings.TrimSpace(p.description) != "" { + out += ufmt.Sprintf("%s\n\n", p.description) + } else { + out += "No description provided.\n\n" + } + + out += "## Proposal information\n\n" + out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(p.Status().String())) + + out += ufmt.Sprintf( + "**Voting stats:**\n- YES %d (%d%%)\n- NO %d (%d%%)\n- ABSTAIN %d (%d%%)\n- MISSING VOTES %d (%d%%)\n", stats.YayVotes, stats.YayPercent(), stats.NayVotes, @@ -81,10 +92,13 @@ func (p *proposal) Render() string { stats.MissingVotes(), stats.MissingVotesPercent(), ) - output += "\n\n" - output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3) - return output + out += "\n\n" + thresholdOut := strings.ToUpper(ufmt.Sprintf("%t", stats.YayVotes > (2*stats.TotalVotingPower)/3)) + + out += ufmt.Sprintf("**Threshold met: %s**\n\n", thresholdOut) + + return out } // addProposal adds a new simpledao proposal to the store diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index d99a161bcdf..9263d8d440b 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -2,12 +2,10 @@ package govdao import ( "std" - "strconv" "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/p/demo/simpledao" - "gno.land/p/demo/ufmt" ) var ( @@ -65,55 +63,3 @@ func GetPropStore() dao.PropStore { func GetMembStore() membstore.MemberStore { return members } - -func Render(path string) string { - if path == "" { - numProposals := d.Size() - - if numProposals == 0 { - return "No proposals found :(" // corner case - } - - output := "" - - offset := uint64(0) - if numProposals >= 10 { - offset = uint64(numProposals) - 10 - } - - // Fetch the last 10 proposals - for idx, prop := range d.Proposals(offset, uint64(10)) { - output += ufmt.Sprintf( - "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n", - idx, - "/r/gov/dao/v2", - idx, - prop.Status().String(), - prop.Author().String(), - ) - } - - return output - } - - // Display the detailed proposal - idx, err := strconv.Atoi(path) - if err != nil { - return "404: Invalid proposal ID" - } - - // Fetch the proposal - prop, err := d.ProposalByID(uint64(idx)) - if err != nil { - return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) - } - - // Render the proposal - output := "" - output += ufmt.Sprintf("# Prop #%d", idx) - output += "\n\n" - output += prop.Render() - output += "\n\n" - - return output -} diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index bc379bf18df..4da6e0a2484 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -7,4 +7,6 @@ require ( gno.land/p/demo/simpledao v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest + gno.land/p/moul/txlink v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index 7b25eeb1db3..7d8975e1fe8 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -42,9 +42,11 @@ func init() { executor := validators.NewPropExecutor(changesFn) // Create a proposal + title := "Valset change" description := "manual valset changes proposal example" prop := dao.ProposalRequest{ + Title: title, Description: description, Executor: executor, } @@ -73,52 +75,98 @@ func main() { // Output: // -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// # GovDAO Proposals +// +// ## [Prop #0 - Valset change](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// // // -- -// # Prop #0 +// # Proposal #0 - Valset change // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // manual valset changes proposal example // -// Status: active +// ## Proposal information +// +// **Status: ACTIVE** // -// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%) +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) // -// Threshold met: false +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] // // // -- // -- -// # Prop #0 +// # Proposal #0 - Valset change // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // manual valset changes proposal example // -// Status: accepted +// ## Proposal information +// +// **Status: ACCEPTED** // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) // -// Threshold met: true +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. // // // -- // No valset changes to apply. // -- // -- -// # Prop #0 +// # Proposal #0 - Valset change // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // manual valset changes proposal example // -// Status: execution successful +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// ### Actions // -// Threshold met: true +// The voting period for this proposal is over. // // // -- diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 4eb993b80dc..84a64bc4ee2 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -19,9 +19,11 @@ func init() { ) // Create a proposal + title := "govdao blog post title" description := "post a new blogpost about govdao" prop := dao.ProposalRequest{ + Title: title, Description: description, Executor: ex, } @@ -50,35 +52,68 @@ func main() { // Output: // -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// # GovDAO Proposals +// +// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// // // -- -// # Prop #0 +// # Proposal #0 - govdao blog post title // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // post a new blogpost about govdao // -// Status: active +// ## Proposal information +// +// **Status: ACTIVE** // -// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%) +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) // -// Threshold met: false +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] // // // -- // -- -// # Prop #0 +// # Proposal #0 - govdao blog post title // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // post a new blogpost about govdao // -// Status: accepted +// ## Proposal information +// +// **Status: ACCEPTED** // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) // -// Threshold met: true +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. // // // -- @@ -87,17 +122,30 @@ func main() { // No posts. // -- // -- -// # Prop #0 +// # Proposal #0 - govdao blog post title // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // post a new blogpost about govdao // -// Status: execution successful +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// ### Actions // -// Threshold met: true +// The voting period for this proposal is over. // // // -- diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 546213431e4..068f520e7e2 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -28,9 +28,11 @@ func init() { } // Create a proposal + title := "new govdao member addition" description := "add new members to the govdao" prop := dao.ProposalRequest{ + Title: title, Description: description, Executor: govdao.NewMemberPropExecutor(memberFn), } @@ -65,57 +67,117 @@ func main() { // -- // 1 // -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// # GovDAO Proposals +// +// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// // // -- -// # Prop #0 +// # Proposal #0 - new govdao member addition // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // add new members to the govdao // -// Status: active +// ## Proposal information +// +// **Status: ACTIVE** +// +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) +// // -// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%) +// **Threshold met: FALSE** // -// Threshold met: false +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] // // // -- // -- -// # Prop #0 +// # Proposal #0 - new govdao member addition // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // add new members to the govdao // -// Status: accepted +// ## Proposal information +// +// **Status: ACCEPTED** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// ### Actions // -// Threshold met: true +// The voting period for this proposal is over. // // // -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// # GovDAO Proposals +// +// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0) +// +// **Status: ACCEPTED** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// // // -- // -- -// # Prop #0 +// # Proposal #0 - new govdao member addition // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // add new members to the govdao // -// Status: execution successful +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (25%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 30 (75%) +// // -// Voting stats: YES 10 (25%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 30 (75%) +// **Threshold met: FALSE** // -// Threshold met: false +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. // // // -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// # GovDAO Proposals +// +// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0) +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// // // -- // 4 diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno index 8eff79ffb5a..13ca572c512 100644 --- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -9,8 +9,10 @@ import ( func init() { mExec := params.NewStringPropExecutor("prop1.string", "value1") + title := "Setting prop1.string param" comment := "setting prop1.string param" prop := dao.ProposalRequest{ + Title: title, Description: comment, Executor: mExec, } @@ -36,124 +38,95 @@ func main() { // Output: // new prop 0 // -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// # GovDAO Proposals +// +// ## [Prop #0 - Setting prop1.string param](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// // // -- -// # Prop #0 +// # Proposal #0 - Setting prop1.string param // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // setting prop1.string param // -// Status: active +// ## Proposal information +// +// **Status: ACTIVE** // -// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%) +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) // -// Threshold met: false +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] // // // -- // -- -// # Prop #0 +// # Proposal #0 - Setting prop1.string param // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // setting prop1.string param // -// Status: accepted +// ## Proposal information +// +// **Status: ACCEPTED** // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) // -// Threshold met: true +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. // // // -- // -- -// # Prop #0 +// # Proposal #0 - Setting prop1.string param // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// ## Description // // setting prop1.string param // -// Status: execution successful +// ## Proposal information // -// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// **Status: EXECUTION SUCCESSFUL** // -// Threshold met: true +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. // // - -// Events: -// [ -// { -// "type": "ProposalAdded", -// "attrs": [ -// { -// "key": "proposal-id", -// "value": "0" -// }, -// { -// "key": "proposal-author", -// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" -// } -// ], -// "pkg_path": "gno.land/r/gov/dao/v2", -// "func": "EmitProposalAdded" -// }, -// { -// "type": "VoteAdded", -// "attrs": [ -// { -// "key": "proposal-id", -// "value": "0" -// }, -// { -// "key": "author", -// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" -// }, -// { -// "key": "option", -// "value": "YES" -// } -// ], -// "pkg_path": "gno.land/r/gov/dao/v2", -// "func": "EmitVoteAdded" -// }, -// { -// "type": "ProposalAccepted", -// "attrs": [ -// { -// "key": "proposal-id", -// "value": "0" -// } -// ], -// "pkg_path": "gno.land/r/gov/dao/v2", -// "func": "EmitProposalAccepted" -// }, -// { -// "type": "set", -// "attrs": [ -// { -// "key": "k", -// "value": "prop1.string" -// } -// ], -// "pkg_path": "gno.land/r/sys/params", -// "func": "" -// }, -// { -// "type": "ProposalExecuted", -// "attrs": [ -// { -// "key": "proposal-id", -// "value": "0" -// }, -// { -// "key": "exec-status", -// "value": "accepted" -// } -// ], -// "pkg_path": "gno.land/r/gov/dao/v2", -// "func": "ExecuteProposal" -// } -// ] diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno new file mode 100644 index 00000000000..4cca397e851 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/render.gno @@ -0,0 +1,123 @@ +package govdao + +import ( + "strconv" + "strings" + + "gno.land/p/demo/dao" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" + "gno.land/r/demo/users" +) + +func Render(path string) string { + var out string + + if path == "" { + out += "# GovDAO Proposals\n\n" + numProposals := d.Size() + + if numProposals == 0 { + out += "No proposals found :(" // corner case + return out + } + + offset := uint64(0) + if numProposals >= 10 { + offset = uint64(numProposals) - 10 + } + + // Fetch the last 10 proposals + proposals := d.Proposals(offset, uint64(10)) + for i := len(proposals) - 1; i >= 0; i-- { + prop := proposals[i] + + title := prop.Title() + if len(title) > 40 { + title = title[:40] + "..." + } + + propID := offset + uint64(i) + out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID) + out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String())) + + user := users.GetUserByAddress(prop.Author()) + authorDisplayText := prop.Author().String() + if user != nil { + authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name) + } + + out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText) + + if i != 0 { + out += "---\n\n" + } + } + + return out + } + + // Display the detailed proposal + idx, err := strconv.Atoi(path) + if err != nil { + return "404: Invalid proposal ID" + } + + // Fetch the proposal + prop, err := d.ProposalByID(uint64(idx)) + if err != nil { + return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) + } + + // Render the proposal page + out += renderPropPage(prop, idx) + + return out +} + +func renderPropPage(prop dao.Proposal, idx int) string { + var out string + + out += ufmt.Sprintf("# Proposal #%d - %s\n\n", idx, prop.Title()) + out += prop.Render() + out += renderAuthor(prop) + out += renderActionBar(prop, idx) + out += "\n\n" + + return out +} + +func renderAuthor(p dao.Proposal) string { + var out string + + authorUsername := "" + user := users.GetUserByAddress(p.Author()) + if user != nil { + authorUsername = user.Name + } + + if authorUsername != "" { + out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername) + } else { + out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String()) + } + + return out +} + +func renderActionBar(p dao.Proposal, idx int) string { + var out string + + out += "### Actions\n\n" + if p.Status() == dao.Active { + out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]", + txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"), + txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"), + txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"), + ) + } else { + out += "The voting period for this proposal is over." + } + + return out +} From 43c9116c22de8518055b3eaf94c112ee99377a1e Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 2 Dec 2024 16:23:37 +0100 Subject: [PATCH 275/345] test(gnovm): test performance improvements (#3210) Fix master CI runs, and miscellaneous improvements locally, too. - ci: switch to using `-covermode=set` rather than atomic, as it significantly degrades performance while not being shown on codecov. [more info](https://github.com/gnolang/gno/pull/3210#issuecomment-2511455953) - gnolang tests: use `t.Parallel()` to parallelize known "long" tests, both in `-short` and long versions. - stdlibs: provide `unicode` native shims for some common functions used in some standard library tests. This may lead to some small inconsistencies between on-chain behaviour and off-chain should the `unicode` packages diverge; but I think we might we might want to consider a native-based `unicode` stdlib, anyway. - thanks to these improvements, there is no longer the need to run `-short` on PRs, as the CI runs in ~9 mins, ie. 8 minutes less than the gno.land tests. --- .github/workflows/gnovm.yml | 2 - .github/workflows/test_template.yml | 5 +- gnovm/pkg/gnolang/files_test.go | 79 ++++++++++++---- gnovm/pkg/test/test.go | 2 + gnovm/stdlibs/bytes/compare_test.gno | 2 + gnovm/tests/stdlibs/generated.go | 114 ++++++++++++++++++++++++ gnovm/tests/stdlibs/unicode/natives.gno | 8 ++ gnovm/tests/stdlibs/unicode/natives.go | 8 ++ 8 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 gnovm/tests/stdlibs/unicode/natives.gno create mode 100644 gnovm/tests/stdlibs/unicode/natives.go diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 8311d113047..7e7586b23d9 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -13,8 +13,6 @@ jobs: uses: ./.github/workflows/main_template.yml with: modulepath: "gnovm" - # in pull requests, append -short so that the CI runs quickly. - tests-extra-args: ${{ github.event_name == 'pull_request' && '-short' || '' }} secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} fmt: diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml index ccbae792c78..c7956b4caf4 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -41,11 +41,14 @@ jobs: # Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags. export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..." + # codecov only supports "boolean" coverage (whether a line is + # covered or not); so using -covermode=count or atomic would be + # pointless here. # XXX: Simplify coverage of txtar - the current setup is a bit # confusing and meticulous. There will be some improvements in Go # 1.23 regarding coverage, so we can use this as a workaround until # then. - go test -covermode=atomic -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR + go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR # Print results (set +x; echo 'go coverage results:') diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go index f1bc87d21d8..09be600b198 100644 --- a/gnovm/pkg/gnolang/files_test.go +++ b/gnovm/pkg/gnolang/files_test.go @@ -33,19 +33,26 @@ func (nopReader) Read(p []byte) (int, error) { return 0, io.EOF } // fix a specific test: // go test -run TestFiles/'^bin1.gno' -short -v -update-golden-tests . func TestFiles(t *testing.T) { + t.Parallel() + rootDir, err := filepath.Abs("../../../") require.NoError(t, err) - opts := &test.TestOptions{ - RootDir: rootDir, - Output: io.Discard, - Error: io.Discard, - Sync: *withSync, + newOpts := func() *test.TestOptions { + o := &test.TestOptions{ + RootDir: rootDir, + Output: io.Discard, + Error: io.Discard, + Sync: *withSync, + } + o.BaseStore, o.TestStore = test.Store( + rootDir, true, + nopReader{}, o.WriterForStore(), io.Discard, + ) + return o } - opts.BaseStore, opts.TestStore = test.Store( - rootDir, true, - nopReader{}, opts.WriterForStore(), io.Discard, - ) + // sharedOpts is used for all "short" tests. + sharedOpts := newOpts() dir := "../../tests/" fsys := os.DirFS(dir) @@ -59,7 +66,8 @@ func TestFiles(t *testing.T) { return nil } subTestName := path[len("files/"):] - if strings.HasSuffix(path, "_long.gno") && testing.Short() { + isLong := strings.HasSuffix(path, "_long.gno") + if isLong && testing.Short() { t.Run(subTestName, func(t *testing.T) { t.Skip("skipping in -short") }) @@ -73,6 +81,12 @@ func TestFiles(t *testing.T) { var criticalError error t.Run(subTestName, func(t *testing.T) { + opts := sharedOpts + if isLong { + // Long tests are run in parallel, and with their own store. + t.Parallel() + opts = newOpts() + } changed, err := opts.RunFiletest(path, content) if err != nil { t.Fatal(err.Error()) @@ -94,16 +108,24 @@ func TestFiles(t *testing.T) { // TestStdlibs tests all the standard library packages. func TestStdlibs(t *testing.T) { + t.Parallel() + rootDir, err := filepath.Abs("../../../") require.NoError(t, err) - var capture bytes.Buffer - out := io.Writer(&capture) - if testing.Verbose() { - out = os.Stdout + newOpts := func() (capture *bytes.Buffer, opts *test.TestOptions) { + var out io.Writer + if testing.Verbose() { + out = os.Stdout + } else { + capture = new(bytes.Buffer) + out = capture + } + opts = test.NewTestOptions(rootDir, nopReader{}, out, out) + opts.Verbose = true + return } - opts := test.NewTestOptions(rootDir, nopReader{}, out, out) - opts.Verbose = true + sharedCapture, sharedOpts := newOpts() dir := "../../stdlibs/" fsys := os.DirFS(dir) @@ -118,12 +140,31 @@ func TestStdlibs(t *testing.T) { fp := filepath.Join(dir, path) memPkg := gnolang.ReadMemPackage(fp, path) t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) { - if testing.Short() { - switch memPkg.Path { - case "bytes", "strconv", "regexp/syntax": + capture, opts := sharedCapture, sharedOpts + switch memPkg.Path { + // Excluded in short + case + "bufio", + "bytes", + "strconv": + if testing.Short() { t.Skip("Skipped because of -short, and this stdlib is very long currently.") } + fallthrough + // Run using separate store, as it's faster + case + "math/rand", + "regexp", + "regexp/syntax", + "sort": + t.Parallel() + capture, opts = newOpts() + } + + if capture != nil { + capture.Reset() } + err := test.Test(memPkg, "", opts) if !testing.Verbose() { t.Log(capture.String()) diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 9374db263ee..5de37a68405 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -284,6 +284,8 @@ func (opts *TestOptions) runTestFiles( if opts.Metrics { alloc = gno.NewAllocator(math.MaxInt64) } + // reset store ops, if any - we only need them for some filetests. + opts.TestStore.SetLogStoreOps(false) // Check if we already have the package - it may have been eagerly // loaded. diff --git a/gnovm/stdlibs/bytes/compare_test.gno b/gnovm/stdlibs/bytes/compare_test.gno index f2b1e7c692b..5ebeba33889 100644 --- a/gnovm/stdlibs/bytes/compare_test.gno +++ b/gnovm/stdlibs/bytes/compare_test.gno @@ -66,6 +66,8 @@ func TestCompareIdenticalSlice(t *testing.T) { } func TestCompareBytes(t *testing.T) { + t.Skip("This test takes very long to run on Gno at time of writing, even in its short form") + lengths := make([]int, 0) // lengths to test in ascending order for i := 0; i <= 128; i++ { lengths = append(lengths, i) diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go index 2cc904a9170..db5ecdec05d 100644 --- a/gnovm/tests/stdlibs/generated.go +++ b/gnovm/tests/stdlibs/generated.go @@ -9,6 +9,7 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" testlibs_std "github.com/gnolang/gno/gnovm/tests/stdlibs/std" testlibs_testing "github.com/gnolang/gno/gnovm/tests/stdlibs/testing" + testlibs_unicode "github.com/gnolang/gno/gnovm/tests/stdlibs/unicode" ) // NativeFunc represents a function in the standard library which has a native @@ -325,6 +326,118 @@ var nativeFuncs = [...]NativeFunc{ func(m *gno.Machine) { r0 := testlibs_testing.X_unixNano() + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, + { + "unicode", + "IsPrint", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("rune")}, + }, + []gno.FieldTypeExpr{ + {Name: gno.N("r0"), Type: gno.X("bool")}, + }, + false, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 rune + rp0 = reflect.ValueOf(&p0).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + + r0 := testlibs_unicode.IsPrint(p0) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, + { + "unicode", + "IsGraphic", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("rune")}, + }, + []gno.FieldTypeExpr{ + {Name: gno.N("r0"), Type: gno.X("bool")}, + }, + false, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 rune + rp0 = reflect.ValueOf(&p0).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + + r0 := testlibs_unicode.IsGraphic(p0) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, + { + "unicode", + "SimpleFold", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("rune")}, + }, + []gno.FieldTypeExpr{ + {Name: gno.N("r0"), Type: gno.X("rune")}, + }, + false, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 rune + rp0 = reflect.ValueOf(&p0).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + + r0 := testlibs_unicode.SimpleFold(p0) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, + { + "unicode", + "IsUpper", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("rune")}, + }, + []gno.FieldTypeExpr{ + {Name: gno.N("r0"), Type: gno.X("bool")}, + }, + false, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 rune + rp0 = reflect.ValueOf(&p0).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + + r0 := testlibs_unicode.IsUpper(p0) + m.PushValue(gno.Go2GnoValue( m.Alloc, m.Store, @@ -337,6 +450,7 @@ var nativeFuncs = [...]NativeFunc{ var initOrder = [...]string{ "std", "testing", + "unicode", } // InitOrder returns the initialization order of the standard libraries. diff --git a/gnovm/tests/stdlibs/unicode/natives.gno b/gnovm/tests/stdlibs/unicode/natives.gno new file mode 100644 index 00000000000..c7efaac70cc --- /dev/null +++ b/gnovm/tests/stdlibs/unicode/natives.gno @@ -0,0 +1,8 @@ +package unicode + +// Optimized as native bindings in tests. + +func IsPrint(r rune) bool +func IsGraphic(r rune) bool +func SimpleFold(r rune) rune +func IsUpper(r rune) bool diff --git a/gnovm/tests/stdlibs/unicode/natives.go b/gnovm/tests/stdlibs/unicode/natives.go new file mode 100644 index 00000000000..e627f4fe6be --- /dev/null +++ b/gnovm/tests/stdlibs/unicode/natives.go @@ -0,0 +1,8 @@ +package unicode + +import "unicode" + +func IsPrint(r rune) bool { return unicode.IsPrint(r) } +func IsGraphic(r rune) bool { return unicode.IsGraphic(r) } +func SimpleFold(r rune) rune { return unicode.SimpleFold(r) } +func IsUpper(r rune) bool { return unicode.IsUpper(r) } From 8fa4997cafa486fda99b899436ef9805eb59313d Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:57:35 +0100 Subject: [PATCH 276/345] ci: add debug on github-bot matrix subcommand + fixes (#3244) This PR will allow debugging errors of [this type](https://github.com/gnolang/gno/actions/runs/12072757244) that unfortunately cannot be tested locally since they rely on the context of GitHub Actions. Since I also had to add flags to the matrix subcommand, I moved the two matrix and check subcommands into subfolders. This PR also modify the comment to stick to moul's request and fixes several Github Actions errors. Related to #3238 Changes: - https://github.com/gnolang/gno/pull/3244/commits/d11ad5a08e457921907e3db32b8576921dde8563 moves matrix and check subcommands to their own packages in internal - https://github.com/gnolang/gno/pull/3244/commits/462ac01321ff15e34cbe956a7ecc07096e665e28 https://github.com/gnolang/gno/pull/3244/commits/5c1edda51950c74c8bccb7eb8c16c036df3bd1f7 https://github.com/gnolang/gno/pull/3244/commits/ffdce936c39c1ad587f0ed17158f579b4ded067e adds a debug to matrix subcommand (print event input / matrix output) + direct output of matrix to GitHub Actions using a matrix-key flag - https://github.com/gnolang/gno/pull/3244/commits/6af501d4cd923c122e8ea6791ab58f394e2bbf1f embed comment template file as a string at compile time instead of opening it at runtime - https://github.com/gnolang/gno/pull/3244/commits/59c3ad6835191cae92dc811de4484b6a6793ea74 modifies bot comment to meet [this requirements](https://github.com/gnolang/gno/issues/3238#issuecomment-2506520120) - https://github.com/gnolang/gno/pull/3244/commits/241a75532ce5e035ac745b4cd66f3bea2d9a420f filter out from the matrix generation and the PR processing all issues or closed PRs (process / list only opened PRs)
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan --- .github/workflows/bot.yml | 4 +- contribs/github-bot/README.md | 4 +- contribs/github-bot/comment.tmpl | 51 ------- .../github-bot/{ => internal/check}/check.go | 62 +++----- .../{params/params.go => check/cmd.go} | 75 ++++++---- .../{ => internal/check}/comment.go | 43 +++--- .../github-bot/internal/check/comment.tmpl | 54 +++++++ .../{ => internal/check}/comment_test.go | 41 ++++-- contribs/github-bot/internal/client/client.go | 52 +++++-- .../{ => internal/config}/config.go | 60 ++++---- contribs/github-bot/internal/matrix/cmd.go | 53 +++++++ contribs/github-bot/internal/matrix/matrix.go | 139 ++++++++++++++++++ .../{ => internal/matrix}/matrix_test.go | 45 +++--- .../internal/requirements/assignee_test.go | 2 +- .../internal/requirements/branch.go | 2 +- .../internal/requirements/label_test.go | 2 +- contribs/github-bot/internal/utils/actions.go | 2 +- .../github-bot/internal/utils/github_const.go | 6 +- .../internal/{params => utils}/prlist.go | 3 +- contribs/github-bot/main.go | 24 ++- contribs/github-bot/matrix.go | 117 --------------- 21 files changed, 490 insertions(+), 351 deletions(-) delete mode 100644 contribs/github-bot/comment.tmpl rename contribs/github-bot/{ => internal/check}/check.go (78%) rename contribs/github-bot/internal/{params/params.go => check/cmd.go} (56%) rename contribs/github-bot/{ => internal/check}/comment.go (90%) create mode 100644 contribs/github-bot/internal/check/comment.tmpl rename contribs/github-bot/{ => internal/check}/comment_test.go (86%) rename contribs/github-bot/{ => internal/config}/config.go (54%) create mode 100644 contribs/github-bot/internal/matrix/cmd.go create mode 100644 contribs/github-bot/internal/matrix/matrix.go rename contribs/github-bot/{ => internal/matrix}/matrix_test.go (91%) rename contribs/github-bot/internal/{params => utils}/prlist.go (91%) delete mode 100644 contribs/github-bot/matrix.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 21950459ae8..cbfec5730fc 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -55,13 +55,15 @@ jobs: working-directory: contribs/github-bot env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: echo "pr-numbers=$(go run . matrix)" >> "$GITHUB_OUTPUT" + run: go run . matrix -matrix-key 'pr-numbers' -verbose # This job processes each pull request in the matrix individually while ensuring # that a same PR cannot be processed concurrently by mutliple runners process-pr: name: Process PR needs: define-prs-matrix + # Just skip this job if PR numbers matrix is empty (prevent failed state) + if: ${{ needs.define-prs-matrix.outputs.pr-numbers != '[]' && needs.define-prs-matrix.outputs.pr-numbers != '' }} runs-on: ubuntu-latest strategy: matrix: diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index 78c9c3c01b8..7932300cb9d 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -13,7 +13,7 @@ The bot operates by defining a set of rules that are evaluated against each pull - **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members. - **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files. -The bot configuration is defined in Go and is located in the file [config.go](./config.go). +The bot configuration is defined in Go and is located in the file [config.go](./internal/config/config.go). ### GitHub Token @@ -31,7 +31,7 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token USAGE github-bot check [flags] -This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. +This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly. A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. FLAGS diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl deleted file mode 100644 index ebd07fdd4b9..00000000000 --- a/contribs/github-bot/comment.tmpl +++ /dev/null @@ -1,51 +0,0 @@ -# Merge Requirements - -The following requirements must be fulfilled before a pull request can be merged. -Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member. - -These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go). - -## Automated Checks - -{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} -{{ end }} - -{{ if .AutoRules }}
Details
-{{ range .AutoRules }} -
{{ .Description | stripLinks }}
- -### If -``` -{{ .ConditionDetails | stripLinks }} -``` -### Then -``` -{{ .RequirementDetails | stripLinks }} -``` -
-{{ end }} -
-{{ else }}*No automated checks match this pull request.*{{ end }} - -## Manual Checks - -{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} -{{ end }} - -{{ if .ManualRules }}
Details
-{{ range .ManualRules }} -
{{ .Description | stripLinks }}
- -### If -``` -{{ .ConditionDetails }} -``` -### Can be checked by -{{range $item := .Teams }} - team {{ $item | stripLinks }} -{{ else }} -- Any user with comment edit permission -{{end}} -
-{{ end }} -
-{{ else }}*No manual checks match this pull request.*{{ end }} diff --git a/contribs/github-bot/check.go b/contribs/github-bot/internal/check/check.go similarity index 78% rename from contribs/github-bot/check.go rename to contribs/github-bot/internal/check/check.go index 8019246d27c..5ca2235e823 100644 --- a/contribs/github-bot/check.go +++ b/contribs/github-bot/internal/check/check.go @@ -1,4 +1,4 @@ -package main +package check import ( "context" @@ -9,44 +9,30 @@ import ( "sync/atomic" "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/config" "github.com/gnolang/gno/contribs/github-bot/internal/logger" - p "github.com/gnolang/gno/contribs/github-bot/internal/params" "github.com/gnolang/gno/contribs/github-bot/internal/utils" - "github.com/gnolang/gno/tm2/pkg/commands" "github.com/google/go-github/v64/github" "github.com/sethvargo/go-githubactions" "github.com/xlab/treeprint" ) -func newCheckCmd() *commands.Command { - params := &p.Params{} - - return commands.NewCommand( - commands.Metadata{ - Name: "check", - ShortUsage: "github-bot check [flags]", - ShortHelp: "checks requirements for a pull request to be merged", - LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.", - }, - params, - func(_ context.Context, _ []string) error { - params.ValidateFlags() - return execCheck(params) - }, - ) -} - -func execCheck(params *p.Params) error { +func execCheck(flags *checkFlags) error { // Create context with timeout if specified in the parameters. ctx := context.Background() - if params.Timeout > 0 { + if flags.Timeout > 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), params.Timeout) + ctx, cancel = context.WithTimeout(context.Background(), flags.Timeout) defer cancel() } // Init GitHub API client. - gh, err := client.New(ctx, params) + gh, err := client.New(ctx, &client.Config{ + Owner: flags.Owner, + Repo: flags.Repo, + Verbose: *flags.Verbose, + DryRun: flags.DryRun, + }) if err != nil { return fmt.Errorf("comment update handling failed: %w", err) } @@ -69,7 +55,7 @@ func execCheck(params *p.Params) error { var prs []*github.PullRequest // If requested, retrieve all open pull requests. - if params.PRAll { + if flags.PRAll { prs, err = gh.ListPR(utils.PRStateOpen) if err != nil { return fmt.Errorf("unable to list all PR: %w", err) @@ -77,11 +63,11 @@ func execCheck(params *p.Params) error { } else { // Otherwise, retrieve only specified pull request(s) // (flag or GitHub Action context). - prs = make([]*github.PullRequest, len(params.PRNums)) - for i, prNum := range params.PRNums { - pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) + prs = make([]*github.PullRequest, len(flags.PRNums)) + for i, prNum := range flags.PRNums { + pr, err := gh.GetOpenedPullRequest(prNum) if err != nil { - return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) + return fmt.Errorf("unable to process PR list: %w", err) } prs[i] = pr } @@ -101,7 +87,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { } // Process all pull requests in parallel. - autoRules, manualRules := config(gh) + autoRules, manualRules := config.Config(gh) var wg sync.WaitGroup // Used in dry-run mode to log cleanly from different goroutines. @@ -122,15 +108,15 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) // Check if conditions of this rule are met by this PR. - if !autoRule.ifC.IsMet(pr, ifDetails) { + if !autoRule.If.IsMet(pr, ifDetails) { continue } - c := AutoContent{Description: autoRule.description, Satisfied: false} + c := AutoContent{Description: autoRule.Description, Satisfied: false} thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail)) // Check if requirements of this rule are satisfied by this PR. - if autoRule.thenR.IsSatisfied(pr, thenDetails) { + if autoRule.Then.IsSatisfied(pr, thenDetails) { thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) c.Satisfied = true } else { @@ -153,13 +139,13 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) // Check if conditions of this rule are met by this PR. - if !manualRule.ifC.IsMet(pr, ifDetails) { + if !manualRule.If.IsMet(pr, ifDetails) { continue } // Get check status from current comment, if any. checkedBy := "" - check, ok := checks[manualRule.description] + check, ok := checks[manualRule.Description] if ok { checkedBy = check.checkedBy } @@ -167,10 +153,10 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { commentContent.ManualRules = append( commentContent.ManualRules, ManualContent{ - Description: manualRule.description, + Description: manualRule.Description, ConditionDetails: ifDetails.String(), CheckedBy: checkedBy, - Teams: manualRule.teams, + Teams: manualRule.Teams, }, ) diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/check/cmd.go similarity index 56% rename from contribs/github-bot/internal/params/params.go rename to contribs/github-bot/internal/check/cmd.go index c11d1b62419..7ea6c02795b 100644 --- a/contribs/github-bot/internal/params/params.go +++ b/contribs/github-bot/internal/check/cmd.go @@ -1,118 +1,131 @@ -package params +package check import ( + "context" "flag" "fmt" "os" "time" "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/gnolang/gno/tm2/pkg/commands" "github.com/sethvargo/go-githubactions" ) -type Params struct { +type checkFlags struct { Owner string Repo string PRAll bool - PRNums PRList - Verbose bool + PRNums utils.PRList + Verbose *bool DryRun bool Timeout time.Duration flagSet *flag.FlagSet } -func (p *Params) RegisterFlags(fs *flag.FlagSet) { +func NewCheckCmd(verbose *bool) *commands.Command { + flags := &checkFlags{Verbose: verbose} + + return commands.NewCommand( + commands.Metadata{ + Name: "check", + ShortUsage: "github-bot check [flags]", + ShortHelp: "checks requirements for a pull request to be merged", + LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.", + }, + flags, + func(_ context.Context, _ []string) error { + flags.validateFlags() + return execCheck(flags) + }, + ) +} + +func (flags *checkFlags) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( - &p.Owner, + &flags.Owner, "owner", "", "owner of the repo to process, if empty, will be retrieved from GitHub Actions context", ) fs.StringVar( - &p.Repo, + &flags.Repo, "repo", "", "repo to process, if empty, will be retrieved from GitHub Actions context", ) fs.BoolVar( - &p.PRAll, + &flags.PRAll, "pr-all", false, "process all opened pull requests", ) fs.TextVar( - &p.PRNums, + &flags.PRNums, "pr-numbers", - PRList(nil), + utils.PRList(nil), "pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context", ) fs.BoolVar( - &p.Verbose, - "verbose", - false, - "set logging level to debug", - ) - - fs.BoolVar( - &p.DryRun, + &flags.DryRun, "dry-run", false, "print if pull request requirements are satisfied without updating anything on GitHub", ) fs.DurationVar( - &p.Timeout, + &flags.Timeout, "timeout", 0, "timeout after which the bot execution is interrupted", ) - p.flagSet = fs + flags.flagSet = fs } -func (p *Params) ValidateFlags() { +func (flags *checkFlags) validateFlags() { // Helper to display an error + usage message before exiting. errorUsage := func(err string) { - fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err) - p.flagSet.Usage() + fmt.Fprintf(flags.flagSet.Output(), "Error: %s\n\n", err) + flags.flagSet.Usage() os.Exit(1) } // Check if flags are coherent. - if p.PRAll && len(p.PRNums) != 0 { + if flags.PRAll && len(flags.PRNums) != 0 { errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.") } // If one of these values is empty, it must be retrieved // from GitHub Actions context. - if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) { + if flags.Owner == "" || flags.Repo == "" || (len(flags.PRNums) == 0 && !flags.PRAll) { actionCtx, err := githubactions.Context() if err != nil { errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err)) } - if p.Owner == "" { - if p.Owner, _ = actionCtx.Repo(); p.Owner == "" { + if flags.Owner == "" { + if flags.Owner, _ = actionCtx.Repo(); flags.Owner == "" { errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.") } } - if p.Repo == "" { - if _, p.Repo = actionCtx.Repo(); p.Repo == "" { + if flags.Repo == "" { + if _, flags.Repo = actionCtx.Repo(); flags.Repo == "" { errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.") } } - if len(p.PRNums) == 0 && !p.PRAll { + if len(flags.PRNums) == 0 && !flags.PRAll { prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) if err != nil { errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error())) } - p.PRNums = PRList{prNum} + flags.PRNums = utils.PRList{prNum} } } } diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/internal/check/comment.go similarity index 90% rename from contribs/github-bot/comment.go rename to contribs/github-bot/internal/check/comment.go index f6605ea8554..434df8f9e76 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/internal/check/comment.go @@ -1,7 +1,8 @@ -package main +package check import ( "bytes" + _ "embed" "errors" "fmt" "regexp" @@ -9,12 +10,15 @@ import ( "text/template" "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/config" "github.com/gnolang/gno/contribs/github-bot/internal/utils" - "github.com/google/go-github/v64/github" "github.com/sethvargo/go-githubactions" ) +//go:embed comment.tmpl +var tmplString string // Embed template used for comment generation. + var errTriggeredByBot = errors.New("event triggered by bot") // Compile regex only once. @@ -95,6 +99,18 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte return nil } + // Get PR number from GitHub Actions context. + prNumFloat, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64) + if !ok || prNumFloat <= 0 { + return errors.New("unable to get issue number on issue comment event") + } + prNum := int(prNumFloat) + + // Ignore if this comment update is not related to an opened PR. + if _, err := gh.GetOpenedPullRequest(prNum); err != nil { + return nil // May come from an issue or a closed PR + } + // Return if comment was edited by bot (current authenticated user). authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { @@ -129,17 +145,11 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte return errors.New("unable to get changes body content on issue comment event") } - // Get PR number from GitHub Actions context. - prNum, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64) - if !ok || prNum <= 0 { - return errors.New("unable to get issue number on issue comment event") - } - // Check if change is only a checkbox being checked or unckecked. if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { // If not, restore previous comment body. if !gh.DryRun { - gh.SetBotComment(previous, int(prNum)) + gh.SetBotComment(previous, prNum) } return errors.New("bot comment edited outside of checkboxes") } @@ -157,12 +167,12 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte // Get teams allowed to edit this box from config. var teams []string found := false - _, manualRules := config(gh) + _, manualRules := config.Config(gh) for _, manualRule := range manualRules { - if manualRule.description == key { + if manualRule.Description == key { found = true - teams = manualRule.teams + teams = manualRule.Teams } } @@ -175,9 +185,9 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte // If teams specified in rule, check if actor is a member of one of them. if len(teams) > 0 { - if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed + if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed to check the boxes. if !gh.DryRun { - gh.SetBotComment(previous, int(prNum)) // Restore previous state + gh.SetBotComment(previous, prNum) // Then restore previous state. } return errors.New("checkbox edited by a user not allowed to") } @@ -199,7 +209,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte // Update comment with username. if edited != "" && !gh.DryRun { - gh.SetBotComment(edited, int(prNum)) + gh.SetBotComment(edited, prNum) gh.Logger.Debugf("Comment manual checks updated successfully") } @@ -217,8 +227,7 @@ func generateComment(content CommentContent) (string, error) { } // Bind markdown stripping function to template generator. - const tmplFile = "comment.tmpl" - tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) + tmpl, err := template.New("comment").Funcs(funcMap).Parse(tmplString) if err != nil { return "", fmt.Errorf("unable to init template: %w", err) } diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl new file mode 100644 index 00000000000..4312019dd2e --- /dev/null +++ b/contribs/github-bot/internal/check/comment.tmpl @@ -0,0 +1,54 @@ +I'm a bot that assists the Gno Core team in maintaining this repository. My role is to ensure that contributors understand and follow our guidelines, helping to streamline the development process. + +The following requirements must be fulfilled before a pull request can be merged. +Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member. + +These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). + +## Automated Checks + +{{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} +{{ end }}{{ else }}*No automated checks match this pull request.*{{ end }} + +## Manual Checks + +{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} +{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }} + +{{ if or .AutoRules .ManualRules }}
Debug
+{{ if .AutoRules }}
Automated Checks
+{{ range .AutoRules }} +
{{ .Description | stripLinks }}
+ +### If +``` +{{ .ConditionDetails | stripLinks }} +``` +### Then +``` +{{ .RequirementDetails | stripLinks }} +``` +
+{{ end }} +
+{{ end }} + +{{ if .ManualRules }}
Manual Checks
+{{ range .ManualRules }} +
{{ .Description | stripLinks }}
+ +### If +``` +{{ .ConditionDetails }} +``` +### Can be checked by +{{range $item := .Teams }} - team {{ $item | stripLinks }} +{{ else }} +- Any user with comment edit permission +{{end}} +
+{{ end }} +
+{{ end }} +
+{{ end }} diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/internal/check/comment_test.go similarity index 86% rename from contribs/github-bot/comment_test.go rename to contribs/github-bot/internal/check/comment_test.go index fd8790dd9e1..0334b76f95c 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/internal/check/comment_test.go @@ -1,4 +1,4 @@ -package main +package check import ( "context" @@ -108,19 +108,34 @@ func TestCommentUpdateHandler(t *testing.T) { } gh := newGHClient() - // Exit without error because EventName is empty + // Exit without error because EventName is empty. assert.NoError(t, handleCommentUpdate(gh, actionCtx)) actionCtx.EventName = utils.EventIssueComment - // Exit with error because Event.action is not set + // Exit with error because Event.action is not set. assert.Error(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event["action"] = "" - // Exit without error because Event.action is set but not 'deleted' + // Exit without error because Event.action is set but not 'deleted'. assert.NoError(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event["action"] = "deleted" - // Exit with error because mock not setup to return authUser + // Exit with error because Event.issue.number is not set. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number") + + // Exit without error can't get open pull request associated with PR num. + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + mockOptions = append(mockOptions, mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/42", + Method: "GET", + }, + github.PullRequest{Number: github.Int(42), State: github.String(utils.PRStateOpen)}, + )) + gh = newGHClient() + + // Exit with error because mock not setup to return authUser. assert.Error(t, handleCommentUpdate(gh, actionCtx)) mockOptions = append(mockOptions, mock.WithRequestMatchPages( mock.EndpointPattern{ @@ -132,31 +147,27 @@ func TestCommentUpdateHandler(t *testing.T) { gh = newGHClient() actionCtx.Actor = bot - // Exit with error because authUser and action actor is the same user + // Exit with error because authUser and action actor is the same user. assert.ErrorIs(t, handleCommentUpdate(gh, actionCtx), errTriggeredByBot) actionCtx.Actor = user - // Exit with error because Event.comment.user.login is not set + // Exit with error because Event.comment.user.login is not set. assert.Error(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event = setValue(t, actionCtx.Event, user, "comment", "user", "login") - // Exit without error because comment author is not the bot + // Exit without error because comment author is not the bot. assert.NoError(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event = setValue(t, actionCtx.Event, bot, "comment", "user", "login") - // Exit with error because Event.comment.body is not set + // Exit with error because Event.comment.body is not set. assert.Error(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "comment", "body") - // Exit with error because Event.changes.body.from is not set + // Exit with error because Event.changes.body.from is not set. assert.Error(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event = setValue(t, actionCtx.Event, "updated_body", "changes", "body", "from") - // Exit with error because Event.issue.number is not set - assert.Error(t, handleCommentUpdate(gh, actionCtx)) - actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number") - - // Exit with error because checkboxes are differents + // Exit with error because checkboxes are differents. assert.Error(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from") diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go index 474146ad3da..a5c875e0d22 100644 --- a/contribs/github-bot/internal/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -7,8 +7,7 @@ import ( "os" "github.com/gnolang/gno/contribs/github-bot/internal/logger" - p "github.com/gnolang/gno/contribs/github-bot/internal/params" - + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" ) @@ -32,21 +31,28 @@ type GitHub struct { Repo string } +type Config struct { + Owner string + Repo string + Verbose bool + DryRun bool +} + // GetBotComment retrieves the bot's (current user) comment on provided PR number. func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { - // List existing comments + // List existing comments. const ( sort = "created" direction = "desc" ) - // Get current user (bot) + // Get current user (bot). currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { return nil, fmt.Errorf("unable to get current user: %w", err) } - // Pagination option + // Pagination option. opts := &github.IssueListCommentsOptions{ Sort: github.String(sort), Direction: github.String(direction), @@ -67,7 +73,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err) } - // Get the comment created by current user + // Get the comment created by current user. for _, comment := range comments { if comment.GetUser().GetLogin() == currentUser.GetLogin() { return comment, nil @@ -86,7 +92,12 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { // SetBotComment creates a bot's comment on the provided PR number // or updates it if it already exists. func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) { - // Create bot comment if it does not already exist + // Prevent updating anything in dry run mode. + if gh.DryRun { + return nil, errors.New("should not write bot comment in dry run mode") + } + + // Create bot comment if it does not already exist. comment, err := gh.GetBotComment(prNum) if errors.Is(err, ErrBotCommentNotFound) { newComment, _, err := gh.Client.Issues.CreateComment( @@ -119,6 +130,17 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e return editComment, nil } +func (gh *GitHub) GetOpenedPullRequest(prNum int) (*github.PullRequest, error) { + pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) + if err != nil { + return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) + } else if pr.GetState() != utils.PRStateOpen { + return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState()) + } + + return pr, nil +} + // ListTeamMembers lists the members of the specified team. func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { var ( @@ -268,25 +290,25 @@ func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) { } // New initializes the API client, the logger, and creates an instance of GitHub. -func New(ctx context.Context, params *p.Params) (*GitHub, error) { +func New(ctx context.Context, cfg *Config) (*GitHub, error) { gh := &GitHub{ Ctx: ctx, - Owner: params.Owner, - Repo: params.Repo, - DryRun: params.DryRun, + Owner: cfg.Owner, + Repo: cfg.Repo, + DryRun: cfg.DryRun, } // Detect if the current process was launched by a GitHub Action and return - // a logger suitable for terminal output or the GitHub Actions web interface - gh.Logger = logger.NewLogger(params.Verbose) + // a logger suitable for terminal output or the GitHub Actions web interface. + gh.Logger = logger.NewLogger(cfg.Verbose) - // Retrieve GitHub API token from env + // Retrieve GitHub API token from env. token, set := os.LookupEnv("GITHUB_TOKEN") if !set { return nil, errors.New("GITHUB_TOKEN is not set in env") } - // Init GitHub API client using token + // Init GitHub API client using token. gh.Client = github.NewClient(nil).WithAuthToken(token) return gh, nil diff --git a/contribs/github-bot/config.go b/contribs/github-bot/internal/config/config.go similarity index 54% rename from contribs/github-bot/config.go rename to contribs/github-bot/internal/config/config.go index 4a28565ef7f..ac1d185f759 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -1,4 +1,4 @@ -package main +package config import ( "github.com/gnolang/gno/contribs/github-bot/internal/client" @@ -9,37 +9,37 @@ import ( type Teams []string // Automatic check that will be performed by the bot. -type automaticCheck struct { - description string - ifC c.Condition // If the condition is met, the rule is displayed and the requirement is executed. - thenR r.Requirement // If the requirement is satisfied, the check passes. +type AutomaticCheck struct { + Description string + If c.Condition // If the condition is met, the rule is displayed and the requirement is executed. + Then r.Requirement // If the requirement is satisfied, the check passes. } // Manual check that will be performed by users. -type manualCheck struct { - description string - ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment. - teams Teams // Members of these teams can check the checkbox to make the check pass. +type ManualCheck struct { + Description string + If c.Condition // If the condition is met, a checkbox will be displayed on bot comment. + Teams Teams // Members of these teams can check the checkbox to make the check pass. } // This function returns the configuration of the bot consisting of automatic and manual checks // in which the GitHub client is injected. -func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { - auto := []automaticCheck{ +func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { + auto := []AutomaticCheck{ { - description: "Maintainers must be able to edit this pull request", - ifC: c.Always(), - thenR: r.MaintainerCanModify(), + Description: "Maintainers must be able to edit this pull request", + If: c.Always(), + Then: r.MaintainerCanModify(), }, { - description: "The pull request head branch must be up-to-date with its base", - ifC: c.Always(), - thenR: r.UpToDateWith(gh, r.PR_BASE), + Description: "The pull request head branch must be up-to-date with its base", + If: c.Always(), + Then: r.UpToDateWith(gh, r.PR_BASE), }, { - description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", - ifC: c.FileChanged(gh, "^docs/"), - thenR: r.Or( + Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", + If: c.FileChanged(gh, "^docs/"), + Then: r.Or( r.And( r.AuthorInTeam(gh, "devrels"), r.ReviewByTeamMembers(gh, "tech-staff", 1), @@ -52,15 +52,15 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { }, } - manual := []manualCheck{ + manual := []ManualCheck{ { - description: "The pull request description provides enough details", - ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")), - teams: Teams{"core-contributors"}, + Description: "The pull request description provides enough details", + If: c.Not(c.AuthorInTeam(gh, "core-contributors")), + Teams: Teams{"core-contributors"}, }, { - description: "Determine if infra needs to be updated before merging", - ifC: c.And( + Description: "Determine if infra needs to be updated before merging", + If: c.And( c.BaseBranch("master"), c.Or( c.FileChanged(gh, `Dockerfile`), @@ -70,17 +70,17 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`), ), ), - teams: Teams{"devops"}, + Teams: Teams{"devops"}, }, } // Check for duplicates in manual rule descriptions (needs to be unique for the bot operations). unique := make(map[string]struct{}) for _, rule := range manual { - if _, exists := unique[rule.description]; exists { - gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.description) + if _, exists := unique[rule.Description]; exists { + gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.Description) } - unique[rule.description] = struct{}{} + unique[rule.Description] = struct{}{} } return auto, manual diff --git a/contribs/github-bot/internal/matrix/cmd.go b/contribs/github-bot/internal/matrix/cmd.go new file mode 100644 index 00000000000..8bcc3a34424 --- /dev/null +++ b/contribs/github-bot/internal/matrix/cmd.go @@ -0,0 +1,53 @@ +package matrix + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type matrixFlags struct { + verbose *bool + matrixKey string + flagSet *flag.FlagSet +} + +func NewMatrixCmd(verbose *bool) *commands.Command { + flags := &matrixFlags{verbose: verbose} + + return commands.NewCommand( + commands.Metadata{ + Name: "matrix", + ShortUsage: "github-bot matrix [flags]", + ShortHelp: "parses GitHub Actions event and defines matrix accordingly", + LongHelp: "This tool retrieves the GitHub Actions context, parses the attached event, and defines the matrix with the pull request numbers to be processed accordingly", + }, + flags, + func(_ context.Context, _ []string) error { + flags.validateFlags() + return execMatrix(flags) + }, + ) +} + +func (flags *matrixFlags) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &flags.matrixKey, + "matrix-key", + "", + "key of the matrix to set in Github Actions output (required)", + ) + + flags.flagSet = fs +} + +func (flags *matrixFlags) validateFlags() { + if flags.matrixKey == "" { + fmt.Fprintf(flags.flagSet.Output(), "Error: no matrix-key provided\n\n") + flags.flagSet.Usage() + os.Exit(1) + } +} diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go new file mode 100644 index 00000000000..9c8f12e4214 --- /dev/null +++ b/contribs/github-bot/internal/matrix/matrix.go @@ -0,0 +1,139 @@ +package matrix + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/sethvargo/go-githubactions" +) + +func execMatrix(flags *matrixFlags) error { + // Get GitHub Actions context to retrieve event. + actionCtx, err := githubactions.Context() + if err != nil { + return fmt.Errorf("unable to get GitHub Actions context: %w", err) + } + + // If verbose is set, print the Github Actions event for debugging purpose. + if *flags.verbose { + jsonBytes, err := json.MarshalIndent(actionCtx.Event, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal event to json: %w", err) + } + fmt.Println("Event:", string(jsonBytes)) + } + + // Init Github client using only GitHub Actions context. + owner, repo := actionCtx.Repo() + gh, err := client.New(context.Background(), &client.Config{ + Owner: owner, + Repo: repo, + Verbose: *flags.verbose, + DryRun: true, + }) + if err != nil { + return fmt.Errorf("unable to init GitHub client: %w", err) + } + + // Retrieve PR list from GitHub Actions event. + prList, err := getPRListFromEvent(gh, actionCtx) + if err != nil { + return err + } + + // Format PR list for GitHub Actions matrix definition. + bytes, err := prList.MarshalText() + if err != nil { + return fmt.Errorf("unable to marshal PR list: %w", err) + } + matrix := fmt.Sprintf("%s=[%s]", flags.matrixKey, string(bytes)) + + // If verbose is set, print the matrix for debugging purpose. + if *flags.verbose { + fmt.Printf("Matrix: %s\n", matrix) + } + + // Get the path of the GitHub Actions environment file used for output. + output, ok := os.LookupEnv("GITHUB_OUTPUT") + if !ok { + return errors.New("unable to get GITHUB_OUTPUT var") + } + + // Open GitHub Actions output file + file, err := os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("unable to open GitHub Actions output file: %w", err) + } + defer file.Close() + + // Append matrix to GitHub Actions output file + if _, err := fmt.Fprintf(file, "%s\n", matrix); err != nil { + return fmt.Errorf("unable to write matrix in GitHub Actions output file: %w", err) + } + + return nil +} + +func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (utils.PRList, error) { + var prList utils.PRList + + switch actionCtx.EventName { + // Event triggered from GitHub Actions user interface. + case utils.EventWorkflowDispatch: + // Get input entered by the user. + rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string) + if !ok { + return nil, errors.New("unable to get workflow dispatch input") + } + input := strings.TrimSpace(rawInput) + + // If all PR are requested, list them from GitHub API. + if input == "all" { + prs, err := gh.ListPR(utils.PRStateOpen) + if err != nil { + return nil, fmt.Errorf("unable to list all PR: %w", err) + } + + prList = make(utils.PRList, len(prs)) + for i := range prs { + prList[i] = prs[i].GetNumber() + } + } else { + // If a PR list is provided, parse it. + if err := prList.UnmarshalText([]byte(input)); err != nil { + return nil, fmt.Errorf("invalid PR list provided as input: %w", err) + } + } + + // Event triggered by an issue / PR comment being created / edited / deleted + // or any update on a PR. + case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget: + // For these events, retrieve the number of the associated PR from the context. + prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) + if err != nil { + return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err) + } + prList = utils.PRList{prNum} + + default: + return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName) + } + + // Then only keep provided PR that are opened. + var openedPRList utils.PRList = nil + for _, prNum := range prList { + if _, err := gh.GetOpenedPullRequest(prNum); err != nil { + gh.Logger.Warningf("Can't get PR from event: %v", err) + } else { + openedPRList = append(openedPRList, prNum) + } + } + + return openedPRList, nil +} diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go similarity index 91% rename from contribs/github-bot/matrix_test.go rename to contribs/github-bot/internal/matrix/matrix_test.go index bce4ec1bd8f..fe5b7452a49 100644 --- a/contribs/github-bot/matrix_test.go +++ b/contribs/github-bot/internal/matrix/matrix_test.go @@ -1,4 +1,4 @@ -package main +package matrix import ( "context" @@ -9,7 +9,6 @@ import ( "github.com/gnolang/gno/contribs/github-bot/internal/client" "github.com/gnolang/gno/contribs/github-bot/internal/logger" - "github.com/gnolang/gno/contribs/github-bot/internal/params" "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -34,7 +33,7 @@ func TestProcessEvent(t *testing.T) { name string gaCtx *githubactions.GitHubContext prs []*github.PullRequest - expectedPRList params.PRList + expectedPRList utils.PRList expectedError bool }{ { @@ -44,7 +43,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"issue": map[string]any{"number": 1.}}, }, prs, - params.PRList{1}, + utils.PRList{1}, false, }, { "valid pull_request event", @@ -53,7 +52,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, }, prs, - params.PRList{1}, + utils.PRList{1}, false, }, { "valid pull_request_target event", @@ -62,7 +61,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, }, prs, - params.PRList{1}, + utils.PRList{1}, false, }, { "invalid event (PR number not set)", @@ -71,7 +70,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"issue": nil}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid event name", @@ -80,7 +79,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"issue": map[string]any{"number": 1.}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "valid workflow_dispatch all", @@ -89,7 +88,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, }, openPRs, - params.PRList{1, 2, 3}, + utils.PRList{1, 2, 3}, false, }, { "valid workflow_dispatch all (no prs)", @@ -98,7 +97,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, }, nil, - params.PRList{}, + utils.PRList(nil), false, }, { "valid workflow_dispatch list", @@ -107,7 +106,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}}, }, prs, - params.PRList{1, 2, 3}, + utils.PRList{1, 2, 3}, false, }, { "valid workflow_dispatch list with spaces", @@ -116,7 +115,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}}, }, prs, - params.PRList{1, 2, 3}, + utils.PRList{1, 2, 3}, false, }, { "invalid workflow_dispatch list (1 closed)", @@ -125,8 +124,8 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}}, }, prs, - params.PRList(nil), - true, + utils.PRList{1, 2, 3}, + false, }, { "invalid workflow_dispatch list (1 doesn't exist)", &githubactions.GitHubContext{ @@ -134,8 +133,8 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}}, }, prs, - params.PRList(nil), - true, + utils.PRList(nil), + false, }, { "invalid workflow_dispatch list (all closed)", &githubactions.GitHubContext{ @@ -143,8 +142,8 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}}, }, prs, - params.PRList(nil), - true, + utils.PRList(nil), + false, }, { "invalid workflow_dispatch list (empty)", &githubactions.GitHubContext{ @@ -152,7 +151,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid workflow_dispatch list (unset)", @@ -161,7 +160,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": ""}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid workflow_dispatch list (not a number list)", @@ -170,7 +169,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid workflow_dispatch list (number list with invalid elem)", @@ -179,7 +178,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, } { @@ -214,7 +213,7 @@ func TestProcessEvent(t *testing.T) { prNumStr := parts[len(parts)-1] prNum, err = strconv.Atoi(prNumStr) if err != nil { - panic(err) // Should never happen + panic(err) // Should never happen. } } diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go index d72e8ad2a19..aa86fb0054d 100644 --- a/contribs/github-bot/internal/requirements/assignee_test.go +++ b/contribs/github-bot/internal/requirements/assignee_test.go @@ -45,7 +45,7 @@ func TestAssignee(t *testing.T) { mock.WithRequestMatchHandler( mock.EndpointPattern{ Pattern: "/repos/issues/0/assignees", - Method: "GET", // It looks like this mock package doesn't support mocking POST requests + Method: "GET", // It looks like this mock package doesn't support mocking POST requests. }, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { requested = true diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go index b686a093015..6481285ae82 100644 --- a/contribs/github-bot/internal/requirements/branch.go +++ b/contribs/github-bot/internal/requirements/branch.go @@ -29,7 +29,7 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre } head := pr.GetHead().GetRef() - // If pull request is open from a fork, prepend head ref with fork owner login + // If pull request is open from a fork, prepend head ref with fork owner login. if pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName() { head = fmt.Sprintf("%s:%s", pr.GetHead().GetRepo().GetOwner().GetLogin(), pr.GetHead().GetRef()) } diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go index 7e991b55756..631bff9e64b 100644 --- a/contribs/github-bot/internal/requirements/label_test.go +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -45,7 +45,7 @@ func TestLabel(t *testing.T) { mock.WithRequestMatchHandler( mock.EndpointPattern{ Pattern: "/repos/issues/0/labels", - Method: "GET", // It looks like this mock package doesn't support mocking POST requests + Method: "GET", // It looks like this mock package doesn't support mocking POST requests. }, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { requested = true diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go index 91b8ac7e6b4..3e08a8e1548 100644 --- a/contribs/github-bot/internal/utils/actions.go +++ b/contribs/github-bot/internal/utils/actions.go @@ -23,7 +23,7 @@ func IndexMap(m map[string]any, keys ...string) any { return nil } -// Retrieve PR number from GitHub Actions context +// Retrieve PR number from GitHub Actions context. func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) { firstKey := "" diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go index 564b7d3fb38..26d7d54d477 100644 --- a/contribs/github-bot/internal/utils/github_const.go +++ b/contribs/github-bot/internal/utils/github_const.go @@ -1,14 +1,14 @@ package utils -// GitHub const +// GitHub API const. const ( - // GitHub Actions Event Names + // GitHub Actions Event Names. EventIssueComment = "issue_comment" EventPullRequest = "pull_request" EventPullRequestTarget = "pull_request_target" EventWorkflowDispatch = "workflow_dispatch" - // Pull Request States + // Pull Request States. PRStateOpen = "open" PRStateClosed = "closed" ) diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/utils/prlist.go similarity index 91% rename from contribs/github-bot/internal/params/prlist.go rename to contribs/github-bot/internal/utils/prlist.go index ace7bcbe3b6..2893bf802b5 100644 --- a/contribs/github-bot/internal/params/prlist.go +++ b/contribs/github-bot/internal/utils/prlist.go @@ -1,4 +1,4 @@ -package params +package utils import ( "encoding" @@ -7,6 +7,7 @@ import ( "strings" ) +// Type used to (un)marshal input/output for check and matrix subcommands. type PRList []int // PRList is both a TextMarshaler and a TextUnmarshaler. diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 9895f44dc70..e11fe6ffd78 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -2,25 +2,43 @@ package main import ( "context" + "flag" "os" + "github.com/gnolang/gno/contribs/github-bot/internal/check" + "github.com/gnolang/gno/contribs/github-bot/internal/matrix" "github.com/gnolang/gno/tm2/pkg/commands" ) +type rootFlags struct { + verbose bool +} + func main() { + flags := &rootFlags{} + cmd := commands.NewCommand( commands.Metadata{ ShortUsage: "github-bot [flags]", LongHelp: "Bot that allows for advanced management of GitHub pull requests.", }, - commands.NewEmptyConfig(), + flags, commands.HelpExec, ) cmd.AddSubCommands( - newCheckCmd(), - newMatrixCmd(), + check.NewCheckCmd(&flags.verbose), + matrix.NewMatrixCmd(&flags.verbose), ) cmd.Execute(context.Background(), os.Args[1:]) } + +func (flags *rootFlags) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &flags.verbose, + "verbose", + false, + "set logging level to debug", + ) +} diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go deleted file mode 100644 index 56d6667589a..00000000000 --- a/contribs/github-bot/matrix.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/gnolang/gno/contribs/github-bot/internal/client" - "github.com/gnolang/gno/contribs/github-bot/internal/params" - "github.com/gnolang/gno/contribs/github-bot/internal/utils" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/sethvargo/go-githubactions" -) - -func newMatrixCmd() *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "matrix", - ShortUsage: "github-bot matrix", - ShortHelp: "parses GitHub Actions event and defines matrix accordingly", - LongHelp: "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.", - }, - commands.NewEmptyConfig(), - func(_ context.Context, _ []string) error { - return execMatrix() - }, - ) -} - -func execMatrix() error { - // Get GitHub Actions context to retrieve event. - actionCtx, err := githubactions.Context() - if err != nil { - return fmt.Errorf("unable to get GitHub Actions context: %w", err) - } - - // Init Github client using only GitHub Actions context - owner, repo := actionCtx.Repo() - gh, err := client.New(context.Background(), ¶ms.Params{Owner: owner, Repo: repo}) - if err != nil { - return fmt.Errorf("unable to init GitHub client: %w", err) - } - - // Retrieve PR list from GitHub Actions event - prList, err := getPRListFromEvent(gh, actionCtx) - if err != nil { - return err - } - - // Print PR list for GitHub Actions matrix definition - bytes, err := prList.MarshalText() - if err != nil { - return fmt.Errorf("unable to marshal PR list: %w", err) - } - fmt.Printf("[%s]", string(bytes)) - - return nil -} - -func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (params.PRList, error) { - var prList params.PRList - - switch actionCtx.EventName { - // Event triggered from GitHub Actions user interface - case utils.EventWorkflowDispatch: - // Get input entered by the user - rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string) - if !ok { - return nil, errors.New("unable to get workflow dispatch input") - } - input := strings.TrimSpace(rawInput) - - // If all PR are requested, list them from GitHub API - if input == "all" { - prs, err := gh.ListPR(utils.PRStateOpen) - if err != nil { - return nil, fmt.Errorf("unable to list all PR: %w", err) - } - - prList = make(params.PRList, len(prs)) - for i := range prs { - prList[i] = prs[i].GetNumber() - } - } else { - // If a PR list is provided, parse it - if err := prList.UnmarshalText([]byte(input)); err != nil { - return nil, fmt.Errorf("invalid PR list provided as input: %w", err) - } - - // Then check if all provided PR are opened - for _, prNum := range prList { - pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) - if err != nil { - return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) - } else if pr.GetState() != utils.PRStateOpen { - return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState()) - } - } - } - - // Event triggered by an issue / PR comment being created / edited / deleted - // or any update on a PR - case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget: - // For these events, retrieve the number of the associated PR from the context - prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) - if err != nil { - return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err) - } - prList = params.PRList{prNum} - - default: - return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName) - } - - return prList, nil -} From a6f1aba4f429a79eb9b895faa45bf5ac53ecd242 Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Tue, 3 Dec 2024 17:19:56 +0700 Subject: [PATCH 277/345] fix(gnovm): handle type alias declaration for PrimitiveType (#3222) Fixes: https://github.com/gnolang/gno/issues/3203
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- gnovm/pkg/gnolang/preprocess.go | 7 ++--- gnovm/tests/files/type40.gno | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 gnovm/tests/files/type40.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 6e82786b318..78b11a4ebc5 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2352,6 +2352,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // } *dst = *dt2 } + case PrimitiveType: + dst = tmp.(PrimitiveType) + case *PointerType: + *dst = *(tmp.(*PointerType)) default: panic(fmt.Sprintf("unexpected type declaration type %v", reflect.TypeOf(dst))) @@ -4283,9 +4287,6 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { // predefineNow preprocessed dependent types. panic("should not happen") } - } else { - // all names are declared types. - panic("should not happen") } } else if idx, ok := UverseNode().GetLocalIndex(tx.Name); ok { // uverse name diff --git a/gnovm/tests/files/type40.gno b/gnovm/tests/files/type40.gno new file mode 100644 index 00000000000..65210798007 --- /dev/null +++ b/gnovm/tests/files/type40.gno @@ -0,0 +1,46 @@ +package main + +type ( + // PrimitiveType + Number = int32 + Number2 = Number + + // PointerType + Pointer = *int32 + Pointer2 = Pointer + + // Interface + Interface = interface{} + Interface2 = Interface + + // S + Struct = struct{Name string} + Struct2 = Struct +) + +func fNumber(n Number) { println(n) } +func fPointer(p Pointer) { println(*p) } +func fInterface(i Interface) { println(i) } +func fStruct(s Struct) { println(s.Name) } + +func main() { + var n Number2 = 5 + fNumber(n) + + var num int32 = 6 + var p Pointer2 = &num + fPointer(p) + + var i Interface2 + i = 7 + fInterface(i) + + var s Struct2 = Struct2{Name: "yo"} + fStruct(s) +} + +// Output: +// 5 +// 6 +// 7 +// yo \ No newline at end of file From bc44a39b174cfb85a4daba7e10e1f9ab07f3858d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:23:02 +0100 Subject: [PATCH 278/345] feat: add grc20reg that works... today (#3135) - [x] Switch to storing a `type XXX func() grc20.Token` instead of a `grc20.Token` directly. - [x] Implement `grc20reg`. - [x] Add new tests in `gnovm/tests` to demonstrate the current VM's management of the cross-realm feature and support potential changes in #2743. - [x] Create a demo in `atomicswap` or a similar application. (https://github.com/gnolang/gno/pull/2510#issuecomment-2480500066) - [x] Try using a `Token.Getter()` helper. (Works! f99654e30) - [ ] Demonstrate how to manage "disappearing" functions during garbage collection by checking if the function pointer is nil or non-resolvable. Alternative to #2516 NOT(!) depending on #2743 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/Makefile | 2 +- examples/gno.land/p/demo/grc/grc20/types.gno | 41 +- examples/gno.land/r/demo/bar20/bar20.gno | 4 +- examples/gno.land/r/demo/bar20/gno.mod | 1 + examples/gno.land/r/demo/foo20/foo20.gno | 4 +- examples/gno.land/r/demo/foo20/gno.mod | 1 + examples/gno.land/r/demo/grc20factory/gno.mod | 1 + .../r/demo/grc20factory/grc20factory.gno | 4 +- examples/gno.land/r/demo/grc20reg/gno.mod | 9 + .../gno.land/r/demo/grc20reg/grc20reg.gno | 76 + .../r/demo/grc20reg/grc20reg_test.gno | 59 + .../r/demo/tests/crossrealm/crossrealm.gno | 28 + .../r/demo/tests/crossrealm_b/crossrealm.gno | 25 + .../r/demo/tests/crossrealm_b/gno.mod | 3 + examples/gno.land/r/demo/tests/tests.gno | 2 + examples/gno.land/r/demo/wugnot/gno.mod | 1 + examples/gno.land/r/demo/wugnot/wugnot.gno | 4 +- gnovm/stdlibs/math/overflow/overflow.gno | 6 +- gnovm/tests/files/zrealm_crossrealm15.gno | 27 + gnovm/tests/files/zrealm_crossrealm16.gno | 24 + gnovm/tests/files/zrealm_crossrealm17.gno | 27 + gnovm/tests/files/zrealm_crossrealm18.gno | 35 + .../files/zrealm_crossrealm19_stdlibs.gno | 32 + gnovm/tests/files/zrealm_crossrealm20.gno | 43 + gnovm/tests/files/zrealm_crossrealm21.gno | 723 ++++++ gnovm/tests/files/zrealm_crossrealm22.gno | 2282 +++++++++++++++++ gnovm/tests/files/zrealm_crossrealm4.gno | 12 +- gnovm/tests/files/zrealm_crossrealm5.gno | 8 +- gnovm/tests/files/zrealm_tests0.gno | 73 +- 29 files changed, 3495 insertions(+), 62 deletions(-) create mode 100644 examples/gno.land/r/demo/grc20reg/gno.mod create mode 100644 examples/gno.land/r/demo/grc20reg/grc20reg.gno create mode 100644 examples/gno.land/r/demo/grc20reg/grc20reg_test.gno create mode 100644 examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno create mode 100644 examples/gno.land/r/demo/tests/crossrealm_b/gno.mod create mode 100644 gnovm/tests/files/zrealm_crossrealm15.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm16.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm17.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm18.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm20.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm21.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm22.gno diff --git a/examples/Makefile b/examples/Makefile index 578b4faf15b..cdc73ee6b3a 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -45,7 +45,7 @@ test: .PHONY: lint lint: - go run ../gnovm/cmd/gno lint $(OFFICIAL_PACKAGES) + go run ../gnovm/cmd/gno lint -v $(OFFICIAL_PACKAGES) .PHONY: test.sync test.sync: diff --git a/examples/gno.land/p/demo/grc/grc20/types.gno b/examples/gno.land/p/demo/grc/grc20/types.gno index cf67858ccf3..816bbe8a1d9 100644 --- a/examples/gno.land/p/demo/grc/grc20/types.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -39,11 +39,11 @@ type Teller interface { // // Returns an error if the operation failed. // - // IMPORTANT: Beware that changing an allowance with this method brings the risk - // that someone may use both the old and the new allowance by unfortunate - // transaction ordering. One possible solution to mitigate this race - // condition is to first reduce the spender's allowance to 0 and set the - // desired value afterwards: + // IMPORTANT: Beware that changing an allowance with this method brings + // the risk that someone may use both the old and the new allowance by + // unfortunate transaction ordering. One possible solution to mitigate + // this race condition is to first reduce the spender's allowance to 0 + // and set the desired value afterwards: // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Approve(spender std.Address, amount uint64) error @@ -63,12 +63,23 @@ type Teller interface { // name, symbol, and decimals, as well as methods for interacting with the // ledger, including checking balances and allowances. type Token struct { - name string // Name of the token (e.g., "Dummy Token"). - symbol string // Symbol of the token (e.g., "DUMMY"). - decimals uint // Number of decimal places used for the token's precision. - ledger *PrivateLedger // Pointer to the PrivateLedger that manages balances and allowances. + // Name of the token (e.g., "Dummy Token"). + name string + // Symbol of the token (e.g., "DUMMY"). + symbol string + // Number of decimal places used for the token's precision. + decimals uint + // Pointer to the PrivateLedger that manages balances and allowances. + ledger *PrivateLedger } +// TokenGetter is a function type that returns a Token pointer. This type allows +// bypassing a limitation where we cannot directly pass Token pointers between +// realms. Instead, we pass this function which can then be called to get the +// Token pointer. For more details on this limitation and workaround, see: +// https://github.com/gnolang/gno/pull/3135 +type TokenGetter func() *Token + // PrivateLedger is a struct that holds the balances and allowances for the // token. It provides administrative functions for minting, burning, // transferring tokens, and managing allowances. @@ -77,10 +88,14 @@ type Token struct { // information regarding token balances and allowances, and allows direct, // unrestricted access to all administrative functions. type PrivateLedger struct { - totalSupply uint64 // Total supply of the token managed by this ledger. - balances avl.Tree // std.Address -> uint64 - allowances avl.Tree // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 - token *Token // Pointer to the associated Token struct + // Total supply of the token managed by this ledger. + totalSupply uint64 + // std.Address -> uint64 + balances avl.Tree + // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 + allowances avl.Tree + // Pointer to the associated Token struct + token *Token } var ( diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index de51b8b47d9..25636fcda78 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -9,6 +9,7 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - // XXX: grc20reg.Register(Token, "") + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") } func Faucet() string { diff --git a/examples/gno.land/r/demo/bar20/gno.mod b/examples/gno.land/r/demo/bar20/gno.mod index 2ec82d7be0b..9fb0f083e1b 100644 --- a/examples/gno.land/r/demo/bar20/gno.mod +++ b/examples/gno.land/r/demo/bar20/gno.mod @@ -5,4 +5,5 @@ require ( gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 31fa577c515..97b2e52b94b 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -10,6 +10,7 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/demo/grc20reg" "gno.land/r/demo/users" ) @@ -21,7 +22,8 @@ var ( func init() { privateLedger.Mint(owner.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) - // XXX: grc20reg.Register(Token, "") + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") } func TotalSupply() uint64 { diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index 4035f9b1200..64b8f90a27d 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -7,5 +7,6 @@ require ( gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/users v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index bf5e9c9ec96..a2d2a55fdf0 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -7,4 +7,5 @@ require ( gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index 901a9b9f33c..cfd32479f9d 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -8,6 +8,7 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" ) var instances avl.Tree // symbol -> instance @@ -42,7 +43,8 @@ func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64 faucet: faucet, } instances.Set(symbol, &inst) - // XXX: grc20reg.Register(token, symbol) + getter := func() *grc20.Token { return token } + grc20reg.Register(getter, symbol) } func (inst instance) Token() *grc20.Token { diff --git a/examples/gno.land/r/demo/grc20reg/gno.mod b/examples/gno.land/r/demo/grc20reg/gno.mod new file mode 100644 index 00000000000..f02ee09c35a --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/gno.mod @@ -0,0 +1,9 @@ +module gno.land/r/demo/grc20reg + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/fqname v0.0.0-latest + gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/grc20reg/grc20reg.gno b/examples/gno.land/r/demo/grc20reg/grc20reg.gno new file mode 100644 index 00000000000..ff46ec94860 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/grc20reg.gno @@ -0,0 +1,76 @@ +package grc20reg + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/fqname" + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ufmt" +) + +var registry = avl.NewTree() // rlmPath[.slug] -> TokenGetter (slug is optional) + +func Register(tokenGetter grc20.TokenGetter, slug string) { + rlmPath := std.PrevRealm().PkgPath() + key := fqname.Construct(rlmPath, slug) + registry.Set(key, tokenGetter) + std.Emit( + registerEvent, + "pkgpath", rlmPath, + "slug", slug, + ) +} + +func Get(key string) grc20.TokenGetter { + tokenGetter, ok := registry.Get(key) + if !ok { + return nil + } + return tokenGetter.(grc20.TokenGetter) +} + +func MustGet(key string) grc20.TokenGetter { + tokenGetter := Get(key) + if tokenGetter == nil { + panic("unknown token: " + key) + } + return tokenGetter +} + +func Render(path string) string { + switch { + case path == "": // home + // TODO: add pagination + s := "" + count := 0 + registry.Iterate("", "", func(key string, tokenI interface{}) bool { + count++ + tokenGetter := tokenI.(grc20.TokenGetter) + token := tokenGetter() + rlmPath, slug := fqname.Parse(key) + rlmLink := fqname.RenderLink(rlmPath, slug) + infoLink := "/r/demo/grc20reg:" + key + s += ufmt.Sprintf("- **%s** - %s - [info](%s)\n", token.GetName(), rlmLink, infoLink) + return false + }) + if count == 0 { + return "No registered token." + } + return s + default: // specific token + key := path + tokenGetter := MustGet(key) + token := tokenGetter() + rlmPath, slug := fqname.Parse(key) + rlmLink := fqname.RenderLink(rlmPath, slug) + s := ufmt.Sprintf("# %s\n", token.GetName()) + s += ufmt.Sprintf("- symbol: **%s**\n", token.GetSymbol()) + s += ufmt.Sprintf("- realm: %s\n", rlmLink) + s += ufmt.Sprintf("- decimals: %d\n", token.GetDecimals()) + s += ufmt.Sprintf("- total supply: %d\n", token.TotalSupply()) + return s + } +} + +const registerEvent = "register" diff --git a/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno b/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno new file mode 100644 index 00000000000..c93365ff7a1 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno @@ -0,0 +1,59 @@ +package grc20reg + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/urequire" +) + +func TestRegistry(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/foo")) + realmAddr := std.CurrentRealm().PkgPath() + token, ledger := grc20.NewToken("TestToken", "TST", 4) + ledger.Mint(std.CurrentRealm().Addr(), 1234567) + tokenGetter := func() *grc20.Token { return token } + // register + Register(tokenGetter, "") + regTokenGetter := Get(realmAddr) + regToken := regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + expected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo) +` + got := Render("") + urequire.True(t, strings.Contains(got, expected)) + // 404 + invalidToken := Get("0xdeadbeef") + urequire.True(t, invalidToken == nil) + + // register with a slug + Register(tokenGetter, "mySlug") + regTokenGetter = Get(realmAddr + ".mySlug") + regToken = regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + // override + Register(tokenGetter, "") + regTokenGetter = Get(realmAddr + "") + regToken = regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + got = Render("") + urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)`)) + urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).mySlug - [info](/r/demo/grc20reg:gno.land/r/demo/foo.mySlug)`)) + + expected = `# TestToken +- symbol: **TST** +- realm: [gno.land/r/demo/foo](/r/demo/foo).mySlug +- decimals: 4 +- total supply: 1234567 +` + got = Render("gno.land/r/demo/foo.mySlug") + urequire.Equal(t, expected, got) +} diff --git a/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno b/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno index 97273f642de..1cc5a3f8e18 100644 --- a/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno +++ b/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno @@ -27,3 +27,31 @@ func Make1() *p_crossrealm.Container { B: local, } } + +type Fooer interface{ Foo() } + +var fooer Fooer + +func SetFooer(f Fooer) Fooer { + fooer = f + return fooer +} + +func GetFooer() Fooer { return fooer } + +func CallFooerFoo() { fooer.Foo() } + +type FooerGetter func() Fooer + +var fooerGetter FooerGetter + +func SetFooerGetter(fg FooerGetter) FooerGetter { + fooerGetter = fg + return fg +} + +func GetFooerGetter() FooerGetter { + return fooerGetter +} + +func CallFooerGetterFoo() { fooerGetter().Foo() } diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno b/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno new file mode 100644 index 00000000000..d412b6ee6b1 --- /dev/null +++ b/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno @@ -0,0 +1,25 @@ +package crossrealm_b + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) SetS(newVal string) { + f.s = newVal +} + +func (f *fooer) Foo() { + println("hello " + f.s + " cur=" + std.CurrentRealm().PkgPath() + " prev=" + std.PrevRealm().PkgPath()) +} + +var ( + Fooer = &fooer{s: "A"} + FooerGetter = func() crossrealm.Fooer { return Fooer } + FooerGetterBuilder = func() crossrealm.FooerGetter { return func() crossrealm.Fooer { return Fooer } } +) diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod new file mode 100644 index 00000000000..74548712caa --- /dev/null +++ b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/tests/crossrealm_b + +require gno.land/r/demo/tests/crossrealm v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index 421ac6528c9..e7fde94ea08 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -50,6 +50,8 @@ type TestRealmObject struct { Field string } +var TestRealmObjectValue TestRealmObject + func ModifyTestRealmObject(t *TestRealmObject) { t.Field += "_modified" } diff --git a/examples/gno.land/r/demo/wugnot/gno.mod b/examples/gno.land/r/demo/wugnot/gno.mod index f076e90e068..c7081ce6963 100644 --- a/examples/gno.land/r/demo/wugnot/gno.mod +++ b/examples/gno.land/r/demo/wugnot/gno.mod @@ -4,5 +4,6 @@ require ( gno.land/p/demo/grc/grc20 v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/users v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index bb109644778..09538b860ca 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/demo/grc20reg" "gno.land/r/demo/users" ) @@ -18,7 +19,8 @@ const ( ) func init() { - // XXX: grc20reg.Register(Token, "") + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") } func Deposit() { diff --git a/gnovm/stdlibs/math/overflow/overflow.gno b/gnovm/stdlibs/math/overflow/overflow.gno index 9bdeff0720f..0bc2e03a522 100644 --- a/gnovm/stdlibs/math/overflow/overflow.gno +++ b/gnovm/stdlibs/math/overflow/overflow.gno @@ -223,7 +223,7 @@ func Div8p(a, b int8) int8 { func Quo8(a, b int8) (int8, int8, bool) { if b == 0 { return 0, 0, false - } else if b == -1 && a == math.MinInt8 { + } else if b == -1 && a == int8(math.MinInt8) { return 0, 0, false } c := a / b @@ -313,7 +313,7 @@ func Div16p(a, b int16) int16 { func Quo16(a, b int16) (int16, int16, bool) { if b == 0 { return 0, 0, false - } else if b == -1 && a == math.MinInt16 { + } else if b == -1 && a == int16(math.MinInt16) { return 0, 0, false } c := a / b @@ -403,7 +403,7 @@ func Div32p(a, b int32) int32 { func Quo32(a, b int32) (int32, int32, bool) { if b == 0 { return 0, 0, false - } else if b == -1 && a == math.MinInt32 { + } else if b == -1 && a == int32(math.MinInt32) { return 0, 0, false } c := a / b diff --git a/gnovm/tests/files/zrealm_crossrealm15.gno b/gnovm/tests/files/zrealm_crossrealm15.gno new file mode 100644 index 00000000000..b6f38d81abb --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm15.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct{} + +func (fooer) Foo() { println("hello " + std.CurrentRealm().PkgPath()) } + +var f *fooer + +func init() { + f = &fooer{} + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() +} + +func main() { + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm16.gno b/gnovm/tests/files/zrealm_crossrealm16.gno new file mode 100644 index 00000000000..e1b4001801c --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm16.gno @@ -0,0 +1,24 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct{} + +func (fooer) Foo() { println("hello " + std.CurrentRealm().PkgPath()) } + +var f *fooer + +func main() { + f = &fooer{} + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm17.gno b/gnovm/tests/files/zrealm_crossrealm17.gno new file mode 100644 index 00000000000..9abb918689a --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm17.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type container struct{ *fooer } + +func (container) Foo() { println("hello container " + std.CurrentRealm().PkgPath()) } + +type fooer struct{} + +var f *fooer + +func main() { + f = &fooer{} + c := &container{f} + crossrealm.SetFooer(c) + crossrealm.CallFooerFoo() + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm18.gno b/gnovm/tests/files/zrealm_crossrealm18.gno new file mode 100644 index 00000000000..f7a318ed3a0 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm18.gno @@ -0,0 +1,35 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct{} + +func (fooer) Foo() { println("hello " + std.CurrentRealm().PkgPath()) } + +var f crossrealm.Fooer = crossrealm.SetFooer(&fooer{}) + +func init() { + crossrealm.CallFooerFoo() +} + +func main() { + crossrealm.CallFooerFoo() + print(".") +} + +// Output: +// hello gno.land/r/crossrealm_test +// hello gno.land/r/crossrealm_test +// . + +// Error: + +// Realm: +// switchrealm["gno.land/r/crossrealm_test"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/crossrealm_test"] diff --git a/gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno b/gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno new file mode 100644 index 00000000000..a3b864755fd --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno @@ -0,0 +1,32 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) Foo() { + f.s = "B" + println("hello " + f.s + " " + std.CurrentRealm().PkgPath()) +} + +var f *fooer + +func init() { + f = &fooer{s: "A"} + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() +} + +func main() { + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm20.gno b/gnovm/tests/files/zrealm_crossrealm20.gno new file mode 100644 index 00000000000..32fac2e95b9 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm20.gno @@ -0,0 +1,43 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) Foo() { + f.s = "B" + println("hello " + f.s + " " + std.CurrentRealm().PkgPath()) +} + +var f *fooer + +func init() { + f = &fooer{s: "A"} + fg := func() crossrealm.Fooer { return f } + crossrealm.SetFooerGetter(fg) + crossrealm.CallFooerGetterFoo() + f.s = "C" + crossrealm.CallFooerGetterFoo() +} + +func main() { + print(".") +} + +// Output: +// hello B gno.land/r/crossrealm_test +// hello B gno.land/r/crossrealm_test +// . + +// Realm: +// switchrealm["gno.land/r/crossrealm_test"] + +// Error: +// diff --git a/gnovm/tests/files/zrealm_crossrealm21.gno b/gnovm/tests/files/zrealm_crossrealm21.gno new file mode 100644 index 00000000000..634fbea13c8 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm21.gno @@ -0,0 +1,723 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" + "gno.land/r/demo/tests/crossrealm_b" +) + +func main() { + f := crossrealm_b.Fooer + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() + f.SetS("B") + crossrealm.CallFooerFoo() + print(".") +} + +// Output: +// hello A cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// hello B cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . + +// Realm: +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "5", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm_b.fooer" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:3" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "B" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/crossrealm_test"] + +// Error: +// diff --git a/gnovm/tests/files/zrealm_crossrealm22.gno b/gnovm/tests/files/zrealm_crossrealm22.gno new file mode 100644 index 00000000000..18985f7719d --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm22.gno @@ -0,0 +1,2282 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" + "gno.land/r/demo/tests/crossrealm_b" +) + +func main() { + f := crossrealm_b.Fooer + crossrealm.SetFooerGetter(func() crossrealm.Fooer { return f }) + crossrealm.CallFooerGetterFoo() + f.SetS("B") + crossrealm.CallFooerGetterFoo() + println(".") + + f.SetS("C") + crossrealm.SetFooerGetter(crossrealm_b.FooerGetter) + crossrealm.CallFooerGetterFoo() + println(".") + + f.SetS("D") + crossrealm.SetFooerGetter(crossrealm_b.FooerGetterBuilder()) + crossrealm.CallFooerGetterFoo() + println(".") +} + +// Output: +// hello A cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// hello B cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . +// hello C cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . +// hello D cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . + +// Realm: +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// c[1712ac7adcfdc8e58a67e5615e20fb312394c4df:6]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:6", +// "ModTime": "0", +// "OwnerID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "RefCount": "1" +// }, +// "Parent": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f5a516808f8976c33939133293d598ce3bca4e8d:3" +// }, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "files/zrealm_crossrealm22.gno", +// "Line": "11", +// "PkgPath": "gno.land/r/crossrealm_test" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm_b.fooer" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:3" +// }, +// "Index": "0", +// "TV": null +// } +// } +// ] +// } +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "5", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Hash": "23de97a577d573252d00394ce9b71c24b0646546", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:6" +// }, +// "FileName": "", +// "IsMethod": false, +// "Name": "", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/crossrealm_test", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "28", +// "File": "files/zrealm_crossrealm22.gno", +// "Line": "13", +// "PkgPath": "gno.land/r/crossrealm_test" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// switchrealm["gno.land/r/crossrealm_test"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "B" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/crossrealm_test"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "C" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "6", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:5" +// }, +// "FileName": "", +// "IsMethod": false, +// "Name": "", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "23", +// "File": "crossrealm.gno", +// "Line": "23", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// d[1712ac7adcfdc8e58a67e5615e20fb312394c4df:6] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "D" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// c[1712ac7adcfdc8e58a67e5615e20fb312394c4df:7]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:7", +// "ModTime": "0", +// "OwnerID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "RefCount": "1" +// }, +// "Parent": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:5" +// }, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "23", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "6", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Hash": "89352b352826005a86eee78e6c832b43ae0ab6a6", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:7" +// }, +// "FileName": "", +// "IsMethod": false, +// "Name": "", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "62", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/crossrealm_test"] + +// Error: +// diff --git a/gnovm/tests/files/zrealm_crossrealm4.gno b/gnovm/tests/files/zrealm_crossrealm4.gno index 6aa9c5247d8..ed73b7ad6bb 100644 --- a/gnovm/tests/files/zrealm_crossrealm4.gno +++ b/gnovm/tests/files/zrealm_crossrealm4.gno @@ -5,18 +5,18 @@ import ( "gno.land/r/demo/tests" ) -// NOTE: it is valid to persist external realm types. -var somevalue tests.TestRealmObject +// NOTE: it is valid to persist a pointer to an external object +var somevalue *tests.TestRealmObject func init() { - somevalue.Field = "test" + somevalue = &tests.TestRealmObjectValue } func main() { - // NOTE: but it is invalid to modify it using an external realm function. + // NOTE: it is valid to modify it using the external realm function. somevalue.Modify() println(somevalue) } -// Error: -// cannot modify external-realm or non-realm object +// Output: +// &(struct{("_modified" string)} gno.land/r/demo/tests.TestRealmObject) diff --git a/gnovm/tests/files/zrealm_crossrealm5.gno b/gnovm/tests/files/zrealm_crossrealm5.gno index 6aa9c5247d8..c7560b21463 100644 --- a/gnovm/tests/files/zrealm_crossrealm5.gno +++ b/gnovm/tests/files/zrealm_crossrealm5.gno @@ -6,15 +6,15 @@ import ( ) // NOTE: it is valid to persist external realm types. -var somevalue tests.TestRealmObject +var somevalue *tests.TestRealmObject func init() { - somevalue.Field = "test" + somevalue = &tests.TestRealmObjectValue } func main() { - // NOTE: but it is invalid to modify it using an external realm function. - somevalue.Modify() + // NOTE: but it is invalid to modify it directly. + somevalue.Field = "test" println(somevalue) } diff --git a/gnovm/tests/files/zrealm_tests0.gno b/gnovm/tests/files/zrealm_tests0.gno index 82e4d418217..afb7e4a7c3b 100644 --- a/gnovm/tests/files/zrealm_tests0.gno +++ b/gnovm/tests/files/zrealm_tests0.gno @@ -26,7 +26,7 @@ func main() { // Realm: // switchrealm["gno.land/r/demo/tests"] -// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18]={ +// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:19]={ // "Fields": [ // { // "T": { @@ -40,17 +40,17 @@ func main() { // } // ], // "ObjectInfo": { -// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18", +// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:19", // "ModTime": "0", -// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", +// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18", // "RefCount": "1" // } // } -// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17]={ +// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18]={ // "ObjectInfo": { -// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", +// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18", // "ModTime": "0", -// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16", +// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", // "RefCount": "1" // }, // "Value": { @@ -60,12 +60,12 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "d3d6ffa52602f2bc976051d79294d219750aca64", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18" +// "Hash": "6b9b731f6118c2419f23ba57e1481679f17f4a8f", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:19" // } // } // } -// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16]={ +// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17]={ // "Data": null, // "List": [ // { @@ -80,8 +80,8 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "4ea1e08156f3849b74a0f41f92cd4b48fb94926b", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:11" +// "Hash": "148d314678615253ee2032d7ecff6b144b474baf", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:12" // }, // "Index": "0", // "TV": null @@ -99,8 +99,8 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "ce86ea1156e75a44cd9d7ba2261819b100aa4ed1", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:14" +// "Hash": "fa414e1770821b8deb8e6d46d97828c47f7d5fa5", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:15" // }, // "Index": "0", // "TV": null @@ -118,8 +118,8 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "b66192fbd8a8dde79b6f854b5cc3c4cc965cfd92", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17" +// "Hash": "aaa64d049cf8660d689780acac9f546f270eaa4e", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18" // }, // "Index": "0", // "TV": null @@ -127,7 +127,7 @@ func main() { // } // ], // "ObjectInfo": { -// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16", +// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", // "ModTime": "0", // "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:2", // "RefCount": "1" @@ -138,7 +138,7 @@ func main() { // "ObjectInfo": { // "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:2", // "IsEscaped": true, -// "ModTime": "15", +// "ModTime": "16", // "RefCount": "5" // }, // "Parent": null, @@ -207,8 +207,8 @@ func main() { // "@type": "/gno.SliceValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "ad25f70f66c8c53042afd1377e5ff5ab744bf1a5", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16" +// "Hash": "3c58838c5667649add1ff8ee48a1cdc187fcd2ef", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17" // }, // "Length": "3", // "Maxcap": "3", @@ -1153,7 +1153,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "57", +// "Line": "59", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1185,6 +1185,17 @@ func main() { // }, // { // "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests.TestRealmObject" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "5e56ba76fc0add1a3a67f7a8b6709f4f27215f93", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:10" +// } +// }, +// { +// "T": { // "@type": "/gno.FuncType", // "Params": [ // { @@ -1221,7 +1232,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "53", +// "Line": "55", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1338,7 +1349,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "75", +// "Line": "77", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1374,7 +1385,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "80", +// "Line": "82", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1410,7 +1421,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "88", +// "Line": "90", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1456,7 +1467,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "92", +// "Line": "94", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1512,7 +1523,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "96", +// "Line": "98", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1569,7 +1580,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "100", +// "Line": "102", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1626,7 +1637,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "104", +// "Line": "106", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1682,7 +1693,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "108", +// "Line": "110", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1738,7 +1749,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "112", +// "Line": "114", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1761,7 +1772,7 @@ func main() { // } // ] // } -// d[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:13] +// d[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:14] // switchrealm["gno.land/r/demo/tests_foo"] // switchrealm["gno.land/r/demo/tests_foo"] // switchrealm["gno.land/r/demo/tests_foo"] From 304222966eccb14b6c77d48b70b0cbe265bf2b4f Mon Sep 17 00:00:00 2001 From: cuibuwei <166905851+cuibuwei@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:00:45 +0800 Subject: [PATCH 279/345] chore: fix some function names in comment (#3254)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
Signed-off-by: cuibuwei --- tm2/pkg/p2p/netaddress.go | 2 +- tm2/pkg/sdk/baseapp.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tm2/pkg/p2p/netaddress.go b/tm2/pkg/p2p/netaddress.go index 1ce34afff34..77f89b2a4b3 100644 --- a/tm2/pkg/p2p/netaddress.go +++ b/tm2/pkg/p2p/netaddress.go @@ -134,7 +134,7 @@ func NewNetAddressFromStrings(idaddrs []string) ([]*NetAddress, []error) { return netAddrs, errs } -// NewNetAddressIPPort returns a new NetAddress using the provided IP +// NewNetAddressFromIPPort returns a new NetAddress using the provided IP // and port number. func NewNetAddressFromIPPort(id ID, ip net.IP, port uint16) *NetAddress { return &NetAddress{ diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index c11f81d852a..415309eab9a 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -262,7 +262,7 @@ func (app *BaseApp) setConsensusParams(consensusParams *abci.ConsensusParams) { app.consensusParams = consensusParams } -// setConsensusParams stores the consensus params to the main store. +// storeConsensusParams stores the consensus params to the main store. func (app *BaseApp) storeConsensusParams(consensusParams *abci.ConsensusParams) { consensusParamsBz, err := amino.Marshal(consensusParams) if err != nil { From 78f0e200133e9b7cbae930e3e1436d139c183588 Mon Sep 17 00:00:00 2001 From: Miguel Victoria Villaquiran Date: Wed, 4 Dec 2024 03:59:31 -0500 Subject: [PATCH 280/345] feat(genesis): deployerAddress passed as parameter (#3253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/gnolang/gno/issues/2573 Had to change the PR from a personal repository as the pipeline was failing old PR: https://github.com/gnolang/gno/pull/2986
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: 6h057 Co-authored-by: Miloš Živković --- contribs/gnogenesis/README.md | 4 ++ .../internal/txs/txs_add_packages.go | 58 +++++++++++++++---- .../internal/txs/txs_add_packages_test.go | 26 +++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/contribs/gnogenesis/README.md b/contribs/gnogenesis/README.md index 32cf3e6bb94..25c82992f8f 100644 --- a/contribs/gnogenesis/README.md +++ b/contribs/gnogenesis/README.md @@ -169,6 +169,10 @@ To clear specific transactions, use the transaction hash: ```shell gnogenesis txs remove "5HuU9LN8WUa2NsjiNxp8Xii9n0zlSGXc9UqzLHB+DPs=" ``` +To specify a deployer address (package creator) on add packages command +```shell +gnogenesis txs add packages ./examples --deployer-address=SOME_ADDRESS +``` The transaction hash is the base64 encoding of the Amino-Binary encoded `std.Tx` transaction hash. diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go index 1b4e6e7cffb..cf863c72116 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -3,26 +3,49 @@ package txs import ( "context" "errors" + "flag" "fmt" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) -var errInvalidPackageDir = errors.New("invalid package directory") +var ( + errInvalidPackageDir = errors.New("invalid package directory") + errInvalidDeployerAddr = errors.New("invalid deployer address") +) +// Keep in sync with gno.land/cmd/start.go var ( - // Keep in sync with gno.land/cmd/start.go - genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) + defaultCreator = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 + genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) ) +type addPkgCfg struct { + txsCfg *txsCfg + deployerAddress string +} + +func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.deployerAddress, + "deployer-address", + defaultCreator.String(), + "the address that will be used to deploy the package", + ) +} + // newTxsAddPackagesCmd creates the genesis txs add packages subcommand func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + cfg := &addPkgCfg{ + txsCfg: txsCfg, + } + return commands.NewCommand( commands.Metadata{ Name: "packages", @@ -30,20 +53,20 @@ func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { ShortHelp: "imports transactions from the given packages into the genesis.json", LongHelp: "Imports the transactions from a given package directory recursively to the genesis.json", }, - commands.NewEmptyConfig(), + cfg, func(_ context.Context, args []string) error { - return execTxsAddPackages(txsCfg, io, args) + return execTxsAddPackages(cfg, io, args) }, ) } func execTxsAddPackages( - cfg *txsCfg, + cfg *addPkgCfg, io commands.IO, args []string, ) error { // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + genesis, loadErr := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) if loadErr != nil { return fmt.Errorf("unable to load genesis, %w", loadErr) } @@ -53,10 +76,23 @@ func execTxsAddPackages( return errInvalidPackageDir } + var ( + creator = defaultCreator + err error + ) + + // Check if the deployer address is set + if cfg.deployerAddress != defaultCreator.String() { + creator, err = crypto.AddressFromString(cfg.deployerAddress) + if err != nil { + return fmt.Errorf("%w, %w", errInvalidDeployerAddr, err) + } + } + parsedTxs := make([]gnoland.TxWithMetadata, 0) for _, path := range args { // Generate transactions from the packages (recursively) - txs, err := gnoland.LoadPackagesFromDir(path, genesisDeployAddress, genesisDeployFee) + txs, err := gnoland.LoadPackagesFromDir(path, creator, genesisDeployFee) if err != nil { return fmt.Errorf("unable to load txs from directory, %w", err) } @@ -70,7 +106,7 @@ func execTxsAddPackages( } // Save the updated genesis - if err := genesis.SaveAs(cfg.GenesisPath); err != nil { + if err := genesis.SaveAs(cfg.txsCfg.GenesisPath); err != nil { return fmt.Errorf("unable to save genesis.json, %w", err) } diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go index 12a9287f171..c3405d6ff8d 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -60,6 +60,32 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error()) }) + t.Run("invalid deployer address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + t.TempDir(), // package dir + "--deployer-address", + "beep-boop", // invalid address + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidDeployerAddr) + }) + t.Run("valid package", func(t *testing.T) { t.Parallel() From 6585cad6b8e253602653ac2bcd91f9a752ae5102 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Wed, 4 Dec 2024 10:08:25 +0100 Subject: [PATCH 281/345] fix: impl empty statement exec (#3252) Implement empty statement in the runtime exec. Closes https://github.com/gnolang/gno/issues/3202 --- gnovm/pkg/gnolang/go2gno.go | 2 ++ gnovm/pkg/gnolang/op_exec.go | 1 + gnovm/tests/files/goto_empty_stmt.gno | 10 ++++++++++ 3 files changed, 13 insertions(+) create mode 100644 gnovm/tests/files/goto_empty_stmt.gno diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 99e051f7913..338efa20fcc 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -471,6 +471,8 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { PkgName: pkgName, Decls: decls, } + case *ast.EmptyStmt: + return &EmptyStmt{} default: panic(fmt.Sprintf("unknown Go type %v: %s\n", reflect.TypeOf(gon), diff --git a/gnovm/pkg/gnolang/op_exec.go b/gnovm/pkg/gnolang/op_exec.go index 900b5f8e9bb..5f71ffefa0c 100644 --- a/gnovm/pkg/gnolang/op_exec.go +++ b/gnovm/pkg/gnolang/op_exec.go @@ -769,6 +769,7 @@ EXEC_SWITCH: } m.PushOp(OpBody) m.PushStmt(b.GetBodyStmt()) + case *EmptyStmt: default: panic(fmt.Sprintf("unexpected statement %#v", s)) } diff --git a/gnovm/tests/files/goto_empty_stmt.gno b/gnovm/tests/files/goto_empty_stmt.gno new file mode 100644 index 00000000000..fd939de1045 --- /dev/null +++ b/gnovm/tests/files/goto_empty_stmt.gno @@ -0,0 +1,10 @@ +package main + +func main() { + println("Hi") + goto done +done: +} + +// Output: +// Hi \ No newline at end of file From 8c660aca81ab0347769d5f391f9b7c6a6b2b6e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:10:14 +0100 Subject: [PATCH 282/345] chore(deps): bump coursier/setup-action from 1.3.8 to 1.3.9 in the actions group (#3258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions group with 1 update: [coursier/setup-action](https://github.com/coursier/setup-action). Updates `coursier/setup-action` from 1.3.8 to 1.3.9
Release notes

Sourced from coursier/setup-action's releases.

v1.3.9

What's Changed

Updates / maintenance

Full Changelog: https://github.com/coursier/setup-action/compare/v1...v1.3.9

Commits
  • 039f736 build(deps-dev): bump @​types/node from 22.10.0 to 22.10.1
  • b0150fa build(deps-dev): bump eslint-plugin-github from 5.1.2 to 5.1.3
  • 0329715 build(deps-dev): bump eslint-plugin-github from 5.1.1 to 5.1.2
  • 45da7eb build(deps-dev): bump @​typescript-eslint/parser from 8.15.0 to 8.16.0
  • 049f21e build(deps-dev): bump @​types/node from 22.9.3 to 22.10.0
  • ca73c3e build(deps-dev): bump prettier from 3.3.3 to 3.4.1
  • e3d80af build(deps-dev): bump @​typescript-eslint/eslint-plugin
  • 72f329c bugfix: Migrate to new config format
  • 7441104 build(deps-dev): bump eslint from 8.57.1 to 9.15.0
  • d67c30e Update dist
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coursier/setup-action&package-manager=github_actions&previous-version=1.3.8&new-version=1.3.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/fossa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index c536b428a5c..41d9a2cba94 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -25,7 +25,7 @@ jobs: uses: coursier/cache-action@v6.4.6 - name: Set up JDK 17 - uses: coursier/setup-action@v1.3.8 + uses: coursier/setup-action@v1.3.9 with: jvm: temurin:1.17 From a7a38b6eea44500b259972a3c05d026dce31eabe Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 4 Dec 2024 10:57:29 +0100 Subject: [PATCH 283/345] chore: s/NativeStore/NativeResolver/g (#3262) It's not a "store", this was a misnomer from the beginning. --- gno.land/pkg/sdk/vm/keeper.go | 4 ++-- gnovm/pkg/gnolang/realm.go | 2 +- gnovm/pkg/gnolang/store.go | 24 ++++++++++++------------ gnovm/pkg/gnolang/store_test.go | 2 +- gnovm/pkg/gnolang/values.go | 2 +- gnovm/pkg/test/imports.go | 2 +- gnovm/stdlibs/stdlibs.go | 4 ++-- gnovm/tests/stdlibs/stdlibs.go | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 0dca794ee71..68f784a52e7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -100,7 +100,7 @@ func (vm *VMKeeper) Initialize( alloc := gno.NewAllocator(maxAllocTx) vm.gnoStore = gno.NewStore(alloc, baseStore, iavlStore) - vm.gnoStore.SetNativeStore(stdlibs.NativeStore) + vm.gnoStore.SetNativeResolver(stdlibs.NativeResolver) if vm.gnoStore.NumMemPackages() > 0 { // for now, all mem packages must be re-run after reboot. @@ -146,7 +146,7 @@ func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { } gs := gno.NewStore(nil, cachedStdlib.base, cachedStdlib.iavl) - gs.SetNativeStore(stdlibs.NativeStore) + gs.SetNativeResolver(stdlibs.NativeResolver) loadStdlib(gs, stdlibDir) cachedStdlib.gno = gs }) diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 5913f13a0f7..d25d456edf3 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -1132,7 +1132,7 @@ func copyValueWithRefs(val Value) Value { if cv.Closure != nil { closure = toRefValue(cv.Closure) } - // nativeBody funcs which don't come from NativeStore (and thus don't + // nativeBody funcs which don't come from NativeResolver (and thus don't // have NativePkg/Name) can't be persisted, and should not be able // to get here anyway. if cv.nativeBody != nil && cv.NativePkg == "" { diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 2c0ee05a1d7..b721194823d 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -25,8 +25,8 @@ import ( // cause writes to happen to the store, such as MemPackages to iavlstore. type PackageGetter func(pkgPath string, store Store) (*PackageNode, *PackageValue) -// NativeStore is a function which can retrieve native bodies of native functions. -type NativeStore func(pkgName string, name Name) func(m *Machine) +// NativeResolver is a function which can retrieve native bodies of native functions. +type NativeResolver func(pkgName string, name Name) func(m *Machine) // Store is the central interface that specifies the communications between the // GnoVM and the underlying data store; currently, generally the gno.land @@ -62,7 +62,7 @@ type Store interface { GetMemFile(path string, name string) *gnovm.MemFile IterMemPackage() <-chan *gnovm.MemPackage ClearObjectCache() // run before processing a message - SetNativeStore(NativeStore) // for "new" natives XXX + SetNativeResolver(NativeResolver) // for "new" natives XXX GetNative(pkgPath string, name Name) func(m *Machine) // for "new" natives XXX SetLogStoreOps(enabled bool) SprintStoreOps() string @@ -95,7 +95,7 @@ type defaultStore struct { // store configuration; cannot be modified in a transaction pkgGetter PackageGetter // non-realm packages cacheNativeTypes map[reflect.Type]Type // reflect doc: reflect.Type are comparable - nativeStore NativeStore // for injecting natives + nativeResolver NativeResolver // for injecting natives // transient opslog []StoreOp // for debugging and testing. @@ -116,7 +116,7 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore // store configuration pkgGetter: nil, cacheNativeTypes: make(map[reflect.Type]Type), - nativeStore: nil, + nativeResolver: nil, } InitStoreCaches(ds) return ds @@ -144,7 +144,7 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans // store configuration pkgGetter: ds.pkgGetter, cacheNativeTypes: ds.cacheNativeTypes, - nativeStore: ds.nativeStore, + nativeResolver: ds.nativeResolver, // transient current: nil, @@ -174,8 +174,8 @@ func (transactionStore) SetPackageGetter(pg PackageGetter) { // panic("Go2GnoType may not be called in a transaction store") // } -func (transactionStore) SetNativeStore(ns NativeStore) { - panic("SetNativeStore may not be called in a transaction store") +func (transactionStore) SetNativeResolver(ns NativeResolver) { + panic("SetNativeResolver may not be called in a transaction store") } // CopyCachesFromStore allows to copy a store's internal object, type and @@ -685,13 +685,13 @@ func (ds *defaultStore) ClearObjectCache() { ds.SetCachePackage(Uverse()) } -func (ds *defaultStore) SetNativeStore(ns NativeStore) { - ds.nativeStore = ns +func (ds *defaultStore) SetNativeResolver(ns NativeResolver) { + ds.nativeResolver = ns } func (ds *defaultStore) GetNative(pkgPath string, name Name) func(m *Machine) { - if ds.nativeStore != nil { - return ds.nativeStore(pkgPath, name) + if ds.nativeResolver != nil { + return ds.nativeResolver(pkgPath, name) } return nil } diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index f7f03b947f6..e280032e3d9 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -58,7 +58,7 @@ func TestTransactionStore_blockedMethods(t *testing.T) { // These methods should panic as they modify store settings, which should // only be changed in the root store. assert.Panics(t, func() { transactionStore{}.SetPackageGetter(nil) }) - assert.Panics(t, func() { transactionStore{}.SetNativeStore(nil) }) + assert.Panics(t, func() { transactionStore{}.SetNativeResolver(nil) }) } func TestCopyFromCachedStore(t *testing.T) { diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 8e27bcbcbdb..e7a6274a780 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -548,7 +548,7 @@ type FuncValue struct { Captures []TypedValue `json:",omitempty"` // HeapItemValues captured from closure. FileName Name // file name where declared PkgPath string - NativePkg string // for native bindings through NativeStore + NativePkg string // for native bindings through NativeResolver NativeName Name // not redundant with Name; this cannot be changed in userspace body []Stmt // function body diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index dabb5644cdd..b57fc6388b1 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -160,7 +160,7 @@ func Store( // Make a new store. resStore = gno.NewStore(nil, baseStore, baseStore) resStore.SetPackageGetter(getPackage) - resStore.SetNativeStore(teststdlibs.NativeStore) + resStore.SetNativeResolver(teststdlibs.NativeResolver) return } diff --git a/gnovm/stdlibs/stdlibs.go b/gnovm/stdlibs/stdlibs.go index c9b16815ab5..3b8b88c1fde 100644 --- a/gnovm/stdlibs/stdlibs.go +++ b/gnovm/stdlibs/stdlibs.go @@ -24,10 +24,10 @@ func FindNative(pkgPath string, name gno.Name) *NativeFunc { return nil } -// NativeStore is used by the GnoVM to determine if the given function, +// NativeResolver is used by the GnoVM to determine if the given function, // specified by its pkgPath and name, has a native implementation; and if so // retrieve it. -func NativeStore(pkgPath string, name gno.Name) func(*gno.Machine) { +func NativeResolver(pkgPath string, name gno.Name) func(*gno.Machine) { nt := FindNative(pkgPath, name) if nt == nil { return nil diff --git a/gnovm/tests/stdlibs/stdlibs.go b/gnovm/tests/stdlibs/stdlibs.go index b0a1050af41..92316bf41fd 100644 --- a/gnovm/tests/stdlibs/stdlibs.go +++ b/gnovm/tests/stdlibs/stdlibs.go @@ -8,11 +8,11 @@ import ( //go:generate go run github.com/gnolang/gno/misc/genstd -func NativeStore(pkgPath string, name gno.Name) func(*gno.Machine) { +func NativeResolver(pkgPath string, name gno.Name) func(*gno.Machine) { for _, nf := range nativeFuncs { if nf.gnoPkg == pkgPath && name == nf.gnoFunc { return nf.f } } - return stdlibs.NativeStore(pkgPath, name) + return stdlibs.NativeResolver(pkgPath, name) } From 7a40481f0f4f97054aa02a5a8211aac01d278a84 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:00:21 +0100 Subject: [PATCH 284/345] docs: update token section in bot README (#3261) Simple update of the bot README to mention the necessary permissions for the bot to operate, see this comment: https://github.com/gnolang/gno/issues/3238#issuecomment-2514895884
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- contribs/github-bot/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index 7932300cb9d..639901c52ee 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -19,11 +19,24 @@ The bot configuration is defined in Go and is located in the file [config.go](./ For the bot to make requests to the GitHub API, it needs a Personal Access Token. The fine-grained permissions to assign to the token for the bot to function are: +#### Repository permissions + - `pull_requests` scope to read is the bare minimum to run the bot in dry-run mode - `pull_requests` scope to write to be able to update bot comment, assign user, apply label and request review - `contents` scope to read to be able to check if the head branch is up to date with another one - `commit_statuses` scope to write to be able to update pull request bot status check +#### Organization permissions + +- `members` scope to read to be able to list the members of a team + +#### Bot account role + +For the bot to create a commit status on a repo - and only for this feature at the time of writing this - the GitHub account of the bot must either: + +- have the `write` role on the repo +- have the `owner` role in the organization that owns the repo + ## Usage ```bash From 2496db78df18d66bad11942312b46ee9016659f3 Mon Sep 17 00:00:00 2001 From: jinoosss <112360739+jinoosss@users.noreply.github.com> Date: Wed, 4 Dec 2024 22:32:01 +0900 Subject: [PATCH 285/345] fix: Modify `app` path method to simulate for ABCI query (#3207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Descriptions To simulate transactions, utilize the `.app/simulate` method for ABCI Query. ### Changes 1. change the path of ABCI Query's `.app` to the result data storage location. - You can receive the query result data as `RequestQuery.ResponseData.Data` instead of `RequestQuery.Value`. - Provide it in a common form with other ABCI Queries. 2. remove the gas-consume logic of mocking signature data that is executed when simulating transactions ([/tm2/pkg/sdk/auth/ante.go#L231-L237](https://github.com/gnolang/gno/blob/master/tm2/pkg/sdk/auth/ante.go#L231-L237)) - We will get the correct value when simulating a real transaction. - We want transactions to run without signatures, but we already have checks in place to see if a signature exists. ([tm2/pkg/sdk/auth/ante.go#L104-L106](https://github.com/gnolang/gno/blob/master/tm2/pkg/sdk/auth/ante.go#L104-L106)) ### Example #### [Request Simulate] ```curl curl --location 'http://localhost:26657' \ --header 'Content-Type: application/json' \ --data '{ "id": 1, "jsonrpc": "2.0", "method": "abci_query", "params": [ ".app/simulate", "CnMKDS9iYW5rLk1zZ1NlbmQSYgooZzFqZzhtdHV0dTlraGhmd2M0bnhtdWhjcGZ0ZjBwYWpkaGZ2c3FmNRIoZzFmZnp4aGE1N2RoMHFndjltYTV2MzkzdXIwemV4ZnZwNmxzanBhZRoMNTAwMDAwMHVnbm90Eg4IgIl6EggzMDB1Z25vdBp+CjoKEy90bS5QdWJLZXlTZWNwMjU2azESIwohA+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2yEkCrIOTBt7YcDGcQ6Ohfv1r3nftAPaTATAtPfYD5zLQf7WDf1KPvWARe//CANtLLtIzcPVl7P/HnHxmfCYEwfGogIgUxMjMxMw==", "0", false ] }' ``` #### [Response] ```curl { "jsonrpc": "2.0", "id": 1, "result": { "response": { "ResponseBase": { "Error": null, "Data": "eyJFcnJvciI6bnVsbCwiRGF0YSI6IiIsIkV2ZW50cyI6W10sIkxvZyI6Im1zZzowLHN1Y2Nlc3M6dHJ1ZSxsb2c6LGV2ZW50czpbXSIsIkluZm8iOiIiLCJHYXNXYW50ZWQiOjEwMDAwMDAsIkdhc1VzZWQiOjQ0NjI5fQ==", "Events": null, "Log": "", "Info": "" }, "Key": null, "Value": null, "Proof": null, "Height": "0" } } } ``` ### Related Issue - https://github.com/gnolang/gno/issues/1826
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: n3wbie Co-authored-by: Miloš Živković --- .../cmd/gnoland/testdata/simulate_gas.txtar | 28 ++++++++++++ tm2/pkg/sdk/auth/ante.go | 45 +------------------ tm2/pkg/sdk/auth/ante_test.go | 5 ++- tm2/pkg/sdk/baseapp.go | 10 ++++- tm2/pkg/sdk/baseapp_test.go | 34 +++++++++++++- 5 files changed, 75 insertions(+), 47 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/simulate_gas.txtar diff --git a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar new file mode 100644 index 00000000000..cd58b4ccc8f --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar @@ -0,0 +1,28 @@ +# load the package +loadpkg gno.land/r/simulate $WORK/simulate + +# start a new node +gnoland start + +# simulate only +gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 +stdout 'GAS USED: 50299' + +# simulate skip +gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 +stdout 'GAS USED: 50299' # same as simulate only + + +-- package/package.gno -- +package call_package + +func Render() string { + return "notok" +} + +-- simulate/simulate.gno -- +package simulate + +func Hello() string { + return "Hello" +} diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index 49662b47a55..d36b376aa8d 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -15,11 +15,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/store" ) -var ( - // simulation signature values used to estimate gas consumption - simSecp256k1Pubkey secp256k1.PubKeySecp256k1 - simSecp256k1Sig [64]byte -) +// simulation signature values used to estimate gas consumption +var simSecp256k1Pubkey secp256k1.PubKeySecp256k1 func init() { // This decodes a valid hex string into a sepc256k1Pubkey for use in transaction simulation @@ -228,14 +225,6 @@ func processSig( return nil, abciResult(std.ErrInternal("setting PubKey on signer's account")) } - if simulate { - // Simulated txs should not contain a signature and are not required to - // contain a pubkey, so we must account for tx size of including a - // std.Signature (Amino encoding) and simulate gas consumption - // (assuming a SECP256k1 simulation key). - consumeSimSigGas(ctx.GasMeter(), pubKey, sig, params) - } - if res := sigGasConsumer(ctx.GasMeter(), sig.Signature, pubKey, params); !res.IsOK() { return nil, res } @@ -251,42 +240,12 @@ func processSig( return acc, res } -func consumeSimSigGas(gasmeter store.GasMeter, pubkey crypto.PubKey, sig std.Signature, params Params) { - simSig := std.Signature{PubKey: pubkey} - if len(sig.Signature) == 0 { - simSig.Signature = simSecp256k1Sig[:] - } - - sigBz := amino.MustMarshalSized(simSig) - cost := store.Gas(len(sigBz) + 6) - - // If the pubkey is a multi-signature pubkey, then we estimate for the maximum - // number of signers. - if _, ok := pubkey.(multisig.PubKeyMultisigThreshold); ok { - cost *= params.TxSigLimit - } - - gasmeter.ConsumeGas(params.TxSizeCostPerByte*cost, "txSize") -} - // ProcessPubKey verifies that the given account address matches that of the // std.Signature. In addition, it will set the public key of the account if it // has not been set. func ProcessPubKey(acc std.Account, sig std.Signature, simulate bool) (crypto.PubKey, sdk.Result) { // If pubkey is not known for account, set it from the std.Signature. pubKey := acc.GetPubKey() - if simulate { - // In simulate mode the transaction comes with no signatures, thus if the - // account's pubkey is nil, both signature verification and gasKVStore.Set() - // shall consume the largest amount, i.e. it takes more gas to verify - // secp256k1 keys than ed25519 ones. - if pubKey == nil { - return simSecp256k1Pubkey, sdk.Result{} - } - - return pubKey, sdk.Result{} - } - if pubKey == nil { pubKey = sig.PubKey if pubKey == nil { diff --git a/tm2/pkg/sdk/auth/ante_test.go b/tm2/pkg/sdk/auth/ante_test.go index be4167a6238..86e34391770 100644 --- a/tm2/pkg/sdk/auth/ante_test.go +++ b/tm2/pkg/sdk/auth/ante_test.go @@ -611,10 +611,11 @@ func TestProcessPubKey(t *testing.T) { wantErr bool }{ {"no sigs, simulate off", args{acc1, std.Signature{}, false}, true}, - {"no sigs, simulate on", args{acc1, std.Signature{}, true}, false}, + {"no sigs, simulate on", args{acc1, std.Signature{}, true}, true}, + {"no sigs, account with pub, simulate off", args{acc2, std.Signature{}, false}, false}, {"no sigs, account with pub, simulate on", args{acc2, std.Signature{}, true}, false}, {"pubkey doesn't match addr, simulate off", args{acc1, std.Signature{PubKey: priv2.PubKey()}, false}, true}, - {"pubkey doesn't match addr, simulate on", args{acc1, std.Signature{PubKey: priv2.PubKey()}, true}, false}, + {"pubkey doesn't match addr, simulate on", args{acc1, std.Signature{PubKey: priv2.PubKey()}, true}, true}, } for _, tt := range tests { tt := tt diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 415309eab9a..1802a21f453 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -409,8 +409,16 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc } else { result = app.Simulate(txBytes, tx) } + res.Height = req.Height - res.Value = amino.MustMarshal(result) + + bytes, err := amino.Marshal(result) + if err != nil { + res.Error = ABCIError(std.ErrInternal(fmt.Sprintf("cannot encode to JSON: %s", err))) + } else { + res.Value = bytes + } + return res case "version": res.Height = req.Height diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 08e8191170a..cf944c44f06 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -634,6 +634,38 @@ func TestDeliverTx(t *testing.T) { } } +// Test that the gas used between Simulate and DeliverTx is the same. +func TestGasUsedBetweenSimulateAndDeliver(t *testing.T) { + t.Parallel() + + anteKey := []byte("ante-key") + anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, mainKey, anteKey)) } + + deliverKey := []byte("deliver-key") + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, newMsgCounterHandler(t, mainKey, deliverKey)) + } + + app := setupBaseApp(t, anteOpt, routerOpt) + app.InitChain(abci.RequestInitChain{ChainID: "test-chain"}) + + header := &bft.Header{ChainID: "test-chain", Height: 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + tx := newTxCounter(0, 0) + txBytes, err := amino.Marshal(tx) + require.Nil(t, err) + + simulateRes := app.Simulate(txBytes, tx) + require.True(t, simulateRes.IsOK(), fmt.Sprintf("%v", simulateRes)) + require.Greater(t, simulateRes.GasUsed, int64(0)) // gas used should be greater than 0 + + deliverRes := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) + require.True(t, deliverRes.IsOK(), fmt.Sprintf("%v", deliverRes)) + + require.Equal(t, simulateRes.GasUsed, deliverRes.GasUsed) // gas used should be the same from simulate and deliver +} + // One call to DeliverTx should process all the messages, in order. func TestMultiMsgDeliverTx(t *testing.T) { t.Parallel() @@ -753,7 +785,7 @@ func TestSimulateTx(t *testing.T) { require.True(t, queryResult.IsOK(), queryResult.Log) var res Result - amino.MustUnmarshal(queryResult.Value, &res) + require.NoError(t, amino.Unmarshal(queryResult.Value, &res)) require.Nil(t, err, "Result unmarshalling failed") require.True(t, res.IsOK(), res.Log) require.Equal(t, gasConsumed, res.GasUsed, res.Log) From c1b928faaa1562aa10fd806939ac04ea060a1caa Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:29:40 +0100 Subject: [PATCH 286/345] docs: update test3 mentions, add test5 mentions (#3259) ## Description This PR updates the mentions of test3 after its deprecation, and adds text on test5.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- docs/concepts/testnets.md | 48 +++++++++++++++++------------ docs/reference/network-config.md | 12 ++++---- docs/reference/stdlibs/std/chain.md | 2 +- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/docs/concepts/testnets.md b/docs/concepts/testnets.md index 4df8e3a4b86..b5286eaec57 100644 --- a/docs/concepts/testnets.md +++ b/docs/concepts/testnets.md @@ -21,6 +21,7 @@ gno.land testnets are categorized by 4 main points: Below you can find a breakdown of each existing testnet by these categories. ## Portal Loop + Portal Loop is an always up-to-date rolling testnet. It is meant to be used as a nightly build of the Gno tech stack. The home page of [gno.land](https://gno.land) is the `gnoweb` render of the Portal Loop testnet. @@ -43,8 +44,28 @@ For more information on the Portal Loop, and how it can be best utilized, check out the [Portal Loop concept page](./portal-loop.md). Also, you can find the Portal Loop faucet on [`gno.land/faucet`](https://gno.land/faucet). +## Test5 + +Test5 a permanent multi-node testnet. It bumped the validator set from 7 to 17 +nodes, introduced GovDAO V2, and added lots of bug fixes and quality of life +improvements. + +Test5 was launched in November 2024. + +- **Persistence of state:** + - State is fully persisted unless there are breaking changes in a new release, + where persistence partly depends on implementing a migration strategy +- **Timeliness of code:** + - Pre-deployed packages and realms are at monorepo commit [2e9f5ce](https://github.com/gnolang/gno/tree/2e9f5ce8ecc90ee81eb3ae41c06bab30ab926150) +- **Intended purpose** + - Running a full node, testing validator coordination, deploying stable Gno + dApps, creating tools that require persisted state & transaction history +- **Versioning strategy**: + - Test5 is to be release-based, following releases of the Gno tech stack. + ## Test4 -Test4 a permanent multi-node testnet. + +Test4 is the first permanent multi-node testnet, launched in July 2024. - **Persistence of state:** - State is fully persisted unless there are breaking changes in a new release, @@ -59,6 +80,7 @@ Test4 a permanent multi-node testnet. of the Gno tech stack. ## Staging + Staging is a testnet that is reset once every 60 minutes. - **Persistence of state:** @@ -73,39 +95,25 @@ Staging is a testnet that is reset once every 60 minutes. - Staging is reset every 60 minutes to match the latest monorepo commit ## TestX -These testnets are deprecated and currently serve as archives of previous progress. - -### Test3 -Test3 is the most recent persistent Gno testnet. It is still being used, but -most packages, such as the AVL package, are outdated. -- **Persistence of state:** - - State is fully preserved -- **Timeliness of code:** - - Test3 is at commit [1ca2d97](https://github.com/gnolang/gno/commit/1ca2d973817b174b5b06eb9da011e1fcd2cca575) -of Gno, and it can contain new on-chain code -- **Intended purpose** - - Running a full node, building an indexer, showing demos, persisting history -- **Versioning strategy**: - - There is no versioning strategy for test3. It will stay the way it is, until -the team chooses to shut it down. +These testnets are deprecated and currently serve as archives of previous progress. -Since gno.land is designed with open-source in mind, anyone can see currently -available code by browsing the [test3 homepage](https://test3.gno.land/). +### Test3 (archive) -Test3 is a single-node testnet, ran by the Gno core team. There is no plan to -upgrade test3 to a multi-node testnet. +The third Gno testnet. Archived data for test3 can be found [here](https://github.com/gnolang/tx-exports/tree/main/test3.gno.land). Launch date: November 4th 2022 Release commit: [1ca2d97](https://github.com/gnolang/gno/commit/1ca2d973817b174b5b06eb9da011e1fcd2cca575) ### Test2 (archive) + The second Gno testnet. Find archive data [here](https://github.com/gnolang/tx-exports/tree/main/test2.gno.land). Launch date: July 10th 2022 Release commit: [652dc7a](https://github.com/gnolang/gno/commit/652dc7a3a62ee0438093d598d123a8c357bf2499) ### Test1 (archive) + The first Gno testnet. Find archive data [here](https://github.com/gnolang/tx-exports/tree/main/test1.gno.land). Launch date: May 6th 2022 diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 6d4fc9ea14a..45a56b772ae 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -4,12 +4,12 @@ id: network-config # Network configurations -| Network | RPC Endpoint | Chain ID | -|-------------|-----------------------------------|---------------| -| Portal Loop | https://rpc.gno.land:443 | `portal-loop` | -| Test4 | https://rpc.test4.gno.land:443 | `test4` | -| Test3 | https://rpc.test3.gno.land:443 | `test3` | -| Staging | https://rpc.staging.gno.land:443 | `staging` | +| Network | RPC Endpoint | Chain ID | +|-------------|----------------------------------|---------------| +| Portal Loop | https://rpc.gno.land:443 | `portal-loop` | +| Test5 | https://rpc.test5.gno.land:443 | `test5` | +| Test4 | https://rpc.test4.gno.land:443 | `test4` | +| Staging | https://rpc.staging.gno.land:443 | `staging` | ### WebSocket endpoints All networks follow the same pattern for websocket connections: diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 089de682cfd..0e5ead338c5 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -49,7 +49,7 @@ Returns the chain ID. #### Usage ```go -chainID := std.GetChainID() // dev | test3 | main ... +chainID := std.GetChainID() // dev | test5 | main ... ``` --- From ebb49480fc73c4baac13452e0f56c5d8235ad05f Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Wed, 4 Dec 2024 22:27:58 +0100 Subject: [PATCH 287/345] feat: Add handling for float types, and also for %v (#3263) This PR includes the changes to `ufmt.Sprintf()` from #2868 as well as another branch where I had added support for `%v`. These changes add support for a variety of float formatting options to Sprintf, as well as support for the %v flag to automatically chose a default representation for a given type. Tests were added for these additions. This PR is needed for the PRNG PR (#2868) to be fully functional, as the generators include some built in statistical examples/tests which will not function without `ufmt.Sprintf()` support for floats.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs
--------- Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- examples/gno.land/p/demo/ufmt/ufmt.gno | 104 +++++++++++++++++--- examples/gno.land/p/demo/ufmt/ufmt_test.gno | 13 +++ 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/examples/gno.land/p/demo/ufmt/ufmt.gno b/examples/gno.land/p/demo/ufmt/ufmt.gno index c2abf43c85a..c9acee1c910 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt.gno @@ -22,6 +22,8 @@ func Println(args ...interface{}) { strs = append(strs, v.String()) case error: strs = append(strs, v.Error()) + case float64: + strs = append(strs, Sprintf("%f", v)) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: strs = append(strs, Sprintf("%d", v)) case bool: @@ -49,21 +51,28 @@ func Println(args ...interface{}) { // // The currently formatted verbs are the following: // -// %s: places a string value directly. -// If the value implements the interface interface{ String() string }, -// the String() method is called to retrieve the value. Same about Error() -// string. -// %c: formats the character represented by Unicode code point -// %d: formats an integer value using package "strconv". -// Currently supports only uint, uint64, int, int64. -// %t: formats a boolean value to "true" or "false". -// %x: formats an integer value as a hexadecimal string. -// Currently supports only uint8, []uint8, [32]uint8. -// %c: formats a rune value as a string. -// Currently supports only rune, int. -// %q: formats a string value as a quoted string. -// %T: formats the type of the value. -// %%: outputs a literal %. Does not consume an argument. +// %s: places a string value directly. +// If the value implements the interface interface{ String() string }, +// the String() method is called to retrieve the value. Same about Error() +// string. +// %c: formats the character represented by Unicode code point +// %d: formats an integer value using package "strconv". +// Currently supports only uint, uint64, int, int64. +// %f: formats a float value, with a default precision of 6. +// %e: formats a float with scientific notation; 1.23456e+78 +// %E: formats a float with scientific notation; 1.23456E+78 +// %F: The same as %f +// %g: formats a float value with %e for large exponents, and %f with full precision for smaller numbers +// %G: formats a float value with %G for large exponents, and %F with full precision for smaller numbers +// %t: formats a boolean value to "true" or "false". +// %x: formats an integer value as a hexadecimal string. +// Currently supports only uint8, []uint8, [32]uint8. +// %c: formats a rune value as a string. +// Currently supports only rune, int. +// %q: formats a string value as a quoted string. +// %T: formats the type of the value. +// %v: formats the value with a default representation appropriate for the value's type +// %%: outputs a literal %. Does not consume an argument. func Sprintf(format string, args ...interface{}) string { // we use runes to handle multi-byte characters sTor := []rune(format) @@ -97,6 +106,51 @@ func Sprintf(format string, args ...interface{}) string { argNum++ switch verb { + case "v": + switch v := arg.(type) { + case nil: + buf += "" + case bool: + if v { + buf += "true" + } else { + buf += "false" + } + case int: + buf += strconv.Itoa(v) + case int8: + buf += strconv.Itoa(int(v)) + case int16: + buf += strconv.Itoa(int(v)) + case int32: + buf += strconv.Itoa(int(v)) + case int64: + buf += strconv.Itoa(int(v)) + case uint: + buf += strconv.FormatUint(uint64(v), 10) + case uint8: + buf += strconv.FormatUint(uint64(v), 10) + case uint16: + buf += strconv.FormatUint(uint64(v), 10) + case uint32: + buf += strconv.FormatUint(uint64(v), 10) + case uint64: + buf += strconv.FormatUint(v, 10) + case float64: + buf += strconv.FormatFloat(v, 'g', -1, 64) + case string: + buf += v + case []byte: + buf += string(v) + case []rune: + buf += string(v) + case interface{ String() string }: + buf += v.String() + case error: + buf += v.Error() + default: + buf += fallback(verb, v) + } case "s": switch v := arg.(type) { case interface{ String() string }: @@ -153,6 +207,24 @@ func Sprintf(format string, args ...interface{}) string { default: buf += fallback(verb, v) } + case "e", "E", "f", "F", "g", "G": + switch v := arg.(type) { + case float64: + switch verb { + case "e": + buf += strconv.FormatFloat(v, byte('e'), -1, 64) + case "E": + buf += strconv.FormatFloat(v, byte('E'), -1, 64) + case "f", "F": + buf += strconv.FormatFloat(v, byte('f'), 6, 64) + case "g": + buf += strconv.FormatFloat(v, byte('g'), -1, 64) + case "G": + buf += strconv.FormatFloat(v, byte('G'), -1, 64) + } + default: + buf += fallback(verb, v) + } case "t": switch v := arg.(type) { case bool: @@ -244,6 +316,8 @@ func fallback(verb string, arg interface{}) string { case error: // note: also "string=" in Go fmt s = "string=" + v.Error() + case float64: + s = "float64=" + Sprintf("%f", v) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: // note: rune, byte would be dups, being aliases if typename, e := typeToString(v); e != nil { diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index 2a583202a93..1cb7231a611 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -20,21 +20,34 @@ func TestSprintf(t *testing.T) { expectedOutput string }{ {"hello %s!", []interface{}{"planet"}, "hello planet!"}, + {"hello %v!", []interface{}{"planet"}, "hello planet!"}, {"hi %%%s!", []interface{}{"worl%d"}, "hi %worl%d!"}, {"%s %c %d %t", []interface{}{"foo", 'α', 421, true}, "foo α 421 true"}, {"string [%s]", []interface{}{"foo"}, "string [foo]"}, {"int [%d]", []interface{}{int(42)}, "int [42]"}, + {"int [%v]", []interface{}{int(42)}, "int [42]"}, {"int8 [%d]", []interface{}{int8(8)}, "int8 [8]"}, + {"int8 [%v]", []interface{}{int8(8)}, "int8 [8]"}, {"int16 [%d]", []interface{}{int16(16)}, "int16 [16]"}, + {"int16 [%v]", []interface{}{int16(16)}, "int16 [16]"}, {"int32 [%d]", []interface{}{int32(32)}, "int32 [32]"}, + {"int32 [%v]", []interface{}{int32(32)}, "int32 [32]"}, {"int64 [%d]", []interface{}{int64(64)}, "int64 [64]"}, + {"int64 [%v]", []interface{}{int64(64)}, "int64 [64]"}, {"uint [%d]", []interface{}{uint(42)}, "uint [42]"}, + {"uint [%v]", []interface{}{uint(42)}, "uint [42]"}, {"uint8 [%d]", []interface{}{uint8(8)}, "uint8 [8]"}, + {"uint8 [%v]", []interface{}{uint8(8)}, "uint8 [8]"}, {"uint16 [%d]", []interface{}{uint16(16)}, "uint16 [16]"}, + {"uint16 [%v]", []interface{}{uint16(16)}, "uint16 [16]"}, {"uint32 [%d]", []interface{}{uint32(32)}, "uint32 [32]"}, + {"uint32 [%v]", []interface{}{uint32(32)}, "uint32 [32]"}, {"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"}, + {"uint64 [%v]", []interface{}{uint64(64)}, "uint64 [64]"}, {"bool [%t]", []interface{}{true}, "bool [true]"}, + {"bool [%v]", []interface{}{true}, "bool [true]"}, {"bool [%t]", []interface{}{false}, "bool [false]"}, + {"bool [%v]", []interface{}{false}, "bool [false]"}, {"no args", nil, "no args"}, {"finish with %", nil, "finish with %"}, {"stringer [%s]", []interface{}{stringer{}}, "stringer [I'm a stringer]"}, From b631207ca737a6b1e3bd30d10d687ccaf9b4918b Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Thu, 5 Dec 2024 10:05:43 +0100 Subject: [PATCH 288/345] feat(examples): Add a useful set of high quality pseudo-random number generators (#2868) I ported a number of my pseudo-random number generator implementations from Ruby to gno while traveling to the retreat last weekend as an exercise in expanding my comfort level with gno code, and expanding my understanding of some of the code internals, while contributing code that others may find interesting or useful. I added two xorshift generators, xorshift64* and xorshiftr128+. These are both many times faster than the PCG generator that is the gno default, and produce high quality randomness with great statistical qualities. In addition to these, I added both the 32-bit ISAAC implementation (with an added function to return 64 bit values), and the 64-bit ISAAC implementation. ISAAC is a stellar pseudo-random number generator. Both implementations are significantly faster than PCG (though not near so fast as the xorshift algorithms), while producing extremely high quality, cryptographically secure randomness that can not be differentiated from real randomness. All of these were built to be compatible with the standard Rand() implementation. This means that any of these can be used as a drop-in replacement for the default PCG algorithm: ``` source = isaac.New() prng := rand.New(source) ``` All of these leverage the `gno.land/p/demo/entropy` package to assist with seeding if no seed is provided. In the case of the ISAAC algorithms, they require 256 uint values for their seed, so they leverage a combination of `entropy` and `xorshiftr128+` to generate any missing numbers in the provided seed. I also added a function to entropy to return uint64, to facilitate using it for seeding. I added tests to entropy, and wrote tests for the other generators, as well. There are a few other things that ended up in this PR. In order to make some fact based assertions about the performance of these generators, I included some code that can be ran via `gno run -expr`. i.e. `gno run -expr 'averageISAAC()' isaac.gno` that can be used to get some benchmarks and some very simple self-statistical-analysis on the results, and when I did so, I discovered that the current `ufmt.Sprintf` implementation didn't support any of the float output flags. I added float support to it's capabilities, which, in turn, required adding `FormatFloat` to the `strconv.gno/strconv.go` implementation in the standard library. I added a test to cover this. I also noticed that there is a test in `tm2/pkg/p2p` that is failing on both master and my branch. Specifically, there is a call to `sw.Logger.Error()` that passes a message and an error, but not `"err"` before the error. Adding that seemed to clear up the build failure. This, specifically, is line 222 of `switch.go`. Currently there is one failing test, which is the code coverage check on tm2, because it is non-obvious to me how to setup a test to properly exercise that one changed line.
Contributors' checklist... - [X] Added new tests, or not needed, or not feasible - [X] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [X] Updated the official documentation or not needed - [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [X] Added references to related issues and PRs
--------- Co-authored-by: Morgan Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Morgan --- examples/gno.land/p/demo/entropy/entropy.gno | 8 + .../gno.land/p/demo/entropy/entropy_test.gno | 32 ++ .../gno.land/p/demo/entropy/z_filetest.gno | 6 + examples/gno.land/p/demo/ufmt/ufmt_test.gno | 6 + .../gno.land/p/wyhaines/rand/isaac/README.md | 86 ++++ .../gno.land/p/wyhaines/rand/isaac/gno.mod | 7 + .../gno.land/p/wyhaines/rand/isaac/isaac.gno | 435 ++++++++++++++++++ .../p/wyhaines/rand/isaac/isaac_test.gno | 165 +++++++ .../p/wyhaines/rand/isaac64/README.md | 97 ++++ .../gno.land/p/wyhaines/rand/isaac64/gno.mod | 7 + .../p/wyhaines/rand/isaac64/isaac64.gno | 429 +++++++++++++++++ .../p/wyhaines/rand/isaac64/isaac64_test.gno | 165 +++++++ .../p/wyhaines/rand/xorshift64star/README.MD | 69 +++ .../p/wyhaines/rand/xorshift64star/gno.mod | 6 + .../rand/xorshift64star/xorshift64star.gno | 172 +++++++ .../xorshift64star/xorshift64star_test.gno | 134 ++++++ .../wyhaines/rand/xorshiftr128plus/README.MD | 60 +++ .../p/wyhaines/rand/xorshiftr128plus/gno.mod | 6 + .../xorshiftr128plus/xorshiftr128plus.gno | 186 ++++++++ .../xorshiftr128plus_test.gno | 142 ++++++ 20 files changed, 2218 insertions(+) create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/README.md create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/isaac.gno create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/README.md create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno diff --git a/examples/gno.land/p/demo/entropy/entropy.gno b/examples/gno.land/p/demo/entropy/entropy.gno index 5e35b8c7227..9e8f656c21b 100644 --- a/examples/gno.land/p/demo/entropy/entropy.gno +++ b/examples/gno.land/p/demo/entropy/entropy.gno @@ -87,3 +87,11 @@ func (i *Instance) Value() uint32 { i.addEntropy() return i.value } + +func (i *Instance) Value64() uint64 { + i.addEntropy() + high := i.value + i.addEntropy() + + return (uint64(high) << 32) | uint64(i.value) +} diff --git a/examples/gno.land/p/demo/entropy/entropy_test.gno b/examples/gno.land/p/demo/entropy/entropy_test.gno index 0deb3ab9aa2..895bfd1e394 100644 --- a/examples/gno.land/p/demo/entropy/entropy_test.gno +++ b/examples/gno.land/p/demo/entropy/entropy_test.gno @@ -33,6 +33,26 @@ func TestInstanceValue(t *testing.T) { } } +func TestInstanceValue64(t *testing.T) { + baseEntropy := New() + baseResult := computeValue64(t, baseEntropy) + + sameHeightEntropy := New() + sameHeightResult := computeValue64(t, sameHeightEntropy) + + if baseResult != sameHeightResult { + t.Errorf("should have the same result: new=%s, base=%s", sameHeightResult, baseResult) + } + + std.TestSkipHeights(1) + differentHeightEntropy := New() + differentHeightResult := computeValue64(t, differentHeightEntropy) + + if baseResult == differentHeightResult { + t.Errorf("should have different result: new=%s, base=%s", differentHeightResult, baseResult) + } +} + func computeValue(t *testing.T, r *Instance) string { t.Helper() @@ -44,3 +64,15 @@ func computeValue(t *testing.T, r *Instance) string { return out } + +func computeValue64(t *testing.T, r *Instance) string { + t.Helper() + + out := "" + for i := 0; i < 10; i++ { + val := int(r.Value64()) + out += strconv.Itoa(val) + " " + } + + return out +} diff --git a/examples/gno.land/p/demo/entropy/z_filetest.gno b/examples/gno.land/p/demo/entropy/z_filetest.gno index 85ed1b10a3d..ddee29b22fd 100644 --- a/examples/gno.land/p/demo/entropy/z_filetest.gno +++ b/examples/gno.land/p/demo/entropy/z_filetest.gno @@ -15,6 +15,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) // should be the same println("---") @@ -24,6 +25,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) std.TestSkipHeights(1) println("---") @@ -33,6 +35,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) } // Output: @@ -42,15 +45,18 @@ func main() { // 1950222777 // 3348280598 // 438354259 +// 6353385488959065197 // --- // 4129293727 // 2141104956 // 1950222777 // 3348280598 // 438354259 +// 6353385488959065197 // --- // 49506731 // 1539580078 // 2695928529 // 1895482388 // 3462727799 +// 16745038698684748445 diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index 1cb7231a611..1a4d4e7e6f2 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -44,6 +44,12 @@ func TestSprintf(t *testing.T) { {"uint32 [%v]", []interface{}{uint32(32)}, "uint32 [32]"}, {"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"}, {"uint64 [%v]", []interface{}{uint64(64)}, "uint64 [64]"}, + {"float64 [%e]", []interface{}{float64(64.1)}, "float64 [6.41e+01]"}, + {"float64 [%E]", []interface{}{float64(64.1)}, "float64 [6.41E+01]"}, + {"float64 [%f]", []interface{}{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%F]", []interface{}{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%g]", []interface{}{float64(64.1)}, "float64 [64.1]"}, + {"float64 [%G]", []interface{}{float64(64.1)}, "float64 [64.1]"}, {"bool [%t]", []interface{}{true}, "bool [true]"}, {"bool [%v]", []interface{}{true}, "bool [true]"}, {"bool [%t]", []interface{}{false}, "bool [false]"}, diff --git a/examples/gno.land/p/wyhaines/rand/isaac/README.md b/examples/gno.land/p/wyhaines/rand/isaac/README.md new file mode 100644 index 00000000000..05f4a94425f --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/README.md @@ -0,0 +1,86 @@ +# package isaac // import "gno.land/p/demo/math/rand/isaac" + +This is a port of the ISAAC cryptographically secure PRNG, +originally based on the reference implementation found at +https://burtleburtle.net/bob/rand/isaacafa.html + +ISAAC has excellent statistical properties, with long cycle times, and +uniformly distributed, unbiased, and unpredictable number generation. It can +not be distinguished from real random data, and in three decades of scrutiny, +no practical attacks have been found. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This +algorithm provides very strong statistical performance, and is cryptographically +secure, while still being substantially faster than the default PCG +implementation in `math/rand`. Note that this package does implement a `Uint64()` +function in order to generate a 64 bit number out of two 32 bit numbers. Doing this +makes the generator only slightly faster than PCG, however, + +Note that the approach to seeing with ISAAC is very important for best results, +and seeding with ISAAC is not as simple as seeding with a single uint64 value. +The ISAAC algorithm requires a 256-element seed. If used for cryptographic +purposes, this will likely require entropy generated off-chain for actual +cryptographically secure seeding. For other purposes, however, one can utilize +the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to +generate any missing seeds if fewer than 256 are provided. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +ISAAC: 1000000 Uint64 generated in 13.23s (uint64) +ISAAC: 1000000 Uint32 generated in 6.43s (uint32) +Ratio: x1.18 times faster than PCG (uint64) +Ratio: x2.42 times faster than PCG (uint32) +``` + +Use it directly: + +``` +prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest + // will be generated using the xorshiftr128plus PRNG. +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = isaac.New() +prng := rand.New(source) +``` + +# TYPES + +` +type ISAAC struct { + // Has unexported fields. +} +` + +`func New(seeds ...uint32) *ISAAC` + ISAAC requires a large, 256-element seed. This implementation will leverage + the entropy package combined with the the xorshiftr128plus PRNG to generate + any missing seeds of fewer than the required number of arguments are + provided. + +`func (isaac *ISAAC) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (isaac *ISAAC) Seed(seed [256]uint32)` + +`func (isaac *ISAAC) Uint32() uint32` + +`func (isaac *ISAAC) Uint64() uint64` + +`func (isaac *ISAAC) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod new file mode 100644 index 00000000000..0cca6aa5174 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/wyhaines/rand/isaac + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno new file mode 100644 index 00000000000..4508dd5d5af --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno @@ -0,0 +1,435 @@ +// This is a port of the ISAAC cryptographically secure PRNG, originally based on the reference +// implementation found at https://burtleburtle.net/bob/rand/isaacafa.html +// +// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed, +// unbiased, and unpredictable number generation. It can not be distinguished from real random +// data, and in three decades of scrutiny, no practical attacks have been found. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementation, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This +// algorithm provides very strong statistical performance, and is cryptographically +// secure, while still being substantially faster than the default PCG +// implementation in `math/rand`. Note that this package does implement a `Uint64()` +// function in order to generate a 64 bit number out of two 32 bit numbers. Doing this +// makes the generator only slightly faster than PCG, however, +// +// Note that the approach to seeing with ISAAC is very important for best results, and seeding with +// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a +// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated +// off-chain for actual cryptographically secure seeding. For other purposes, however, one can +// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate +// any missing seeds if fewer than 256 are provided. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// ISAAC: 1000000 Uint64 generated in 13.23s +// ISAAC: 1000000 Uint32 generated in 6.43s +// Ratio: x1.18 times faster than PCG (uint64) +// Ratio: x2.42 times faster than PCG (uint32) +// +// Use it directly: +// +// prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest +// // will be generated using the xorshiftr128plus PRNG. +// +// Or use it as a drop-in replacement for the default PRNG in Rand: +// +// source = isaac.New() +// prng := rand.New(source) +package isaac + +import ( + "errors" + "math" + "math/rand" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" + "gno.land/p/wyhaines/rand/xorshiftr128plus" +) + +type ISAAC struct { + randrsl [256]uint32 + randcnt uint32 + mm [256]uint32 + aa, bb, cc uint32 + seed [256]uint32 +} + +// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy +// package combined with the the xorshiftr128plus PRNG to generate any missing seeds of +// fewer than the required number of arguments are provided. +func New(seeds ...uint32) *ISAAC { + isaac := &ISAAC{} + seed := [256]uint32{} + + index := 0 + for index = 0; index < len(seeds); index++ { + seed[index] = seeds[index] + } + + if index < 4 { + e := entropy.New() + for ; index < 4; index++ { + seed[index] = e.Value() + } + } + + // Use up to the first four seeds as seeding inputs for xorshiftr128+, in order to + // use it to provide any remaining missing seeds. + prng := xorshiftr128plus.New( + (uint64(seed[0])<<32)|uint64(seed[1]), + (uint64(seed[2])<<32)|uint64(seed[3]), + ) + for ; index < 256; index += 2 { + val := prng.Uint64() + seed[index] = uint32(val & 0xffffffff) + if index+1 < 256 { + seed[index+1] = uint32(val >> 32) + } + } + isaac.Seed(seed) + return isaac +} + +func (isaac *ISAAC) Seed(seed [256]uint32) { + isaac.randrsl = seed + isaac.seed = seed + isaac.randinit(true) +} + +// beUint32() decodes a uint32 from a set of four bytes, assuming big endian encoding. +// binary.bigEndian.Uint32, copied to avoid dependency +func beUint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +// bePutUint32() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint32, copied to avoid dependency +func bePutUint32(b []byte, v uint32) { + _ = b[3] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 24) + b[1] = byte(v >> 16) + b[2] = byte(v >> 8) + b[3] = byte(v) +} + +// A label to identify the marshalled data. +var marshalISAACLabel = []byte("isaac:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (isaac *ISAAC) MarshalBinary() ([]byte, error) { + b := make([]byte, 3094) // 6 + 1024 + 1024 + 1024 + 4 + 4 + 4 + 4 == 3090 + copy(b, marshalISAACLabel) + for i := 0; i < 256; i++ { + bePutUint32(b[6+i*4:], isaac.seed[i]) + } + for i := 256; i < 512; i++ { + bePutUint32(b[6+i*4:], isaac.randrsl[i-256]) + } + for i := 512; i < 768; i++ { + bePutUint32(b[6+i*4:], isaac.mm[i-512]) + } + bePutUint32(b[3078:], isaac.aa) + bePutUint32(b[3082:], isaac.bb) + bePutUint32(b[3086:], isaac.cc) + bePutUint32(b[3090:], isaac.randcnt) + + return b, nil +} + +// errUnmarshalISAAC is returned when unmarshalling fails. +var errUnmarshalISAAC = errors.New("invalid ISAAC encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (isaac *ISAAC) UnmarshalBinary(data []byte) error { + if len(data) != 3094 || string(data[:6]) != string(marshalISAACLabel) { + return errUnmarshalISAAC + } + for i := 0; i < 256; i++ { + isaac.seed[i] = beUint32(data[6+i*4:]) + } + for i := 256; i < 512; i++ { + isaac.randrsl[i-256] = beUint32(data[6+i*4:]) + } + for i := 512; i < 768; i++ { + isaac.mm[i-512] = beUint32(data[6+i*4:]) + } + isaac.aa = beUint32(data[3078:]) + isaac.bb = beUint32(data[3082:]) + isaac.cc = beUint32(data[3086:]) + isaac.randcnt = beUint32(data[3090:]) + return nil +} + +func (isaac *ISAAC) randinit(flag bool) { + isaac.aa = 0 + isaac.bb = 0 + isaac.cc = 0 + + var a, b, c, d, e, f, g, h uint32 = 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9 + + for i := 0; i < 4; i++ { + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + } + + for i := 0; i < 256; i += 8 { + if flag { + a += isaac.randrsl[i] + b += isaac.randrsl[i+1] + c += isaac.randrsl[i+2] + d += isaac.randrsl[i+3] + e += isaac.randrsl[i+4] + f += isaac.randrsl[i+5] + g += isaac.randrsl[i+6] + h += isaac.randrsl[i+7] + } + + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + + if flag { + for i := 0; i < 256; i += 8 { + a += isaac.mm[i] + b += isaac.mm[i+1] + c += isaac.mm[i+2] + d += isaac.mm[i+3] + e += isaac.mm[i+4] + f += isaac.mm[i+5] + g += isaac.mm[i+6] + h += isaac.mm[i+7] + + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + } + + isaac.isaac() + isaac.randcnt = uint32(256) +} + +func (isaac *ISAAC) isaac() { + isaac.cc++ + isaac.bb += isaac.cc + + for i := 0; i < 256; i++ { + x := isaac.mm[i] + switch i % 4 { + case 0: + isaac.aa ^= isaac.aa << 13 + case 1: + isaac.aa ^= isaac.aa >> 6 + case 2: + isaac.aa ^= isaac.aa << 2 + case 3: + isaac.aa ^= isaac.aa >> 16 + } + isaac.aa += isaac.mm[(i+128)&0xff] + + y := isaac.mm[(x>>2)&0xff] + isaac.aa + isaac.bb + isaac.mm[i] = y + isaac.bb = isaac.mm[(y>>10)&0xff] + x + isaac.randrsl[i] = isaac.bb + } +} + +// Returns a random uint32. +func (isaac *ISAAC) Uint32() uint32 { + if isaac.randcnt == uint32(0) { + isaac.isaac() + isaac.randcnt = uint32(256) + } + isaac.randcnt-- + return isaac.randrsl[isaac.randcnt] +} + +// Returns a random uint64 by combining two uint32s. +func (isaac *ISAAC) Uint64() uint64 { + return uint64(isaac.Uint32()) | (uint64(isaac.Uint32()) << 32) +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkISAAC()' xorshift64star.gno +func benchmarkISAAC(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New() + + for i := 0; i < iterations; i++ { + _ = isaac.Uint64() + } + ufmt.Println(ufmt.Sprintf("ISAAC: generate %d uint64\n", iterations)) +} + +// The averageISAAC() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the ISAAC PRNG. +func averageISAAC(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New(987654321, 123456789, 999999999, 111111111) + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("ISAAC average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("ISAAC standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("ISAAC theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} + +func averagePCG(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := rand.NewPCG(987654321, 123456789) + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("PCG average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("PCG standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("PCG theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno new file mode 100644 index 00000000000..b08621e271c --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno @@ -0,0 +1,165 @@ +package isaac + +import ( + "math/rand" + "testing" +) + +type OpenISAAC struct { + Randrsl [256]uint32 + Randcnt uint32 + Mm [256]uint32 + Aa, Bb, Cc uint32 + Seed [256]uint32 +} + +func TestISAACSeeding(t *testing.T) { + isaac := New() +} + +func TestISAACRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.17828173023837635, + 0.7327795780287832, + 0.4850369074875177, + 0.9474842397428482, + 0.6747135561813891, + 0.7522507082868403, + 0.041115261836534356, + 0.7405243709084567, + 0.672863376128768, + 0.11866211399980553, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestISAACUint64(t *testing.T) { + isaac := New() + + expected := []uint64{ + 5986068031949215749, + 10437354066128700566, + 13478007513323023970, + 8969511410255984224, + 3869229557962857982, + 1762449743873204415, + 5292356290662282456, + 7893982194485405616, + 4296136494566588699, + 12414349056998262772, + } + + for i, exp := range expected { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func dupState(i *ISAAC) *OpenISAAC { + state := &OpenISAAC{} + state.Seed = i.seed + state.Randrsl = i.randrsl + state.Mm = i.mm + state.Aa = i.aa + state.Bb = i.bb + state.Cc = i.cc + state.Randcnt = i.randcnt + + return state +} + +func TestISAACMarshalUnmarshal(t *testing.T) { + isaac := New() + + expected1 := []uint64{ + 5986068031949215749, + 10437354066128700566, + 13478007513323023970, + 8969511410255984224, + 3869229557962857982, + } + + expected2 := []uint64{ + 1762449743873204415, + 5292356290662282456, + 7893982194485405616, + 4296136494566588699, + 12414349056998262772, + } + + for i, exp := range expected1 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := isaac.MarshalBinary() + + t.Logf("State: [%v]\n", dupState(isaac)) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := dupState(isaac) + + if err != nil { + t.Errorf("ISAAC.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + isaac.Uint64() + + for i, exp := range expected2 { + val := isaac.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%v]\n", dupState(isaac)) + + // Now restore the state of the PRNG + err = isaac.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%v]\n", dupState(isaac)) + + if state_before.Seed != dupState(isaac).Seed { + t.Errorf("Seed mismatch") + } + if state_before.Randrsl != dupState(isaac).Randrsl { + t.Errorf("Randrsl mismatch") + } + if state_before.Mm != dupState(isaac).Mm { + t.Errorf("Mm mismatch") + } + if state_before.Aa != dupState(isaac).Aa { + t.Errorf("Aa mismatch") + } + if state_before.Bb != dupState(isaac).Bb { + t.Errorf("Bb mismatch") + } + if state_before.Cc != dupState(isaac).Cc { + t.Errorf("Cc mismatch") + } + if state_before.Randcnt != dupState(isaac).Randcnt { + t.Errorf("Randcnt mismatch") + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/README.md b/examples/gno.land/p/wyhaines/rand/isaac64/README.md new file mode 100644 index 00000000000..813b062a5cd --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/README.md @@ -0,0 +1,97 @@ +# package isaac64 // import "gno.land/p/demo/math/rand/isaac64" + +This is a port of the 64-bit version of the ISAAC cryptographically +secure PRNG, originally based on the reference implementation found at +https://burtleburtle.net/bob/rand/isaacafa.html + +ISAAC has excellent statistical properties, with long cycle times, and +uniformly distributed, unbiased, and unpredictable number generation. It can +not be distinguished from real random data, and in three decades of scrutiny, +no practical attacks have been found. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This +algorithm provides very strong statistical performance, and is cryptographically +secure, while still being substantially faster than the default PCG +implementation in `math/rand`. + +Note that the approach to seeing with ISAAC is very important for best results, +and seeding with ISAAC is not as simple as seeding with a single uint64 value. +The ISAAC algorithm requires a 256-element seed. If used for cryptographic +purposes, this will likely require entropy generated off-chain for actual +cryptographically secure seeding. For other purposes, however, one can utilize +the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to +generate any missing seeds if fewer than 256 are provided. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +ISAAC: 1000000 Uint64 generated in 8.95s +ISAAC: 1000000 Uint32 generated in 7.66s +Ratio: x1.74 times faster than PCG (uint64) +Ratio: x2.03 times faster than PCG (uint32) +``` + +Use it directly: + + +``` +prng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest + // will be generated using the xorshiftr128plus PRNG. +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = isaac64.New() +prng := rand.New(source) +``` + +## CONSTANTS + + +``` +const ( + RANDSIZL = 8 + RANDSIZ = 1 << RANDSIZL // 256 +) +``` + +## TYPES + + +``` +type ISAAC struct { + // Has unexported fields. +} +``` + +`func New(seeds ...uint64) *ISAAC` +ISAAC requires a large, 256-element seed. This implementation will leverage +the entropy package combined with the xorshiftr128plus PRNG to generate any +missing seeds if fewer than the required number of arguments are provided. + +`func (isaac *ISAAC) MarshalBinary() ([]byte, error)` +MarshalBinary() returns a byte array that encodes the state of the PRNG. +This can later be used with UnmarshalBinary() to restore the state of the +PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (isaac *ISAAC) Seed(seed [256]uint64)` +Reinitialize the generator with a new seed. A seed must be composed of 256 uint64. + +`func (isaac *ISAAC) Uint32() uint32` +Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result. + +`func (isaac *ISAAC) Uint64() uint64` +Return a 64 bit random integer. + +`func (isaac *ISAAC) UnmarshalBinary(data []byte) error` +UnmarshalBinary() restores the state of the PRNG from a byte array +that was created with MarshalBinary(). UnmarshalBinary implements the +encoding.BinaryUnmarshaler interface. diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod new file mode 100644 index 00000000000..dbc8713094e --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/wyhaines/rand/isaac64 + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno new file mode 100644 index 00000000000..6f2d95150fc --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno @@ -0,0 +1,429 @@ +// This is a port of the 64-bit version of the ISAAC cryptographically secure PRNG, originally +// based on the reference implementation found at https://burtleburtle.net/bob/rand/isaacafa.html +// +// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed, +// unbiased, and unpredictable number generation. It can not be distinguished from real random +// data, and in three decades of scrutiny, no practical attacks have been found. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This algorithm +// provides very strong statistical performance, and is cryptographically secure, while still +// being substantially faster than the default PCG implementation in `math/rand`. +// +// Note that the approach to seeing with ISAAC is very important for best results, and seeding with +// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a +// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated +// off-chain for actual cryptographically secure seeding. For other purposes, however, one can +// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate +// any missing seeds if fewer than 256 are provided. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// ISAAC: 1000000 Uint64 generated in 8.95s +// ISAAC: 1000000 Uint32 generated in 7.66s +// Ratio: x1.74 times faster than PCG (uint64) +// Ratio: x2.03 times faster than PCG (uint32) +// +// Use it directly: +// +// prng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest +// // will be generated using the xorshiftr128plus PRNG. +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = isaac64.New() +// prng := rand.New(source) +package isaac64 + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" + "gno.land/p/wyhaines/rand/xorshiftr128plus" +) + +const ( + RANDSIZL = 8 + RANDSIZ = 1 << RANDSIZL // 256 +) + +type ISAAC struct { + randrsl [256]uint64 + randcnt uint64 + mm [256]uint64 + aa, bb, cc uint64 + seed [256]uint64 +} + +// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy +// package combined with the xorshiftr128plus PRNG to generate any missing seeds if fewer than +// the required number of arguments are provided. +func New(seeds ...uint64) *ISAAC { + isaac := &ISAAC{} + seed := [256]uint64{} + + index := 0 + for index = 0; index < len(seeds) && index < 256; index++ { + seed[index] = seeds[index] + } + + if index < 2 { + e := entropy.New() + for ; index < 2; index++ { + seed[index] = e.Value64() + } + } + + // Use the first two seeds as seeding inputs for xorshiftr128plus, in order to + // use it to provide any remaining missing seeds. + prng := xorshiftr128plus.New( + seed[0], + seed[1], + ) + for ; index < 256; index++ { + seed[index] = prng.Uint64() + } + isaac.Seed(seed) + return isaac +} + +// Reinitialize the generator with a new seed. A seed must be composed of 256 uint64. +func (isaac *ISAAC) Seed(seed [256]uint64) { + isaac.randrsl = seed + isaac.seed = seed + isaac.randinit(true) +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalISAACLabel = []byte("isaac:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (isaac *ISAAC) MarshalBinary() ([]byte, error) { + b := make([]byte, 6+2048*3+8*3+8) // 6 + 2048*3 + 8*3 + 8 == 6182 + copy(b, marshalISAACLabel) + offset := 6 + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.seed[i]) + offset += 8 + } + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.randrsl[i]) + offset += 8 + } + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.mm[i]) + offset += 8 + } + bePutUint64(b[offset:], isaac.aa) + offset += 8 + bePutUint64(b[offset:], isaac.bb) + offset += 8 + bePutUint64(b[offset:], isaac.cc) + offset += 8 + bePutUint64(b[offset:], isaac.randcnt) + return b, nil +} + +// errUnmarshalISAAC is returned when unmarshalling fails. +var errUnmarshalISAAC = errors.New("invalid ISAAC encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (isaac *ISAAC) UnmarshalBinary(data []byte) error { + if len(data) != 6182 || string(data[:6]) != string(marshalISAACLabel) { + return errUnmarshalISAAC + } + offset := 6 + for i := 0; i < 256; i++ { + isaac.seed[i] = beUint64(data[offset:]) + offset += 8 + } + for i := 0; i < 256; i++ { + isaac.randrsl[i] = beUint64(data[offset:]) + offset += 8 + } + for i := 0; i < 256; i++ { + isaac.mm[i] = beUint64(data[offset:]) + offset += 8 + } + isaac.aa = beUint64(data[offset:]) + offset += 8 + isaac.bb = beUint64(data[offset:]) + offset += 8 + isaac.cc = beUint64(data[offset:]) + offset += 8 + isaac.randcnt = beUint64(data[offset:]) + return nil +} + +func (isaac *ISAAC) randinit(flag bool) { + var a, b, c, d, e, f, g, h uint64 + isaac.aa = 0 + isaac.bb = 0 + isaac.cc = 0 + + a = 0x9e3779b97f4a7c13 + b = 0x9e3779b97f4a7c13 + c = 0x9e3779b97f4a7c13 + d = 0x9e3779b97f4a7c13 + e = 0x9e3779b97f4a7c13 + f = 0x9e3779b97f4a7c13 + g = 0x9e3779b97f4a7c13 + h = 0x9e3779b97f4a7c13 + + // scramble it + for i := 0; i < 4; i++ { + mix(&a, &b, &c, &d, &e, &f, &g, &h) + } + + // fill in mm[] with messy stuff + for i := 0; i < RANDSIZ; i += 8 { + if flag { + a += isaac.randrsl[i] + b += isaac.randrsl[i+1] + c += isaac.randrsl[i+2] + d += isaac.randrsl[i+3] + e += isaac.randrsl[i+4] + f += isaac.randrsl[i+5] + g += isaac.randrsl[i+6] + h += isaac.randrsl[i+7] + } + mix(&a, &b, &c, &d, &e, &f, &g, &h) + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + + if flag { + // do a second pass to make all of the seed affect all of mm + for i := 0; i < RANDSIZ; i += 8 { + a += isaac.mm[i] + b += isaac.mm[i+1] + c += isaac.mm[i+2] + d += isaac.mm[i+3] + e += isaac.mm[i+4] + f += isaac.mm[i+5] + g += isaac.mm[i+6] + h += isaac.mm[i+7] + mix(&a, &b, &c, &d, &e, &f, &g, &h) + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + } + + isaac.isaac() + isaac.randcnt = RANDSIZ +} + +func mix(a, b, c, d, e, f, g, h *uint64) { + *a -= *e + *f ^= *h >> 9 + *h += *a + + *b -= *f + *g ^= *a << 9 + *a += *b + + *c -= *g + *h ^= *b >> 23 + *b += *c + + *d -= *h + *a ^= *c << 15 + *c += *d + + *e -= *a + *b ^= *d >> 14 + *d += *e + + *f -= *b + *c ^= *e << 20 + *e += *f + + *g -= *c + *d ^= *f >> 17 + *f += *g + + *h -= *d + *e ^= *g << 14 + *g += *h +} + +func ind(mm []uint64, x uint64) uint64 { + return mm[(x>>3)&(RANDSIZ-1)] +} + +func (isaac *ISAAC) isaac() { + var a, b, x, y uint64 + a = isaac.aa + b = isaac.bb + isaac.cc + 1 + isaac.cc++ + + m := isaac.mm[:] + r := isaac.randrsl[:] + + var i, m2Index int + + // First half + for i = 0; i < RANDSIZ/2; i++ { + m2Index = i + RANDSIZ/2 + switch i % 4 { + case 0: + a = ^(a ^ (a << 21)) + m[m2Index] + case 1: + a = (a ^ (a >> 5)) + m[m2Index] + case 2: + a = (a ^ (a << 12)) + m[m2Index] + case 3: + a = (a ^ (a >> 33)) + m[m2Index] + } + x = m[i] + y = ind(m, x) + a + b + m[i] = y + b = ind(m, y>>RANDSIZL) + x + r[i] = b + } + + // Second half + for i = RANDSIZ / 2; i < RANDSIZ; i++ { + m2Index = i - RANDSIZ/2 + switch i % 4 { + case 0: + a = ^(a ^ (a << 21)) + m[m2Index] + case 1: + a = (a ^ (a >> 5)) + m[m2Index] + case 2: + a = (a ^ (a << 12)) + m[m2Index] + case 3: + a = (a ^ (a >> 33)) + m[m2Index] + } + x = m[i] + y = ind(m, x) + a + b + m[i] = y + b = ind(m, y>>RANDSIZL) + x + r[i] = b + } + + isaac.bb = b + isaac.aa = a +} + +// Return a 64 bit random integer. +func (isaac *ISAAC) Uint64() uint64 { + if isaac.randcnt == 0 { + isaac.isaac() + isaac.randcnt = RANDSIZ + } + isaac.randcnt-- + return isaac.randrsl[isaac.randcnt] +} + +var gencycle int = 0 +var bufferFor32 uint64 = uint64(0) + +// Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result. +func (isaac *ISAAC) Uint32() uint32 { + if gencycle == 0 { + bufferFor32 = isaac.Uint64() + gencycle = 1 + return uint32(bufferFor32 >> 32) + } + + gencycle = 0 + return uint32(bufferFor32 & 0xffffffff) +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkISAAC()' isaac64.gno +func benchmarkISAAC(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New() + + for i := 0; i < iterations; i++ { + _ = isaac.Uint64() + } + ufmt.Println(ufmt.Sprintf("ISAAC: generated %d uint64\n", iterations)) +} + +// The averageISAAC() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the ISAAC PRNG. +func averageISAAC(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New(987654321987654321, 123456789987654321, 1, 997755331886644220) + + var average float64 = 0 + var squares []uint64 = make([]uint64, iterations) + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("ISAAC average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("ISAAC standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("ISAAC theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno new file mode 100644 index 00000000000..239e7f818fb --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno @@ -0,0 +1,165 @@ +package isaac64 + +import ( + "math/rand" + "testing" +) + +type OpenISAAC struct { + Randrsl [256]uint64 + Randcnt uint64 + Mm [256]uint64 + Aa, Bb, Cc uint64 + Seed [256]uint64 +} + +func TestISAACSeeding(t *testing.T) { + isaac := New() +} + +func TestISAACRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.9273376778618531, + 0.327620245173309, + 0.49315436150113456, + 0.9222536383598948, + 0.2999297342641162, + 0.4050531597269049, + 0.5321357451089953, + 0.19478000239059667, + 0.5156043950865713, + 0.9233494881511063, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestISAACUint64(t *testing.T) { + isaac := New() + + expected := []uint64{ + 6781932227698873623, + 14800945299485332986, + 4114322996297394168, + 5328012296808356526, + 12789214124608876433, + 17611101631239575547, + 6877490613942924608, + 15954522518901325556, + 14180160756719376887, + 4977949063252893357, + } + + for i, exp := range expected { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func dupState(i *ISAAC) *OpenISAAC { + state := &OpenISAAC{} + state.Seed = i.seed + state.Randrsl = i.randrsl + state.Mm = i.mm + state.Aa = i.aa + state.Bb = i.bb + state.Cc = i.cc + state.Randcnt = i.randcnt + + return state +} + +func TestISAACMarshalUnmarshal(t *testing.T) { + isaac := New() + + expected1 := []uint64{ + 6781932227698873623, + 14800945299485332986, + 4114322996297394168, + 5328012296808356526, + 12789214124608876433, + } + + expected2 := []uint64{ + 17611101631239575547, + 6877490613942924608, + 15954522518901325556, + 14180160756719376887, + 4977949063252893357, + } + + for i, exp := range expected1 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := isaac.MarshalBinary() + + t.Logf("State: [%v]\n", dupState(isaac)) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := dupState(isaac) + + if err != nil { + t.Errorf("ISAAC.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + isaac.Uint64() + + for i, exp := range expected2 { + val := isaac.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%v]\n", dupState(isaac)) + + // Now restore the state of the PRNG + err = isaac.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%v]\n", dupState(isaac)) + + if state_before.Seed != dupState(isaac).Seed { + t.Errorf("Seed mismatch") + } + if state_before.Randrsl != dupState(isaac).Randrsl { + t.Errorf("Randrsl mismatch") + } + if state_before.Mm != dupState(isaac).Mm { + t.Errorf("Mm mismatch") + } + if state_before.Aa != dupState(isaac).Aa { + t.Errorf("Aa mismatch") + } + if state_before.Bb != dupState(isaac).Bb { + t.Errorf("Bb mismatch") + } + if state_before.Cc != dupState(isaac).Cc { + t.Errorf("Cc mismatch") + } + if state_before.Randcnt != dupState(isaac).Randcnt { + t.Errorf("Randcnt mismatch") + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD b/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD new file mode 100644 index 00000000000..00ed4412db0 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD @@ -0,0 +1,69 @@ +# package xorshift64star // import "gno.land/p/demo/math/rand/xorshift64star" + +Xorshift64* is a very fast psuedo-random number generation algorithm with strong +statistical properties. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the Xorshift64* PRNG algorithm. +This algorithm provides strong statistical performance with most seeds (just +don't seed it with zero), and the performance of this implementation in Gno is +more than four times faster than the default PCG implementation in `math/rand`. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +Xorshift64*: 1000000 Uint64 generated in 3.77s +Ratio: x4.11 times faster than PCG +``` + +Use it directly: + +``` +prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = xorshift64star.New() +prng := rand.New(source) +``` + +## TYPES + +``` +type Xorshift64Star struct { + // Has unexported fields. +} +``` + +Xorshift64Star is a PRNG that implements the Xorshift64* algorithm. + +`func New(seed ...uint64) *Xorshift64Star` + New() creates a new instance of the PRNG with a given seed, which should + be a uint64. If no seed is provided, the PRNG will be seeded via the + gno.land/p/demo/entropy package. + +`func (xs *Xorshift64Star) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (xs *Xorshift64Star) Seed(seed ...uint64)` + Seed() implements the rand.Source interface. It provides a way to set the + seed for the PRNG. + +`func (xs *Xorshift64Star) Uint64() uint64` + Uint64() generates the next random uint64 value. + +`func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod new file mode 100644 index 00000000000..bc40b1bc71b --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/wyhaines/rand/xorshift64star + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno new file mode 100644 index 00000000000..4934fe3a878 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno @@ -0,0 +1,172 @@ +// Xorshift64* is a very fast psuedo-random number generation algorithm with strong +// statistical properties. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the Xorshift64* PRNG algorithm. This algorithm provides +// strong statistical performance with most seeds (just don't seed it with zero), and the performance +// of this implementation in Gno is more than four times faster than the default PCG implementation in +// `math/rand`. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// Xorshift64*: 1000000 Uint64 generated in 3.77s +// Ratio: x4.11 times faster than PCG +// +// Use it directly: +// +// prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = xorshift64star.New() +// prng := rand.New(source) +package xorshift64star + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" +) + +// Xorshift64Star is a PRNG that implements the Xorshift64* algorithm. +type Xorshift64Star struct { + seed uint64 +} + +// New() creates a new instance of the PRNG with a given seed, which +// should be a uint64. If no seed is provided, the PRNG will be seeded via the +// gno.land/p/demo/entropy package. +func New(seed ...uint64) *Xorshift64Star { + xs := &Xorshift64Star{} + xs.Seed(seed...) + return xs +} + +// Seed() implements the rand.Source interface. It provides a way to set the seed for the PRNG. +func (xs *Xorshift64Star) Seed(seed ...uint64) { + if len(seed) == 0 { + e := entropy.New() + xs.seed = e.Value64() + } else { + xs.seed = seed[0] + } +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +// binary.bigEndian.Uint64, copied to avoid dependency +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint64, copied to avoid dependency +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalXorshift64StarLabel = []byte("xorshift64*:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (xs *Xorshift64Star) MarshalBinary() ([]byte, error) { + b := make([]byte, 20) + copy(b, marshalXorshift64StarLabel) + bePutUint64(b[12:], xs.seed) + return b, nil +} + +// errUnmarshalXorshift64Star is returned when unmarshalling fails. +var errUnmarshalXorshift64Star = errors.New("invalid Xorshift64* encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error { + if len(data) != 20 || string(data[:12]) != string(marshalXorshift64StarLabel) { + return errUnmarshalXorshift64Star + } + xs.seed = beUint64(data[12:]) + return nil +} + +// Uint64() generates the next random uint64 value. +func (xs *Xorshift64Star) Uint64() uint64 { + xs.seed ^= xs.seed >> 12 + xs.seed ^= xs.seed << 25 + xs.seed ^= xs.seed >> 27 + xs.seed *= 2685821657736338717 + return xs.seed // Operations naturally wrap around in uint64 +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkXorshift64Star()' xorshift64star.gno +func benchmarkXorshift64Star(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs64s := New() + + for i := 0; i < iterations; i++ { + _ = xs64s.Uint64() + } + ufmt.Println(ufmt.Sprintf("Xorshift64*: generate %d uint64\n", iterations)) +} + +// The averageXorshift64Star() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the Xorshift64* PRNG. +func averageXorshift64Star(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs64s := New() + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := xs64s.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("Xorshift64* average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("Xorshift64* standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("Xorshift64* theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno new file mode 100644 index 00000000000..8a73bd9718d --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno @@ -0,0 +1,134 @@ +package xorshift64star + +import ( + "math/rand" + "testing" +) + +func TestXorshift64StarSeeding(t *testing.T) { + xs64s := New() + value1 := xs64s.Uint64() + + xs64s = New(987654321) + value2 := xs64s.Uint64() + + if value1 != 5083824587905981259 || value2 != 18211065302896784785 || value1 == value2 { + t.Errorf("Expected 5083824587905981259 to be != to 18211065302896784785; got: %d == %d", value1, value2) + } +} + +func TestXorshift64StarRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + .8344002228310946, + 0.01777174153236205, + 0.23521769507865276, + 0.5387610198576143, + 0.631539862225968, + 0.9369068148346704, + 0.6387002315083188, + 0.5047507613688854, + 0.5208486273732391, + 0.25023746271541747, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestXorshift64StarUint64(t *testing.T) { + xs64s := New() + + expected := []uint64{ + 5083824587905981259, + 4607286371009545754, + 2070557085263023674, + 14094662988579565368, + 2910745910478213381, + 18037409026311016155, + 17169624916429864153, + 10459214929523155306, + 11840179828060641081, + 1198750959721587199, + } + + for i, exp := range expected { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func TestXorshift64StarMarshalUnmarshal(t *testing.T) { + xs64s := New() + + expected1 := []uint64{ + 5083824587905981259, + 4607286371009545754, + 2070557085263023674, + 14094662988579565368, + 2910745910478213381, + } + + expected2 := []uint64{ + 18037409026311016155, + 17169624916429864153, + 10459214929523155306, + 11840179828060641081, + 1198750959721587199, + } + + for i, exp := range expected1 { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := xs64s.MarshalBinary() + + t.Logf("Original State: [%x]\n", xs64s.seed) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := xs64s.seed + + if err != nil { + t.Errorf("Xorshift64Star.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + xs64s.Uint64() + + for i, exp := range expected2 { + val := xs64s.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%x]\n", xs64s.seed) + + // Now restore the state of the PRNG + err = xs64s.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%x]\n", xs64s.seed) + + if state_before != xs64s.seed { + t.Errorf("States before and after marshal/unmarshal are not equal; go %x and %x", state_before, xs64s.seed) + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD new file mode 100644 index 00000000000..444d1e1cdd9 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD @@ -0,0 +1,60 @@ +# package xorshiftr128plus // import "gno.land/p/demo/math/rand/xorshiftr128plus" + +Xorshiftr128+ is a very fast psuedo-random number generation algorithm with +strong statistical properties. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the Xorshiftr128+ PRNG algorithm. +This algorithm provides strong statistical performance with most seeds (just +don't seed it with zeros), and the performance of this implementation in Gno is +more than four times faster than the default PCG implementation in `math/rand`. + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.48s +Xorshiftr128+: 1000000 Uint64 generated in 3.22s +Ratio: x4.81 times faster than PCG +``` + +Use it directly: + +``` +prng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = xorshiftr128plus.New() +prng := rand.New(source) +``` + +## TYPES + +``` +type Xorshiftr128Plus struct { + // Has unexported fields. +} +``` + +`func New(seeds ...uint64) *Xorshiftr128Plus` + +`func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (x *Xorshiftr128Plus) Seed(s1, s2 uint64)` + +`func (x *Xorshiftr128Plus) Uint64() uint64` + +`func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod new file mode 100644 index 00000000000..c778fc72550 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/wyhaines/rand/xorshiftr128plus + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno new file mode 100644 index 00000000000..d950ab5108a --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno @@ -0,0 +1,186 @@ +// Xorshiftr128+ is a very fast psuedo-random number generation algorithm with strong +// statistical properties. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the Xorshiftr128+ PRNG algorithm. This algorithm provides +// strong statistical performance with most seeds (just don't seed it with zeros), and the performance +// of this implementation in Gno is more than four times faster than the default PCG implementation in +// `math/rand`. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.48s +// Xorshiftr128+: 1000000 Uint64 generated in 3.22s +// Ratio: x4.81 times faster than PCG +// +// Use it directly: +// +// prng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = xorshiftr128plus.New() +// prng := rand.New(source) +package xorshiftr128plus + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" +) + +type Xorshiftr128Plus struct { + seed [2]uint64 // Seeds +} + +func New(seeds ...uint64) *Xorshiftr128Plus { + var s1, s2 uint64 + seed_length := len(seeds) + if seed_length < 2 { + e := entropy.New() + if seed_length == 0 { + s1 = e.Value64() + s2 = e.Value64() + } else { + s1 = seeds[0] + s2 = e.Value64() + } + } else { + s1 = seeds[0] + s2 = seeds[1] + } + + prng := &Xorshiftr128Plus{} + prng.Seed(s1, s2) + return prng +} + +func (x *Xorshiftr128Plus) Seed(s1, s2 uint64) { + if s1 == 0 && s2 == 0 { + panic("Seeds must not both be zero") + } + x.seed[0] = s1 + x.seed[1] = s2 +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +// binary.bigEndian.Uint64, copied to avoid dependency +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint64, copied to avoid dependency +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalXorshiftr128PlusLabel = []byte("xorshiftr128+:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error) { + b := make([]byte, 30) + copy(b, marshalXorshiftr128PlusLabel) + bePutUint64(b[14:], xs.seed[0]) + bePutUint64(b[22:], xs.seed[1]) + return b, nil +} + +// errUnmarshalXorshiftr128Plus is returned when unmarshalling fails. +var errUnmarshalXorshiftr128Plus = errors.New("invalid Xorshiftr128Plus encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error { + if len(data) != 30 || string(data[:14]) != string(marshalXorshiftr128PlusLabel) { + return errUnmarshalXorshiftr128Plus + } + xs.seed[0] = beUint64(data[14:]) + xs.seed[1] = beUint64(data[22:]) + return nil +} + +func (x *Xorshiftr128Plus) Uint64() uint64 { + x0 := x.seed[0] + x1 := x.seed[1] + x.seed[0] = x1 + x0 ^= x0 << 23 + x0 ^= x0 >> 17 + x0 ^= x1 + x.seed[1] = x0 + x1 + return x.seed[1] +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkXorshiftr128Plus()' xorshiftr128plus.gno +func benchmarkXorshiftr128Plus(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs128p := New() + + for i := 0; i < iterations; i++ { + _ = xs128p.Uint64() + } + ufmt.Println(ufmt.Sprintf("Xorshiftr128Plus: generate %d uint64\n", iterations)) +} + +// The averageXorshiftr128Plus() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the Xorshiftr128+ PRNG. +func averageXorshiftr128Plus(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs128p := New() + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := xs128p.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno new file mode 100644 index 00000000000..c5d86edd073 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno @@ -0,0 +1,142 @@ +package xorshiftr128plus + +import ( + "math/rand" + "testing" +) + +func TestXorshift64StarSeeding(t *testing.T) { + xs128p := New() + value1 := xs128p.Uint64() + + xs128p = New(987654321) + value2 := xs128p.Uint64() + + xs128p = New(987654321, 9876543210) + value3 := xs128p.Uint64() + + if value1 != 13970141264473760763 || + value2 != 17031892808144362974 || + value3 != 8285073084540510 || + value1 == value2 || + value2 == value3 || + value1 == value3 { + t.Errorf("Expected three different values: 13970141264473760763, 17031892808144362974, and 8285073084540510\n got: %d, %d, %d", value1, value2, value3) + } +} + +func TestXorshiftr128PlusRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.9199548549485674, + 0.0027491282372705816, + 0.31493362274701164, + 0.3531250819119609, + 0.09957852858060356, + 0.731941362705936, + 0.3476937688876708, + 0.1444018086140385, + 0.9106467321832331, + 0.8024870151488901, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestXorshiftr128PlusUint64(t *testing.T) { + xs128p := New(987654321, 9876543210) + + expected := []uint64{ + 8285073084540510, + 97010855169053386, + 11353359435625603792, + 10289232744262291728, + 14019961444418950453, + 15829492476941720545, + 2764732928842099222, + 6871047144273883379, + 16142204260470661970, + 11803223757041229095, + } + + for i, exp := range expected { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func TestXorshiftr128PlusMarshalUnmarshal(t *testing.T) { + xs128p := New(987654321, 9876543210) + + expected1 := []uint64{ + 8285073084540510, + 97010855169053386, + 11353359435625603792, + 10289232744262291728, + 14019961444418950453, + } + + expected2 := []uint64{ + 15829492476941720545, + 2764732928842099222, + 6871047144273883379, + 16142204260470661970, + 11803223757041229095, + } + + for i, exp := range expected1 { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := xs128p.MarshalBinary() + + t.Logf("Original State: [%x]\n", xs128p.seed) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := xs128p.seed + + if err != nil { + t.Errorf("Xorshiftr128Plus.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + xs128p.Uint64() + + for i, exp := range expected2 { + val := xs128p.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%x]\n", xs128p.seed) + + // Now restore the state of the PRNG + err = xs128p.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%x]\n", xs128p.seed) + + if state_before != xs128p.seed { + t.Errorf("States before and after marshal/unmarshal are not equal; go %x and %x", state_before, xs128p.seed) + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} From 7ce29ff1512353d02a3dcd44dd2fd98a4c6ee869 Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Thu, 5 Dec 2024 11:13:06 +0100 Subject: [PATCH 289/345] feat: A fully featured btree implementation (#3126) This is a fully featured btree implementation. A friend gave me some incomplete (and broken) btree code for Go, and when I started reworking it, I discovered that it was a broken semi-copy of an old version of Google's btree for Go. I finished reworking it so that it adhere's to that original Google version's API, though there are some differences internally in places, and I think that my version is much easier to follow and to understand. This implementation is quite a bit faster than the AVL tree. I will add links to some benchmarks that I did in a comment. This implementation supports copy-on-write for the trees, for inexpensively creating copies of a tree that are effectively isolated from each other with respect to changes that happen after the fork.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- examples/gno.land/p/demo/btree/btree.gno | 1114 +++++++++++++++++ examples/gno.land/p/demo/btree/btree_test.gno | 678 ++++++++++ examples/gno.land/p/demo/btree/gno.mod | 1 + 3 files changed, 1793 insertions(+) create mode 100644 examples/gno.land/p/demo/btree/btree.gno create mode 100644 examples/gno.land/p/demo/btree/btree_test.gno create mode 100644 examples/gno.land/p/demo/btree/gno.mod diff --git a/examples/gno.land/p/demo/btree/btree.gno b/examples/gno.land/p/demo/btree/btree.gno new file mode 100644 index 00000000000..f909ec6bc91 --- /dev/null +++ b/examples/gno.land/p/demo/btree/btree.gno @@ -0,0 +1,1114 @@ +////////// +// +// Copyright 2014 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// Copyright 2024 New Tendermint +// +// This Gno port of the original Go BTree is substantially rewritten/reimplemented +// from the original, primarily for clarity of code, clarity of documentation, +// and for compatibility with Gno. +// +// Authors: +// Original version authors -- https://github.com/google/btree/graphs/contributors +// Kirk Haines +// +////////// + +// Package btree implements in-memory B-Trees of arbitrary degree. +// +// It has a flatter structure than an equivalent red-black or other binary tree, +// which may yield better memory usage and/or performance. +package btree + +import "sort" + +////////// +// +// Types +// +////////// + +// BTreeOption is a function interface for setting options on a btree with `New()`. +type BTreeOption func(*BTree) + +// BTree is an implementation of a B-Tree. +// +// BTree stores Record instances in an ordered structure, allowing easy insertion, +// removal, and iteration. +type BTree struct { + degree int + length int + root *node + cowCtx *copyOnWriteContext +} + +// Any type that implements this interface can be stored in the BTree. This allows considerable +// +// flexiblity in storage within the BTree. +type Record interface { + // Less compares self to `than`, returning true if self is less than `than` + Less(than Record) bool +} + +// records is the storage within a node. It is expressed as a slice of Record, where a Record +// is any struct that implements the Record interface. +type records []Record + +// node is an internal node in a tree. +// +// It must at all times maintain on of the two conditions: +// - len(children) == 0, len(records) unconstrained +// - len(children) == len(records) + 1 +type node struct { + records records + children children + cowCtx *copyOnWriteContext +} + +// children is the list of child nodes below the current node. It is a slice of nodes. +type children []*node + +// FreeNodeList represents a slice of nodes which are available for reuse. The default +// behavior of New() is for each BTree instance to have its own FreeNodeList. However, +// it is possible for multiple instances of BTree to share the same tree. If one uses +// New(WithFreeNodeList()) to create a tree, one may pass an existing FreeNodeList, allowing +// multiple trees to use a single list. In an application with multiple trees, it might +// be more efficient to allocate a single FreeNodeList with a significant initial capacity, +// and then have all of the trees use that same large FreeNodeList. +type FreeNodeList struct { + nodes []*node +} + +// copyOnWriteContext manages node ownership and ensures that cloned trees +// maintain isolation from each other when a node is changed. +// +// Ownership Rules: +// - Each node is associated with a specific copyOnWriteContext. +// - A tree can modify a node directly only if the tree's context matches the node's context. +// - If a tree attempts to modify a node with a different context, it must create a +// new, writable copy of that node (i.e., perform a clone) before making changes. +// +// Write Operation Invariant: +// - During any write operation, the current node being modified must have the same +// context as the tree requesting the write. +// - To maintain this invariant, before descending into a child node, the system checks +// if the child’s context matches the tree's context. +// - If the contexts match, the node can be modified in place. +// - If the contexts do not match, a mutable copy of the child node is created with the +// correct context before proceeding. +// +// Practical Implications: +// - The node currently being modified inherits the requesting tree's context, allowing +// in-place modifications. +// - Child nodes may initially have different contexts. Before any modification, these +// children are copied to ensure they share the correct context, enabling safe and +// isolated updates without affecting other trees that might be referencing the original nodes. +// +// Example Usage: +// When a tree performs a write operation (e.g., inserting or deleting a node), it uses +// its copyOnWriteContext to determine whether it can modify nodes directly or needs to +// create copies. This mechanism ensures that trees can share nodes efficiently while +// maintaining data integrity. +type copyOnWriteContext struct { + nodes *FreeNodeList +} + +// Record implements an interface with a single function, Less. Any type that implements +// RecordIterator allows callers of all of the iteration functions for the BTree +// to evaluate an element of the tree as it is traversed. The function will receive +// a stored element from the tree. The function must return either a true or a false value. +// True indicates that iteration should continue, while false indicates that it should halt. +type RecordIterator func(i Record) bool + +////////// +// +// Functions +// +////////// + +// NewFreeNodeList creates a new free list. +// size is the maximum size of the returned free list. +func NewFreeNodeList(size int) *FreeNodeList { + return &FreeNodeList{nodes: make([]*node, 0, size)} +} + +func (freeList *FreeNodeList) newNode() (nodeInstance *node) { + index := len(freeList.nodes) - 1 + if index < 0 { + return new(node) + } + nodeInstance = freeList.nodes[index] + freeList.nodes[index] = nil + freeList.nodes = freeList.nodes[:index] + + return nodeInstance +} + +// freeNode adds the given node to the list, returning true if it was added +// and false if it was discarded. + +func (freeList *FreeNodeList) freeNode(nodeInstance *node) (nodeWasAdded bool) { + if len(freeList.nodes) < cap(freeList.nodes) { + freeList.nodes = append(freeList.nodes, nodeInstance) + nodeWasAdded = true + } + return +} + +// A default size for the free node list. We might want to run some benchmarks to see if +// there are any pros or cons to this size versus other sizes. This seems to be a reasonable +// compromise to reduce GC pressure by reusing nodes where possible, without stacking up too +// much baggage in a given tree. +const DefaultFreeNodeListSize = 32 + +// WithDegree sets the degree of the B-Tree. +func WithDegree(degree int) BTreeOption { + return func(bt *BTree) { + if degree <= 1 { + panic("Degrees less than 1 do not make any sense for a BTree. Please provide a degree of 1 or greater.") + } + bt.degree = degree + } +} + +// WithFreeNodeList sets a custom free node list for the B-Tree. +func WithFreeNodeList(freeList *FreeNodeList) BTreeOption { + return func(bt *BTree) { + bt.cowCtx = ©OnWriteContext{nodes: freeList} + } +} + +// New creates a new B-Tree with optional configurations. If configuration is not provided, +// it will default to 16 element nodes. Degree may not be less than 1 (which effectively +// makes the tree into a binary tree). +// +// `New(WithDegree(2))`, for example, will create a 2-3-4 tree (each node contains 1-3 records +// and 2-4 children). +// +// `New(WithFreeNodeList(NewFreeNodeList(64)))` will create a tree with a degree of 16, and +// with a free node list with a size of 64. +func New(options ...BTreeOption) *BTree { + btree := &BTree{ + degree: 16, // default degree + cowCtx: ©OnWriteContext{nodes: NewFreeNodeList(DefaultFreeNodeListSize)}, + } + for _, opt := range options { + opt(btree) + } + return btree +} + +// insertAt inserts a value into the given index, pushing all subsequent values +// forward. +func (recordsSlice *records) insertAt(index int, newRecord Record) { + originalLength := len(*recordsSlice) + + // Extend the slice by one element + *recordsSlice = append(*recordsSlice, nil) + + // Move elements from the end to avoid overwriting during the copy + // TODO: Make this work with slice appends, instead. It should be faster? + if index < originalLength { + for position := originalLength; position > index; position-- { + (*recordsSlice)[position] = (*recordsSlice)[position-1] + } + } + + // Insert the new record + (*recordsSlice)[index] = newRecord +} + +// removeAt removes a Record from the records slice at the specified index. +// It shifts subsequent records to fill the gap and returns the removed Record. +func (recordSlicePointer *records) removeAt(index int) Record { + recordSlice := *recordSlicePointer + removedRecord := recordSlice[index] + copy(recordSlice[index:], recordSlice[index+1:]) + recordSlice[len(recordSlice)-1] = nil + *recordSlicePointer = recordSlice[:len(recordSlice)-1] + + return removedRecord +} + +// Pop removes and returns the last Record from the records slice. +// It also clears the reference to the removed Record to aid garbage collection. +func (r *records) pop() Record { + recordSlice := *r + lastIndex := len(recordSlice) - 1 + removedRecord := recordSlice[lastIndex] + recordSlice[lastIndex] = nil + *r = recordSlice[:lastIndex] + return removedRecord +} + +// This slice is intended only as a supply of records for the truncate function +// that follows, and it should not be changed or altered. +var emptyRecords = make(records, 32) + +// truncate reduces the length of the slice to the specified index, +// and clears the elements beyond that index to prevent memory leaks. +// The index must be less than or equal to the current length of the slice. +func (originalSlice *records) truncate(index int) { + // Split the slice into the part to keep and the part to clear. + recordsToKeep := (*originalSlice)[:index] + recordsToClear := (*originalSlice)[index:] + + // Update the original slice to only contain the records to keep. + *originalSlice = recordsToKeep + + // Clear the memory of the part that was truncated. + for len(recordsToClear) > 0 { + // Copy empty values from `emptyRecords` to the recordsToClear slice. + // This effectively "clears" the memory by overwriting elements. + numCleared := copy(recordsToClear, emptyRecords) + recordsToClear = recordsToClear[numCleared:] + } +} + +// Find determines the appropriate index at which a given Record should be inserted +// into the sorted records slice. If the Record already exists in the slice, +// the method returns its index and sets found to true. +// +// Parameters: +// - record: The Record to search for within the records slice. +// +// Returns: +// - insertIndex: The index at which the Record should be inserted. +// - found: A boolean indicating whether the Record already exists in the slice. +func (recordsSlice records) find(record Record) (insertIndex int, found bool) { + totalRecords := len(recordsSlice) + + // Perform a binary search to find the insertion point for the record + insertionPoint := sort.Search(totalRecords, func(currentIndex int) bool { + return record.Less(recordsSlice[currentIndex]) + }) + + if insertionPoint > 0 { + previousRecord := recordsSlice[insertionPoint-1] + + if !previousRecord.Less(record) { + return insertionPoint - 1, true + } + } + + return insertionPoint, false +} + +// insertAt inserts a value into the given index, pushing all subsequent values +// forward. +func (childSlice *children) insertAt(index int, n *node) { + originalLength := len(*childSlice) + + // Extend the slice by one element + *childSlice = append(*childSlice, nil) + + // Move elements from the end to avoid overwriting during the copy + if index < originalLength { + for i := originalLength; i > index; i-- { + (*childSlice)[i] = (*childSlice)[i-1] + } + } + + // Insert the new record + (*childSlice)[index] = n +} + +// removeAt removes a Record from the records slice at the specified index. +// It shifts subsequent records to fill the gap and returns the removed Record. +func (childSlicePointer *children) removeAt(index int) *node { + childSlice := *childSlicePointer + removedChild := childSlice[index] + copy(childSlice[index:], childSlice[index+1:]) + childSlice[len(childSlice)-1] = nil + *childSlicePointer = childSlice[:len(childSlice)-1] + + return removedChild +} + +// Pop removes and returns the last Record from the records slice. +// It also clears the reference to the removed Record to aid garbage collection. +func (childSlicePointer *children) pop() *node { + childSlice := *childSlicePointer + lastIndex := len(childSlice) - 1 + removedChild := childSlice[lastIndex] + childSlice[lastIndex] = nil + *childSlicePointer = childSlice[:lastIndex] + return removedChild +} + +// This slice is intended only as a supply of records for the truncate function +// that follows, and it should not be changed or altered. +var emptyChildren = make(children, 32) + +// truncate reduces the length of the slice to the specified index, +// and clears the elements beyond that index to prevent memory leaks. +// The index must be less than or equal to the current length of the slice. +func (originalSlice *children) truncate(index int) { + // Split the slice into the part to keep and the part to clear. + childrenToKeep := (*originalSlice)[:index] + childrenToClear := (*originalSlice)[index:] + + // Update the original slice to only contain the records to keep. + *originalSlice = childrenToKeep + + // Clear the memory of the part that was truncated. + for len(childrenToClear) > 0 { + // Copy empty values from `emptyChildren` to the recordsToClear slice. + // This effectively "clears" the memory by overwriting elements. + numCleared := copy(childrenToClear, emptyChildren) + + // Slice recordsToClear to exclude the elements that were just cleared. + childrenToClear = childrenToClear[numCleared:] + } +} + +// mutableFor creates a mutable copy of the node if the current node does not +// already belong to the provided copy-on-write context (COW). If the node is +// already associated with the given COW context, it returns the current node. +// +// Parameters: +// - cowCtx: The copy-on-write context that should own the returned node. +// +// Returns: +// - A pointer to the mutable node associated with the given COW context. +// +// If the current node belongs to a different COW context, this function: +// - Allocates a new node using the provided context. +// - Copies the node’s records and children slices into the newly allocated node. +// - Returns the new node which is now owned by the given COW context. +func (n *node) mutableFor(cowCtx *copyOnWriteContext) *node { + // If the current node is already owned by the provided context, return it as-is. + if n.cowCtx == cowCtx { + return n + } + + // Create a new node in the provided context. + newNode := cowCtx.newNode() + + // Copy the records from the current node into the new node. + newNode.records = append(newNode.records[:0], n.records...) + + // Copy the children from the current node into the new node. + newNode.children = append(newNode.children[:0], n.children...) + + return newNode +} + +// mutableChild ensures that the child node at the given index is mutable and +// associated with the same COW context as the parent node. If the child node +// belongs to a different context, a copy of the child is created and stored in the +// parent node. +// +// Parameters: +// - i: The index of the child node to be made mutable. +// +// Returns: +// - A pointer to the mutable child node. +func (n *node) mutableChild(i int) *node { + // Ensure that the child at index `i` is mutable and belongs to the same context as the parent. + mutableChildNode := n.children[i].mutableFor(n.cowCtx) + // Update the child node reference in the current node to the mutable version. + n.children[i] = mutableChildNode + return mutableChildNode +} + +// split splits the given node at the given index. The current node shrinks, +// and this function returns the record that existed at that index and a new node +// containing all records/children after it. +func (n *node) split(i int) (Record, *node) { + record := n.records[i] + next := n.cowCtx.newNode() + next.records = append(next.records, n.records[i+1:]...) + n.records.truncate(i) + if len(n.children) > 0 { + next.children = append(next.children, n.children[i+1:]...) + n.children.truncate(i + 1) + } + return record, next +} + +// maybeSplitChild checks if a child should be split, and if so splits it. +// Returns whether or not a split occurred. +func (n *node) maybeSplitChild(i, maxRecords int) bool { + if len(n.children[i].records) < maxRecords { + return false + } + first := n.mutableChild(i) + record, second := first.split(maxRecords / 2) + n.records.insertAt(i, record) + n.children.insertAt(i+1, second) + return true +} + +// insert adds a record to the subtree rooted at the current node, ensuring that no node in the subtree +// exceeds the maximum number of allowed records (`maxRecords`). If an equivalent record is already present, +// it replaces the existing one and returns it; otherwise, it returns nil. +// +// Parameters: +// - record: The record to be inserted. +// - maxRecords: The maximum number of records allowed per node. +// +// Returns: +// - The record that was replaced if an equivalent record already existed, otherwise nil. +func (n *node) insert(record Record, maxRecords int) Record { + // Find the position where the new record should be inserted and check if an equivalent record already exists. + insertionIndex, recordExists := n.records.find(record) + + if recordExists { + // If an equivalent record is found, replace it and return the old record. + existingRecord := n.records[insertionIndex] + n.records[insertionIndex] = record + return existingRecord + } + + // If the current node is a leaf (has no children), insert the new record at the calculated index. + if len(n.children) == 0 { + n.records.insertAt(insertionIndex, record) + return nil + } + + // Check if the child node at the insertion index needs to be split due to exceeding maxRecords. + if n.maybeSplitChild(insertionIndex, maxRecords) { + // If a split occurred, compare the new record with the record moved up to the current node. + splitRecord := n.records[insertionIndex] + switch { + case record.Less(splitRecord): + // The new record belongs to the first (left) split node; no change to insertion index. + case splitRecord.Less(record): + // The new record belongs to the second (right) split node; move the insertion index to the next position. + insertionIndex++ + default: + // If the record is equivalent to the split record, replace it and return the old record. + existingRecord := n.records[insertionIndex] + n.records[insertionIndex] = record + return existingRecord + } + } + + // Recursively insert the record into the appropriate child node, now guaranteed to have space. + return n.mutableChild(insertionIndex).insert(record, maxRecords) +} + +// get finds the given key in the subtree and returns it. +func (n *node) get(key Record) Record { + i, found := n.records.find(key) + if found { + return n.records[i] + } else if len(n.children) > 0 { + return n.children[i].get(key) + } + return nil +} + +// min returns the first record in the subtree. +func min(n *node) Record { + if n == nil { + return nil + } + for len(n.children) > 0 { + n = n.children[0] + } + if len(n.records) == 0 { + return nil + } + return n.records[0] +} + +// max returns the last record in the subtree. +func max(n *node) Record { + if n == nil { + return nil + } + for len(n.children) > 0 { + n = n.children[len(n.children)-1] + } + if len(n.records) == 0 { + return nil + } + return n.records[len(n.records)-1] +} + +// toRemove details what record to remove in a node.remove call. +type toRemove int + +const ( + removeRecord toRemove = iota // removes the given record + removeMin // removes smallest record in the subtree + removeMax // removes largest record in the subtree +) + +// remove removes a record from the subtree rooted at the current node. +// +// Parameters: +// - record: The record to be removed (can be nil when the removal type indicates min or max). +// - minRecords: The minimum number of records a node should have after removal. +// - typ: The type of removal operation to perform (removeMin, removeMax, or removeRecord). +// +// Returns: +// - The record that was removed, or nil if no such record was found. +func (n *node) remove(record Record, minRecords int, removalType toRemove) Record { + var targetIndex int + var recordFound bool + + // Determine the index of the record to remove based on the removal type. + switch removalType { + case removeMax: + // If this node is a leaf, remove and return the last record. + if len(n.children) == 0 { + return n.records.pop() + } + targetIndex = len(n.records) // The last record index for removing max. + + case removeMin: + // If this node is a leaf, remove and return the first record. + if len(n.children) == 0 { + return n.records.removeAt(0) + } + targetIndex = 0 // The first record index for removing min. + + case removeRecord: + // Locate the index of the record to be removed. + targetIndex, recordFound = n.records.find(record) + if len(n.children) == 0 { + if recordFound { + return n.records.removeAt(targetIndex) + } + return nil // The record was not found in the leaf node. + } + + default: + panic("invalid removal type") + } + + // If the current node has children, handle the removal recursively. + if len(n.children[targetIndex].records) <= minRecords { + // If the target child node has too few records, grow it before proceeding with removal. + return n.growChildAndRemove(targetIndex, record, minRecords, removalType) + } + + // Get a mutable reference to the child node at the target index. + targetChild := n.mutableChild(targetIndex) + + // If the record to be removed was found in the current node: + if recordFound { + // Replace the current record with its predecessor from the child node, and return the removed record. + replacedRecord := n.records[targetIndex] + n.records[targetIndex] = targetChild.remove(nil, minRecords, removeMax) + return replacedRecord + } + + // Recursively remove the record from the child node. + return targetChild.remove(record, minRecords, removalType) +} + +// growChildAndRemove grows child 'i' to make sure it's possible to remove an +// record from it while keeping it at minRecords, then calls remove to actually +// remove it. +// +// Most documentation says we have to do two sets of special casing: +// 1. record is in this node +// 2. record is in child +// +// In both cases, we need to handle the two subcases: +// +// A) node has enough values that it can spare one +// B) node doesn't have enough values +// +// For the latter, we have to check: +// +// a) left sibling has node to spare +// b) right sibling has node to spare +// c) we must merge +// +// To simplify our code here, we handle cases #1 and #2 the same: +// If a node doesn't have enough records, we make sure it does (using a,b,c). +// We then simply redo our remove call, and the second time (regardless of +// whether we're in case 1 or 2), we'll have enough records and can guarantee +// that we hit case A. +func (n *node) growChildAndRemove(i int, record Record, minRecords int, typ toRemove) Record { + if i > 0 && len(n.children[i-1].records) > minRecords { + // Steal from left child + child := n.mutableChild(i) + stealFrom := n.mutableChild(i - 1) + stolenRecord := stealFrom.records.pop() + child.records.insertAt(0, n.records[i-1]) + n.records[i-1] = stolenRecord + if len(stealFrom.children) > 0 { + child.children.insertAt(0, stealFrom.children.pop()) + } + } else if i < len(n.records) && len(n.children[i+1].records) > minRecords { + // steal from right child + child := n.mutableChild(i) + stealFrom := n.mutableChild(i + 1) + stolenRecord := stealFrom.records.removeAt(0) + child.records = append(child.records, n.records[i]) + n.records[i] = stolenRecord + if len(stealFrom.children) > 0 { + child.children = append(child.children, stealFrom.children.removeAt(0)) + } + } else { + if i >= len(n.records) { + i-- + } + child := n.mutableChild(i) + // merge with right child + mergeRecord := n.records.removeAt(i) + mergeChild := n.children.removeAt(i + 1).mutableFor(n.cowCtx) + child.records = append(child.records, mergeRecord) + child.records = append(child.records, mergeChild.records...) + child.children = append(child.children, mergeChild.children...) + n.cowCtx.freeNode(mergeChild) + } + return n.remove(record, minRecords, typ) +} + +type direction int + +const ( + descend = direction(-1) + ascend = direction(+1) +) + +// iterate provides a simple method for iterating over elements in the tree. +// +// When ascending, the 'start' should be less than 'stop' and when descending, +// the 'start' should be greater than 'stop'. Setting 'includeStart' to true +// will force the iterator to include the first record when it equals 'start', +// thus creating a "greaterOrEqual" or "lessThanEqual" rather than just a +// "greaterThan" or "lessThan" queries. +func (n *node) iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) { + var ok, found bool + var index int + switch dir { + case ascend: + if start != nil { + index, _ = n.records.find(start) + } + for i := index; i < len(n.records); i++ { + if len(n.children) > 0 { + if hit, ok = n.children[i].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + if !includeStart && !hit && start != nil && !start.Less(n.records[i]) { + hit = true + continue + } + hit = true + if stop != nil && !n.records[i].Less(stop) { + return hit, false + } + if !iter(n.records[i]) { + return hit, false + } + } + if len(n.children) > 0 { + if hit, ok = n.children[len(n.children)-1].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + case descend: + if start != nil { + index, found = n.records.find(start) + if !found { + index = index - 1 + } + } else { + index = len(n.records) - 1 + } + for i := index; i >= 0; i-- { + if start != nil && !n.records[i].Less(start) { + if !includeStart || hit || start.Less(n.records[i]) { + continue + } + } + if len(n.children) > 0 { + if hit, ok = n.children[i+1].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + if stop != nil && !stop.Less(n.records[i]) { + return hit, false // continue + } + hit = true + if !iter(n.records[i]) { + return hit, false + } + } + if len(n.children) > 0 { + if hit, ok = n.children[0].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + } + return hit, true +} + +func (tree *BTree) Iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) { + return tree.root.iterate(dir, start, stop, includeStart, hit, iter) +} + +// Clone creates a new BTree instance that shares the current tree's structure using a copy-on-write (COW) approach. +// +// How Cloning Works: +// - The cloned tree (`clonedTree`) shares the current tree’s nodes in a read-only state. This means that no additional memory +// is allocated for shared nodes, and read operations on the cloned tree are as fast as on the original tree. +// - When either the original tree (`t`) or the cloned tree (`clonedTree`) needs to perform a write operation (such as an insert, delete, etc.), +// a new copy of the affected nodes is created on-demand. This ensures that modifications to one tree do not affect the other. +// +// Performance Implications: +// - **Clone Creation:** The creation of a clone is inexpensive since it only involves copying references to the original tree's nodes +// and creating new copy-on-write contexts. +// - **Read Operations:** Reading from either the original tree or the cloned tree has no additional performance overhead compared to the original tree. +// - **Write Operations:** The first write operation on either tree may experience a slight slow-down due to the allocation of new nodes, +// but subsequent write operations will perform at the same speed as if the tree were not cloned. +// +// Returns: +// - A new BTree instance (`clonedTree`) that shares the original tree's structure. +func (t *BTree) Clone() *BTree { + // Create two independent copy-on-write contexts, one for the original tree (`t`) and one for the cloned tree. + originalContext := *t.cowCtx + clonedContext := *t.cowCtx + + // Create a shallow copy of the current tree, which will be the new cloned tree. + clonedTree := *t + + // Assign the new contexts to their respective trees. + t.cowCtx = &originalContext + clonedTree.cowCtx = &clonedContext + + return &clonedTree +} + +// maxRecords returns the max number of records to allow per node. +func (t *BTree) maxRecords() int { + return t.degree*2 - 1 +} + +// minRecords returns the min number of records to allow per node (ignored for the +// root node). +func (t *BTree) minRecords() int { + return t.degree - 1 +} + +func (c *copyOnWriteContext) newNode() (n *node) { + n = c.nodes.newNode() + n.cowCtx = c + return +} + +type freeType int + +const ( + ftFreelistFull freeType = iota // node was freed (available for GC, not stored in nodes) + ftStored // node was stored in the nodes for later use + ftNotOwned // node was ignored by COW, since it's owned by another one +) + +// freeNode frees a node within a given COW context, if it's owned by that +// context. It returns what happened to the node (see freeType const +// documentation). +func (c *copyOnWriteContext) freeNode(n *node) freeType { + if n.cowCtx == c { + // clear to allow GC + n.records.truncate(0) + n.children.truncate(0) + n.cowCtx = nil + if c.nodes.freeNode(n) { + return ftStored + } else { + return ftFreelistFull + } + } else { + return ftNotOwned + } +} + +// Insert adds the given record to the B-tree. If a record already exists in the tree with the same value, +// it is replaced, and the old record is returned. Otherwise, it returns nil. +// +// Notes: +// - The function panics if a nil record is provided as input. +// - If the root node is empty, a new root node is created and the record is inserted. +// +// Parameters: +// - record: The record to be inserted into the B-tree. +// +// Returns: +// - The replaced record if an equivalent record already exists, or nil if no replacement occurred. +func (t *BTree) Insert(record Record) Record { + if record == nil { + panic("nil record cannot be added to BTree") + } + + // If the tree is empty (no root), create a new root node and insert the record. + if t.root == nil { + t.root = t.cowCtx.newNode() + t.root.records = append(t.root.records, record) + t.length++ + return nil + } + + // Ensure that the root node is mutable (associated with the current tree's copy-on-write context). + t.root = t.root.mutableFor(t.cowCtx) + + // If the root node is full (contains the maximum number of records), split the root. + if len(t.root.records) >= t.maxRecords() { + // Split the root node, promoting the middle record and creating a new child node. + middleRecord, newChildNode := t.root.split(t.maxRecords() / 2) + + // Create a new root node to hold the promoted middle record. + oldRoot := t.root + t.root = t.cowCtx.newNode() + t.root.records = append(t.root.records, middleRecord) + t.root.children = append(t.root.children, oldRoot, newChildNode) + } + + // Insert the new record into the subtree rooted at the current root node. + replacedRecord := t.root.insert(record, t.maxRecords()) + + // If no record was replaced, increase the tree's length. + if replacedRecord == nil { + t.length++ + } + + return replacedRecord +} + +// Delete removes an record equal to the passed in record from the tree, returning +// it. If no such record exists, returns nil. +func (t *BTree) Delete(record Record) Record { + return t.deleteRecord(record, removeRecord) +} + +// DeleteMin removes the smallest record in the tree and returns it. +// If no such record exists, returns nil. +func (t *BTree) DeleteMin() Record { + return t.deleteRecord(nil, removeMin) +} + +// Shift is identical to DeleteMin. If the tree is thought of as an ordered list, then Shift() +// removes the element at the start of the list, the smallest element, and returns it. +func (t *BTree) Shift() Record { + return t.deleteRecord(nil, removeMin) +} + +// DeleteMax removes the largest record in the tree and returns it. +// If no such record exists, returns nil. +func (t *BTree) DeleteMax() Record { + return t.deleteRecord(nil, removeMax) +} + +// Pop is identical to DeleteMax. If the tree is thought of as an ordered list, then Shift() +// removes the element at the end of the list, the largest element, and returns it. +func (t *BTree) Pop() Record { + return t.deleteRecord(nil, removeMax) +} + +// deleteRecord removes a record from the B-tree based on the specified removal type (removeMin, removeMax, or removeRecord). +// It returns the removed record if it was found, or nil if no matching record was found. +// +// Parameters: +// - record: The record to be removed (can be nil if the removal type indicates min or max). +// - removalType: The type of removal operation to perform (removeMin, removeMax, or removeRecord). +// +// Returns: +// - The removed record if it existed in the tree, or nil if it was not found. +func (t *BTree) deleteRecord(record Record, removalType toRemove) Record { + // If the tree is empty or the root has no records, return nil. + if t.root == nil || len(t.root.records) == 0 { + return nil + } + + // Ensure the root node is mutable (associated with the tree's copy-on-write context). + t.root = t.root.mutableFor(t.cowCtx) + + // Attempt to remove the specified record from the root node. + removedRecord := t.root.remove(record, t.minRecords(), removalType) + + // Check if the root node has become empty but still has children. + // In this case, the tree height should be reduced, making the first child the new root. + if len(t.root.records) == 0 && len(t.root.children) > 0 { + oldRoot := t.root + t.root = t.root.children[0] + // Free the old root node, as it is no longer needed. + t.cowCtx.freeNode(oldRoot) + } + + // If a record was successfully removed, decrease the tree's length. + if removedRecord != nil { + t.length-- + } + + return removedRecord +} + +// AscendRange calls the iterator for every value in the tree within the range +// [greaterOrEqual, lessThan), until iterator returns false. +func (t *BTree) AscendRange(greaterOrEqual, lessThan Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, greaterOrEqual, lessThan, true, false, iterator) +} + +// AscendLessThan calls the iterator for every value in the tree within the range +// [first, pivot), until iterator returns false. +func (t *BTree) AscendLessThan(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, nil, pivot, false, false, iterator) +} + +// AscendGreaterOrEqual calls the iterator for every value in the tree within +// the range [pivot, last], until iterator returns false. +func (t *BTree) AscendGreaterOrEqual(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, pivot, nil, true, false, iterator) +} + +// Ascend calls the iterator for every value in the tree within the range +// [first, last], until iterator returns false. +func (t *BTree) Ascend(iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, nil, nil, false, false, iterator) +} + +// DescendRange calls the iterator for every value in the tree within the range +// [lessOrEqual, greaterThan), until iterator returns false. +func (t *BTree) DescendRange(lessOrEqual, greaterThan Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, lessOrEqual, greaterThan, true, false, iterator) +} + +// DescendLessOrEqual calls the iterator for every value in the tree within the range +// [pivot, first], until iterator returns false. +func (t *BTree) DescendLessOrEqual(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, pivot, nil, true, false, iterator) +} + +// DescendGreaterThan calls the iterator for every value in the tree within +// the range [last, pivot), until iterator returns false. +func (t *BTree) DescendGreaterThan(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, nil, pivot, false, false, iterator) +} + +// Descend calls the iterator for every value in the tree within the range +// [last, first], until iterator returns false. +func (t *BTree) Descend(iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, nil, nil, false, false, iterator) +} + +// Get looks for the key record in the tree, returning it. It returns nil if +// unable to find that record. +func (t *BTree) Get(key Record) Record { + if t.root == nil { + return nil + } + return t.root.get(key) +} + +// Min returns the smallest record in the tree, or nil if the tree is empty. +func (t *BTree) Min() Record { + return min(t.root) +} + +// Max returns the largest record in the tree, or nil if the tree is empty. +func (t *BTree) Max() Record { + return max(t.root) +} + +// Has returns true if the given key is in the tree. +func (t *BTree) Has(key Record) bool { + return t.Get(key) != nil +} + +// Len returns the number of records currently in the tree. +func (t *BTree) Len() int { + return t.length +} + +// Clear removes all elements from the B-tree. +// +// Parameters: +// - addNodesToFreelist: +// - If true, the tree's nodes are added to the freelist during the clearing process, +// up to the freelist's capacity. +// - If false, the root node is simply dereferenced, allowing Go's garbage collector +// to reclaim the memory. +// +// Benefits: +// - **Performance:** +// - Significantly faster than deleting each element individually, as it avoids the overhead +// of searching and updating the tree structure for each deletion. +// - More efficient than creating a new tree, since it reuses existing nodes by adding them +// to the freelist instead of discarding them to the garbage collector. +// +// Time Complexity: +// - **O(1):** +// - When `addNodesToFreelist` is false. +// - When `addNodesToFreelist` is true but the freelist is already full. +// - **O(freelist size):** +// - When adding nodes to the freelist up to its capacity. +// - **O(tree size):** +// - When iterating through all nodes to add to the freelist, but none can be added due to +// ownership by another tree. + +func (tree *BTree) Clear(addNodesToFreelist bool) { + if tree.root != nil && addNodesToFreelist { + tree.root.reset(tree.cowCtx) + } + tree.root = nil + tree.length = 0 +} + +// reset adds all nodes in the current subtree to the freelist. +// +// The function operates recursively: +// - It first attempts to reset all child nodes. +// - If the freelist becomes full at any point, the process stops immediately. +// +// Parameters: +// - copyOnWriteCtx: The copy-on-write context managing the freelist. +// +// Returns: +// - true: Indicates that the parent node should continue attempting to reset its nodes. +// - false: Indicates that the freelist is full and no further nodes should be added. +// +// Usage: +// This method is called during the `Clear` operation of the B-tree to efficiently reuse +// nodes by adding them to the freelist, thereby avoiding unnecessary allocations and reducing +// garbage collection overhead. +func (currentNode *node) reset(copyOnWriteCtx *copyOnWriteContext) bool { + // Iterate through each child node and attempt to reset it. + for _, childNode := range currentNode.children { + // If any child reset operation signals that the freelist is full, stop the process. + if !childNode.reset(copyOnWriteCtx) { + return false + } + } + + // Attempt to add the current node to the freelist. + // If the freelist is full after this operation, indicate to the parent to stop. + freelistStatus := copyOnWriteCtx.freeNode(currentNode) + return freelistStatus != ftFreelistFull +} diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno new file mode 100644 index 00000000000..5790161c435 --- /dev/null +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -0,0 +1,678 @@ +package btree + +import ( + "fmt" + "sort" + "testing" + + "gno.land/p/demo/btree" +) + +// Content represents a key-value pair where the Key can be either an int or string +// and the Value can be any type. +type Content struct { + Key interface{} + Value interface{} +} + +// Less compares two Content records by their Keys. +// The Key must be either an int or a string. +func (c Content) Less(than Record) bool { + other, ok := than.(Content) + if !ok { + panic("cannot compare: incompatible types") + } + + switch key := c.Key.(type) { + case int: + switch otherKey := other.Key.(type) { + case int: + return key < otherKey + case string: + return true // ints are always less than strings + default: + panic("unsupported key type: must be int or string") + } + case string: + switch otherKey := other.Key.(type) { + case int: + return false // strings are always greater than ints + case string: + return key < otherKey + default: + panic("unsupported key type: must be int or string") + } + default: + panic("unsupported key type: must be int or string") + } +} + +type ContentSlice []Content + +func (s ContentSlice) Len() int { + return len(s) +} + +func (s ContentSlice) Less(i, j int) bool { + return s[i].Less(s[j]) +} + +func (s ContentSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ContentSlice) Copy() ContentSlice { + newSlice := make(ContentSlice, len(s)) + copy(newSlice, s) + return newSlice +} + +// Ensure Content implements the Record interface. +var _ Record = Content{} + +// **************************************************************************** +// Test helpers +// **************************************************************************** + +func genericSeeding(tree *btree.BTree, size int) *btree.BTree { + for i := 0; i < size; i++ { + tree.Insert(Content{Key: i, Value: fmt.Sprintf("Value_%d", i)}) + } + return tree +} + +func intSlicesCompare(left, right []int) int { + if len(left) != len(right) { + if len(left) > len(right) { + return 1 + } else { + return -1 + } + } + + for position, leftInt := range left { + if leftInt != right[position] { + if leftInt > right[position] { + return 1 + } else { + return -1 + } + } + } + + return 0 +} + +// **************************************************************************** +// Tests +// **************************************************************************** + +func TestLen(t *testing.T) { + length := genericSeeding(btree.New(WithDegree(10)), 7).Len() + if length != 7 { + t.Errorf("Length is incorrect. Expected 7, but got %d.", length) + } + + length = genericSeeding(btree.New(WithDegree(5)), 111).Len() + if length != 111 { + t.Errorf("Length is incorrect. Expected 111, but got %d.", length) + } + + length = genericSeeding(btree.New(WithDegree(30)), 123).Len() + if length != 123 { + t.Errorf("Length is incorrect. Expected 123, but got %d.", length) + } + +} + +func TestHas(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 40) + + if tree.Has(Content{Key: 7}) != true { + t.Errorf("Has(7) reported false, but it should be true.") + } + if tree.Has(Content{Key: 39}) != true { + t.Errorf("Has(40) reported false, but it should be true.") + } + if tree.Has(Content{Key: 1111}) == true { + t.Errorf("Has(1111) reported true, but it should be false.") + } +} + +func TestMin(t *testing.T) { + min := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + + if min.Key != 0 { + t.Errorf("Minimum should have been 0, but it was reported as %d.", min) + } +} + +func TestMax(t *testing.T) { + max := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + + if max.Key != 0 { + t.Errorf("Minimum should have been 0, but it was reported as %d.", max) + } +} + +func TestGet(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 40) + + if Content(tree.Get(Content{Key: 7})).Value != "Value_7" { + t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", tree.Get(Content{Key: 7})) + } + if Content(tree.Get(Content{Key: 39})).Value != "Value_39" { + t.Errorf("Get(40) should have returnd 'Value_39', but it returned %v.", tree.Get(Content{Key: 39})) + } + if tree.Get(Content{Key: 1111}) != nil { + t.Errorf("Get(1111) returned %v, but it should be nil.", Content(tree.Get(Content{Key: 1111}))) + } +} + +func TestDescend(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 5) + + expected := []int{4, 3, 2, 1, 0} + found := []int{} + + tree.Descend(func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("Descend returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendGreaterThan(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{9, 8, 7, 6, 5} + found := []int{} + + tree.DescendGreaterThan(Content{Key: 4}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendGreaterThan returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendLessOrEqual(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{4, 3, 2, 1, 0} + found := []int{} + + tree.DescendLessOrEqual(Content{Key: 4}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendRange(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{6, 5, 4, 3, 2} + found := []int{} + + tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscend(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 5) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + tree.Ascend(func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("Ascend returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendGreaterOrEqual(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{5, 6, 7, 8, 9} + found := []int{} + + tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("AscendGreaterOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendLessThan(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + tree.AscendLessThan(Content{Key: 5}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendRange(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{2, 3, 4, 5, 6} + found := []int{} + + tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDeleteMin(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestShift(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of Shift returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestDeleteMax(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{99, 98, 97, 96, 95} + found := []int{} + + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestPop(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{99, 98, 97, 96, 95} + found := []int{} + + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestInsertGet(t *testing.T) { + tree := btree.New(WithDegree(4)) + + expected := []Content{} + + for count := 0; count < 20; count++ { + value := fmt.Sprintf("Value_%d", count) + tree.Insert(Content{Key: count, Value: value}) + expected = append(expected, Content{Key: count, Value: value}) + } + + for count := 0; count < 20; count++ { + if tree.Get(Content{Key: count}) != expected[count] { + t.Errorf("Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.", expected[count], count, tree.Get(Content{Key: count})) + } + } +} + +func TestClone(t *testing.T) { +} + +// ***** The following tests are functional or stress testing type tests. + +func TestBTree(t *testing.T) { + // Create a B-Tree of degree 3 + tree := btree.New(WithDegree(3)) + + //insertData := []Content{} + var insertData ContentSlice + + // Insert integer keys + intKeys := []int{10, 20, 5, 6, 12, 30, 7, 17} + for _, key := range intKeys { + content := Content{Key: key, Value: fmt.Sprintf("Value_%d", key)} + insertData = append(insertData, content) + result := tree.Insert(content) + if result != nil { + t.Errorf("**** Already in the tree? %v", result) + } + } + + // Insert string keys + stringKeys := []string{"apple", "banana", "cherry", "date", "fig", "grape"} + for _, key := range stringKeys { + content := Content{Key: key, Value: fmt.Sprintf("Fruit_%s", key)} + insertData = append(insertData, content) + tree.Insert(content) + } + + if tree.Len() != 14 { + t.Errorf("Tree length wrong. Expected 14 but got %d", tree.Len()) + } + + // Search for existing and non-existing keys + searchTests := []struct { + test Content + expected bool + }{ + {Content{Key: 10, Value: "Value_10"}, true}, + {Content{Key: 15, Value: ""}, false}, + {Content{Key: "banana", Value: "Fruit_banana"}, true}, + {Content{Key: "kiwi", Value: ""}, false}, + } + + t.Logf("Search Tests:\n") + for _, test := range searchTests { + val := tree.Get(test.test) + + if test.expected { + if val != nil && Content(val).Value == test.test.Value { + t.Logf("Found expected key:value %v:%v", test.test.Key, test.test.Value) + } else { + if val == nil { + t.Logf("Didn't find %v, but expected", test.test.Key) + } else { + t.Errorf("Expected key %v:%v, but found %v:%v.", test.test.Key, test.test.Value, Content(val).Key, Content(val).Value) + } + } + } else { + if val != nil { + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.test.Key, Content(val).Key, Content(val).Value) + } else { + t.Logf("Didn't find %v, but wasn't expected", test.test.Key) + } + } + } + + // Iterate in order + t.Logf("\nIn-order Iteration:\n") + pos := 0 + + if tree.Len() != 14 { + t.Errorf("Tree length wrong. Expected 14 but got %d", tree.Len()) + } + + sortedInsertData := insertData.Copy() + sort.Sort(sortedInsertData) + + t.Logf("Insert Data Length: %d", len(insertData)) + t.Logf("Sorted Data Length: %d", len(sortedInsertData)) + t.Logf("Tree Length: %d", tree.Len()) + + tree.Ascend(func(_record btree.Record) bool { + record := Content(_record) + t.Logf("Key:Value == %v:%v", record.Key, record.Value) + if record.Key != sortedInsertData[pos].Key { + t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) + } + pos++ + return true + }) + // // Reverse Iterate + t.Logf("\nReverse-order Iteration:\n") + pos = len(sortedInsertData) - 1 + + tree.Descend(func(_record btree.Record) bool { + record := Content(_record) + t.Logf("Key:Value == %v:%v", record.Key, record.Value) + if record.Key != sortedInsertData[pos].Key { + t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) + } + pos-- + return true + }) + + deleteTests := []Content{ + Content{Key: 10, Value: "Value_10"}, + Content{Key: 15, Value: ""}, + Content{Key: "banana", Value: "Fruit_banana"}, + Content{Key: "kiwi", Value: ""}, + } + for _, test := range deleteTests { + fmt.Printf("\nDeleting %+v\n", test) + tree.Delete(test) + } + + if tree.Len() != 12 { + t.Errorf("Tree length wrong. Expected 12 but got %d", tree.Len()) + } + + for _, test := range deleteTests { + val := tree.Get(test) + if val != nil { + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.Key, Content(val).Key, Content(val).Value) + } else { + t.Logf("Didn't find %v, but wasn't expected", test.Key) + } + } +} + +func TestStress(t *testing.T) { + // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. + // Insert 1000 records into each tree, then search for each record. + // Delete half of the records, skipping every other one, then search for each record. + + for degree := 3; degree <= 12; degree += 3 { + t.Logf("Testing B-Tree of degree %d\n", degree) + tree := btree.New(WithDegree(degree)) + + // Insert 1000 records + t.Logf("Inserting 1000 records\n") + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Insert(content) + } + + // Search for all records + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + // Delete half of the records + for i := 0; i < 1000; i += 2 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + } + + // Search for all records + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + if i%2 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + } + } + + // Now create a very large tree, with 100000 records + // Then delete roughly one third of them, using a very basic random number generation scheme + // (implement it right here) to determine which records to delete. + // Print a few lines using Logf to let the user know what's happening. + + t.Logf("Testing B-Tree of degree 10 with 100000 records\n") + tree := btree.New(WithDegree(10)) + + // Insert 100000 records + t.Logf("Inserting 100000 records\n") + for i := 0; i < 100000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Insert(content) + } + + // Implement a very basic random number generator + seed := 0 + random := func() int { + seed = (seed*1103515245 + 12345) & 0x7fffffff + return seed + } + + // Delete one third of the records + t.Logf("Deleting one third of the records\n") + for i := 0; i < 35000; i++ { + content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + } +} + +// Write a test that populates a large B-Tree with 10000 records. +// It should then `Clone` the tree, make some changes to both the original and the clone, +// And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated +// to the tree they were made in. + +func TestBTreeCloneIsolation(t *testing.T) { + t.Logf("Creating B-Tree of degree 10 with 10000 records\n") + tree := genericSeeding(btree.New(WithDegree(10)), 10000) + + // Clone the tree + t.Logf("Cloning the tree\n") + clone := tree.Clone() + + // Make some changes to the original and the clone + t.Logf("Making changes to the original and the clone\n") + for i := 0; i < 10000; i += 2 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + content = Content{Key: i + 1, Value: fmt.Sprintf("Value_%d", i+1)} + clone.Delete(content) + } + + // Clone the clone + t.Logf("Cloning the clone\n") + clone2 := clone.Clone() + + // Make some changes to all three trees + t.Logf("Making changes to all three trees\n") + for i := 0; i < 10000; i += 3 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + content = Content{Key: i, Value: fmt.Sprintf("Value_%d", i+1)} + clone.Delete(content) + content = Content{Key: i + 2, Value: fmt.Sprintf("Value_%d", i+2)} + clone2.Delete(content) + } + + // Check that the changes are isolated to the tree they were made in + t.Logf("Checking that the changes are isolated to the tree they were made in\n") + for i := 0; i < 10000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + + if i%3 == 0 || i%2 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + val = clone.Get(content) + if i%2 != 0 || i%3 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + val = clone2.Get(content) + if i%2 != 0 || (i-2)%3 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + } +} diff --git a/examples/gno.land/p/demo/btree/gno.mod b/examples/gno.land/p/demo/btree/gno.mod new file mode 100644 index 00000000000..aed2fe6b730 --- /dev/null +++ b/examples/gno.land/p/demo/btree/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/btree From 87ef6043ba116b18efe402ec3c8360d9dd4a98a9 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Fri, 6 Dec 2024 05:41:54 +0800 Subject: [PATCH 290/345] chore: add filetest in gnovm/makefile (#3272)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- gnovm/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gnovm/Makefile b/gnovm/Makefile index 31daf942554..3cf2f74276b 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -97,6 +97,9 @@ _test.stdlibs: go run ./cmd/gno test -v ./stdlibs/... +_test.filetest:; + go test pkg/gnolang/files_test.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS) + ######################################## # Code gen # TODO: move _dev.stringer to go:generate instructions, simplify generate From 9a0da98665fbc5a967f4696acc101c63e680f588 Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Fri, 6 Dec 2024 09:38:08 +0100 Subject: [PATCH 291/345] chore: Fix the linting error. (#3282) --- examples/gno.land/p/demo/btree/btree_test.gno | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno index 5790161c435..a0f7c1c55ca 100644 --- a/examples/gno.land/p/demo/btree/btree_test.gno +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -4,8 +4,6 @@ import ( "fmt" "sort" "testing" - - "gno.land/p/demo/btree" ) // Content represents a key-value pair where the Key can be either an int or string @@ -74,7 +72,7 @@ var _ Record = Content{} // Test helpers // **************************************************************************** -func genericSeeding(tree *btree.BTree, size int) *btree.BTree { +func genericSeeding(tree *BTree, size int) *BTree { for i := 0; i < size; i++ { tree.Insert(Content{Key: i, Value: fmt.Sprintf("Value_%d", i)}) } @@ -108,17 +106,17 @@ func intSlicesCompare(left, right []int) int { // **************************************************************************** func TestLen(t *testing.T) { - length := genericSeeding(btree.New(WithDegree(10)), 7).Len() + length := genericSeeding(New(WithDegree(10)), 7).Len() if length != 7 { t.Errorf("Length is incorrect. Expected 7, but got %d.", length) } - length = genericSeeding(btree.New(WithDegree(5)), 111).Len() + length = genericSeeding(New(WithDegree(5)), 111).Len() if length != 111 { t.Errorf("Length is incorrect. Expected 111, but got %d.", length) } - length = genericSeeding(btree.New(WithDegree(30)), 123).Len() + length = genericSeeding(New(WithDegree(30)), 123).Len() if length != 123 { t.Errorf("Length is incorrect. Expected 123, but got %d.", length) } @@ -126,7 +124,7 @@ func TestLen(t *testing.T) { } func TestHas(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 40) + tree := genericSeeding(New(WithDegree(10)), 40) if tree.Has(Content{Key: 7}) != true { t.Errorf("Has(7) reported false, but it should be true.") @@ -140,7 +138,7 @@ func TestHas(t *testing.T) { } func TestMin(t *testing.T) { - min := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + min := Content(genericSeeding(New(WithDegree(10)), 53).Min()) if min.Key != 0 { t.Errorf("Minimum should have been 0, but it was reported as %d.", min) @@ -148,7 +146,7 @@ func TestMin(t *testing.T) { } func TestMax(t *testing.T) { - max := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + max := Content(genericSeeding(New(WithDegree(10)), 53).Min()) if max.Key != 0 { t.Errorf("Minimum should have been 0, but it was reported as %d.", max) @@ -156,7 +154,7 @@ func TestMax(t *testing.T) { } func TestGet(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 40) + tree := genericSeeding(New(WithDegree(10)), 40) if Content(tree.Get(Content{Key: 7})).Value != "Value_7" { t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", tree.Get(Content{Key: 7})) @@ -170,12 +168,12 @@ func TestGet(t *testing.T) { } func TestDescend(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 5) + tree := genericSeeding(New(WithDegree(10)), 5) expected := []int{4, 3, 2, 1, 0} found := []int{} - tree.Descend(func(_record btree.Record) bool { + tree.Descend(func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -187,12 +185,12 @@ func TestDescend(t *testing.T) { } func TestDescendGreaterThan(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{9, 8, 7, 6, 5} found := []int{} - tree.DescendGreaterThan(Content{Key: 4}, func(_record btree.Record) bool { + tree.DescendGreaterThan(Content{Key: 4}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -204,12 +202,12 @@ func TestDescendGreaterThan(t *testing.T) { } func TestDescendLessOrEqual(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{4, 3, 2, 1, 0} found := []int{} - tree.DescendLessOrEqual(Content{Key: 4}, func(_record btree.Record) bool { + tree.DescendLessOrEqual(Content{Key: 4}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -221,12 +219,12 @@ func TestDescendLessOrEqual(t *testing.T) { } func TestDescendRange(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{6, 5, 4, 3, 2} found := []int{} - tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record btree.Record) bool { + tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -238,12 +236,12 @@ func TestDescendRange(t *testing.T) { } func TestAscend(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 5) + tree := genericSeeding(New(WithDegree(10)), 5) expected := []int{0, 1, 2, 3, 4} found := []int{} - tree.Ascend(func(_record btree.Record) bool { + tree.Ascend(func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -255,12 +253,12 @@ func TestAscend(t *testing.T) { } func TestAscendGreaterOrEqual(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{5, 6, 7, 8, 9} found := []int{} - tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record btree.Record) bool { + tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -272,12 +270,12 @@ func TestAscendGreaterOrEqual(t *testing.T) { } func TestAscendLessThan(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{0, 1, 2, 3, 4} found := []int{} - tree.AscendLessThan(Content{Key: 5}, func(_record btree.Record) bool { + tree.AscendLessThan(Content{Key: 5}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -289,12 +287,12 @@ func TestAscendLessThan(t *testing.T) { } func TestAscendRange(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{2, 3, 4, 5, 6} found := []int{} - tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record btree.Record) bool { + tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -306,7 +304,7 @@ func TestAscendRange(t *testing.T) { } func TestDeleteMin(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{0, 1, 2, 3, 4} found := []int{} @@ -323,7 +321,7 @@ func TestDeleteMin(t *testing.T) { } func TestShift(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{0, 1, 2, 3, 4} found := []int{} @@ -340,7 +338,7 @@ func TestShift(t *testing.T) { } func TestDeleteMax(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{99, 98, 97, 96, 95} found := []int{} @@ -357,7 +355,7 @@ func TestDeleteMax(t *testing.T) { } func TestPop(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{99, 98, 97, 96, 95} found := []int{} @@ -374,7 +372,7 @@ func TestPop(t *testing.T) { } func TestInsertGet(t *testing.T) { - tree := btree.New(WithDegree(4)) + tree := New(WithDegree(4)) expected := []Content{} @@ -398,7 +396,7 @@ func TestClone(t *testing.T) { func TestBTree(t *testing.T) { // Create a B-Tree of degree 3 - tree := btree.New(WithDegree(3)) + tree := New(WithDegree(3)) //insertData := []Content{} var insertData ContentSlice @@ -475,7 +473,7 @@ func TestBTree(t *testing.T) { t.Logf("Sorted Data Length: %d", len(sortedInsertData)) t.Logf("Tree Length: %d", tree.Len()) - tree.Ascend(func(_record btree.Record) bool { + tree.Ascend(func(_record Record) bool { record := Content(_record) t.Logf("Key:Value == %v:%v", record.Key, record.Value) if record.Key != sortedInsertData[pos].Key { @@ -488,7 +486,7 @@ func TestBTree(t *testing.T) { t.Logf("\nReverse-order Iteration:\n") pos = len(sortedInsertData) - 1 - tree.Descend(func(_record btree.Record) bool { + tree.Descend(func(_record Record) bool { record := Content(_record) t.Logf("Key:Value == %v:%v", record.Key, record.Value) if record.Key != sortedInsertData[pos].Key { @@ -530,7 +528,7 @@ func TestStress(t *testing.T) { for degree := 3; degree <= 12; degree += 3 { t.Logf("Testing B-Tree of degree %d\n", degree) - tree := btree.New(WithDegree(degree)) + tree := New(WithDegree(degree)) // Insert 1000 records t.Logf("Inserting 1000 records\n") @@ -576,7 +574,7 @@ func TestStress(t *testing.T) { // Print a few lines using Logf to let the user know what's happening. t.Logf("Testing B-Tree of degree 10 with 100000 records\n") - tree := btree.New(WithDegree(10)) + tree := New(WithDegree(10)) // Insert 100000 records t.Logf("Inserting 100000 records\n") @@ -607,7 +605,7 @@ func TestStress(t *testing.T) { func TestBTreeCloneIsolation(t *testing.T) { t.Logf("Creating B-Tree of degree 10 with 10000 records\n") - tree := genericSeeding(btree.New(WithDegree(10)), 10000) + tree := genericSeeding(New(WithDegree(10)), 10000) // Clone the tree t.Logf("Cloning the tree\n") From 08fb49a1010f461a5ce88a013c935ec217a7acaa Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:41:04 +0100 Subject: [PATCH 292/345] fix: bump golangci lint to 1.62 (#3278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #3066 Bump `golangci-lint` to `1.62`, this bump include the following change. - Removing `gopls` from direct dependency as it creates some conflict with the latest version of `golangci-lint`. `gopls` is not meant to be tracked as a direct dependency tool; it's a personal tool and it's dependent on the user's Go version, not the project-specific version. - Update all `printf`-like methods that should not use non-constant format input. Instead, I choose to duplicate those methods into two separate methods: one should be dedicated to formatting, and the other one to simple direct messaging. ex. `errors.Wrap` -> `errors.Wrapf` - ~Ignoring `gosec` issue with `ripemd160` for now, I will open an issue to double-check this one.~ ✅ Double-checked with @zivkovicmilos & @jaekwon, we can ignore it. - Ignoring `gosec` G115 Integer overflow conversion; there is no solution to check the overflow in the time of conversion, so I think the linter shouldn't check for the overflow.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Morgan --- .github/golangci.yml | 1 + .github/workflows/lint_template.yml | 2 +- gno.land/pkg/gnoclient/client_queries.go | 6 +- gno.land/pkg/gnoclient/client_txs.go | 4 +- gno.land/pkg/sdk/vm/keeper.go | 14 +- gnovm/pkg/gnolang/op_expressions.go | 26 +- gnovm/pkg/gnolang/store.go | 2 +- gnovm/pkg/gnolang/types.go | 36 ++- gnovm/pkg/gnolang/values.go | 48 +-- misc/devdeps/deps.go | 1 - misc/devdeps/go.mod | 131 ++++---- misc/devdeps/go.sum | 284 +++++++++--------- tm2/pkg/bft/blockchain/pool.go | 8 +- tm2/pkg/bft/mempool/clist_mempool.go | 10 +- tm2/pkg/bft/mempool/mempool.go | 2 +- tm2/pkg/bft/rpc/core/blocks.go | 6 +- tm2/pkg/bft/rpc/core/blocks_test.go | 18 +- .../bft/rpc/lib/server/http_server_test.go | 10 +- tm2/pkg/bft/types/evidence.go | 4 +- tm2/pkg/bft/types/genesis.go | 2 +- tm2/pkg/bft/types/validator_set.go | 14 +- tm2/pkg/bft/types/vote_set.go | 10 +- tm2/pkg/bft/wal/wal.go | 42 +-- tm2/pkg/crypto/keys/client/maketx.go | 4 +- tm2/pkg/crypto/keys/keybase.go | 4 +- tm2/pkg/crypto/merkle/proof_key_path.go | 4 +- tm2/pkg/crypto/secp256k1/secp256k1.go | 6 +- tm2/pkg/errors/errors.go | 15 +- tm2/pkg/errors/errors_test.go | 4 +- tm2/pkg/iavl/proof_range.go | 2 +- tm2/pkg/os/os.go | 2 +- tm2/pkg/p2p/switch.go | 2 +- tm2/pkg/std/coin.go | 2 +- tm2/pkg/std/gasprice.go | 6 +- tm2/pkg/store/cache/store_test.go | 4 +- 35 files changed, 369 insertions(+), 367 deletions(-) diff --git a/.github/golangci.yml b/.github/golangci.yml index 43cea27a791..b8bd5537135 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -51,6 +51,7 @@ linters-settings: excludes: - G204 # Subprocess launched with a potential tainted input or cmd arguments - G306 # Expect WriteFile permissions to be 0600 or less + - G115 # Integer overflow conversion, no solution to check the overflow in time of convert, so linter shouldn't check the overflow. stylecheck: checks: [ "all", "-ST1022", "-ST1003" ] errorlint: diff --git a/.github/workflows/lint_template.yml b/.github/workflows/lint_template.yml index 5b792269c02..b7568d19c41 100644 --- a/.github/workflows/lint_template.yml +++ b/.github/workflows/lint_template.yml @@ -25,4 +25,4 @@ jobs: working-directory: ${{ inputs.modulepath }} args: --config=${{ github.workspace }}/.github/golangci.yml - version: v1.59 # sync with misc/devdeps + version: v1.62 # sync with misc/devdeps diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index 9d9d7305116..2e09842ae31 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -31,7 +31,7 @@ func (c *Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { } if qres.Response.Error != nil { - return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) + return qres, errors.Wrapf(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) } return qres, nil @@ -97,7 +97,7 @@ func (c *Client) Render(pkgPath string, args string) (string, *ctypes.ResultABCI return "", nil, errors.Wrap(err, "query render") } if qres.Response.Error != nil { - return "", nil, errors.Wrap(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) + return "", nil, errors.Wrapf(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) } return string(qres.Response.Data), qres, nil @@ -120,7 +120,7 @@ func (c *Client) QEval(pkgPath string, expression string) (string, *ctypes.Resul return "", nil, errors.Wrap(err, "query qeval") } if qres.Response.Error != nil { - return "", nil, errors.Wrap(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) + return "", nil, errors.Wrapf(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) } return string(qres.Response.Data), qres, nil diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index 9d3dbde22ae..d7f6f053242 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -283,10 +283,10 @@ func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxC } if bres.CheckTx.IsErr() { - return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + return bres, errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) } if bres.DeliverTx.IsErr() { - return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + return bres, errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) } return bres, nil diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 68f784a52e7..52eff20ea95 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -390,7 +390,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM addpkg panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM addpkg panic: %v\n%s\n", r, m2.String()) return } @@ -491,10 +491,10 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) case gno.UnhandledPanicError: - err = errors.Wrap(fmt.Errorf("%v", r.Error()), "VM call panic: %s\nStacktrace: %s\n", + err = errors.Wrapf(fmt.Errorf("%v", r.Error()), "VM call panic: %s\nStacktrace: %s\n", r.Error(), m.ExceptionsStacktrace()) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\nMachine State:%s\nStacktrace: %s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM call panic: %v\nMachine State:%s\nStacktrace: %s\n", r, m.String(), m.Stacktrace().String()) return } @@ -594,7 +594,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM run main addpkg panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM run main addpkg panic: %v\n%s\n", r, m.String()) return } @@ -620,7 +620,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM run main call panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM run main call panic: %v\n%s\n", r, m2.String()) return } @@ -750,7 +750,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n", r, m.String()) return } @@ -816,7 +816,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval string panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM query eval string panic: %v\n%s\n", r, m.String()) return } diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index b614e72e945..c0f6225740b 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -88,20 +88,20 @@ func (m *Machine) doOpSelector() { func (m *Machine) doOpSlice() { sx := m.PopExpr().(*SliceExpr) - var low, high, max int = -1, -1, -1 + var lowVal, highVal, maxVal int = -1, -1, -1 // max if sx.Max != nil { - max = m.PopValue().ConvertGetInt() + maxVal = m.PopValue().ConvertGetInt() } // high if sx.High != nil { - high = m.PopValue().ConvertGetInt() + highVal = m.PopValue().ConvertGetInt() } // low if sx.Low != nil { - low = m.PopValue().ConvertGetInt() + lowVal = m.PopValue().ConvertGetInt() } else { - low = 0 + lowVal = 0 } // slice base x xv := m.PopValue() @@ -114,14 +114,14 @@ func (m *Machine) doOpSlice() { } // fill default based on xv if sx.High == nil { - high = xv.GetLength() + highVal = xv.GetLength() } // all low:high:max cases - if max == -1 { - sv := xv.GetSlice(m.Alloc, low, high) + if maxVal == -1 { + sv := xv.GetSlice(m.Alloc, lowVal, highVal) m.PushValue(sv) } else { - sv := xv.GetSlice2(m.Alloc, low, high, max) + sv := xv.GetSlice2(m.Alloc, lowVal, highVal, maxVal) m.PushValue(sv) } } @@ -593,16 +593,16 @@ func (m *Machine) doOpSliceLit2() { // peek slice type. st := m.PeekValue(1).V.(TypeValue).Type // calculate maximum index. - max := 0 + maxVal := 0 for i := 0; i < el; i++ { itv := tvs[i*2+0] idx := itv.ConvertGetInt() - if idx > max { - max = idx + if idx > maxVal { + maxVal = idx } } // construct element buf slice. - es := make([]TypedValue, max+1) + es := make([]TypedValue, maxVal+1) for i := 0; i < el; i++ { itv := tvs[i*2+0] vtv := tvs[i*2+1] diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index b721194823d..4cbc2948f43 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -818,7 +818,7 @@ func backendPackageIndexKey(index uint64) string { } func backendPackagePathKey(path string) string { - return fmt.Sprintf("pkg:" + path) + return "pkg:" + path } // ---------------------------------------- diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index eedb71ffa73..bfc7cc31584 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -41,7 +41,15 @@ func (tid TypeID) String() string { return string(tid) } -func typeid(f string, args ...interface{}) (tid TypeID) { +func typeid(s string) (tid TypeID) { + x := TypeID(s) + if debug { + debug.Println("TYPEID", s) + } + return x +} + +func typeidf(f string, args ...interface{}) (tid TypeID) { fs := fmt.Sprintf(f, args...) x := TypeID(fs) if debug { @@ -521,7 +529,7 @@ func (at *ArrayType) Kind() Kind { func (at *ArrayType) TypeID() TypeID { if at.typeid.IsZero() { - at.typeid = typeid("[%d]%s", at.Len, at.Elt.TypeID().String()) + at.typeid = typeidf("[%d]%s", at.Len, at.Elt.TypeID().String()) } return at.typeid } @@ -564,9 +572,9 @@ func (st *SliceType) Kind() Kind { func (st *SliceType) TypeID() TypeID { if st.typeid.IsZero() { if st.Vrd { - st.typeid = typeid("...%s", st.Elt.TypeID().String()) + st.typeid = typeidf("...%s", st.Elt.TypeID().String()) } else { - st.typeid = typeid("[]%s", st.Elt.TypeID().String()) + st.typeid = typeidf("[]%s", st.Elt.TypeID().String()) } } return st.typeid @@ -607,7 +615,7 @@ func (pt *PointerType) Kind() Kind { func (pt *PointerType) TypeID() TypeID { if pt.typeid.IsZero() { - pt.typeid = typeid("*%s", pt.Elt.TypeID().String()) + pt.typeid = typeidf("*%s", pt.Elt.TypeID().String()) } return pt.typeid } @@ -748,7 +756,7 @@ func (st *StructType) TypeID() TypeID { // may have the same TypeID if and only if neither have // unexported fields. st.PkgPath is only included in field // names that are not uppercase. - st.typeid = typeid( + st.typeid = typeidf( "struct{%s}", FieldTypeList(st.Fields).TypeIDForPackage(st.PkgPath), ) @@ -1078,11 +1086,11 @@ func (ct *ChanType) TypeID() TypeID { if ct.typeid.IsZero() { switch ct.Dir { case SEND | RECV: - ct.typeid = typeid("chan{%s}" + ct.Elt.TypeID().String()) + ct.typeid = typeidf("chan{%s}", ct.Elt.TypeID().String()) case SEND: - ct.typeid = typeid("<-chan{%s}" + ct.Elt.TypeID().String()) + ct.typeid = typeidf("<-chan{%s}", ct.Elt.TypeID().String()) case RECV: - ct.typeid = typeid("chan<-{%s}" + ct.Elt.TypeID().String()) + ct.typeid = typeidf("chan<-{%s}", ct.Elt.TypeID().String()) default: panic("should not happen") } @@ -1298,7 +1306,7 @@ func (ft *FuncType) TypeID() TypeID { } */ if ft.typeid.IsZero() { - ft.typeid = typeid( + ft.typeid = typeidf( "func(%s)(%s)", // pp, ps.UnnamedTypeID(), @@ -1361,7 +1369,7 @@ func (mt *MapType) Kind() Kind { func (mt *MapType) TypeID() TypeID { if mt.typeid.IsZero() { - mt.typeid = typeid( + mt.typeid = typeidf( "map[%s]%s", mt.Key.TypeID().String(), mt.Value.TypeID().String(), @@ -1489,7 +1497,7 @@ func (dt *DeclaredType) TypeID() TypeID { } func DeclaredTypeID(pkgPath string, name Name) TypeID { - return typeid("%s.%s", pkgPath, name) + return typeidf("%s.%s", pkgPath, name) } func (dt *DeclaredType) String() string { @@ -1787,9 +1795,9 @@ func (nt *NativeType) TypeID() TypeID { // > (e.g., base64 instead of "encoding/base64") and is not // > guaranteed to be unique among types. To test for type identity, // > compare the Types directly. - nt.typeid = typeid("go:%s.%s", nt.Type.PkgPath(), nt.Type.String()) + nt.typeid = typeidf("go:%s.%s", nt.Type.PkgPath(), nt.Type.String()) } else { - nt.typeid = typeid("go:%s.%s", nt.Type.PkgPath(), nt.Type.Name()) + nt.typeid = typeidf("go:%s.%s", nt.Type.PkgPath(), nt.Type.Name()) } } return nt.typeid diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index e7a6274a780..4c2e2835f95 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -2248,41 +2248,41 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { } } -func (tv *TypedValue) GetSlice2(alloc *Allocator, low, high, max int) TypedValue { - if low < 0 { +func (tv *TypedValue) GetSlice2(alloc *Allocator, lowVal, highVal, maxVal int) TypedValue { + if lowVal < 0 { panic(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - low)) + lowVal)) } - if high < 0 { + if highVal < 0 { panic(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - high)) + highVal)) } - if max < 0 { + if maxVal < 0 { panic(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - max)) + maxVal)) } - if low > high { + if lowVal > highVal { panic(fmt.Sprintf( "invalid slice index %d > %d", - low, high)) + lowVal, highVal)) } - if high > max { + if highVal > maxVal { panic(fmt.Sprintf( "invalid slice index %d > %d", - high, max)) + highVal, maxVal)) } - if tv.GetCapacity() < high { + if tv.GetCapacity() < highVal { panic(fmt.Sprintf( "slice bounds out of range [%d:%d:%d] with capacity %d", - low, high, max, tv.GetCapacity())) + lowVal, highVal, maxVal, tv.GetCapacity())) } - if tv.GetCapacity() < max { + if tv.GetCapacity() < maxVal { panic(fmt.Sprintf( "slice bounds out of range [%d:%d:%d] with capacity %d", - low, high, max, tv.GetCapacity())) + lowVal, highVal, maxVal, tv.GetCapacity())) } switch bt := baseOf(tv.T).(type) { case *ArrayType: @@ -2294,15 +2294,15 @@ func (tv *TypedValue) GetSlice2(alloc *Allocator, low, high, max int) TypedValue return TypedValue{ T: st, V: alloc.NewSlice( - av, // base - low, // low - high-low, // length - max-low, // maxcap + av, // base + lowVal, // low + highVal-lowVal, // length + maxVal-lowVal, // maxcap ), } case *SliceType: if tv.V == nil { - if low != 0 || high != 0 || max != 0 { + if lowVal != 0 || highVal != 0 || maxVal != 0 { panic("nil slice index out of range") } return TypedValue{ @@ -2314,10 +2314,10 @@ func (tv *TypedValue) GetSlice2(alloc *Allocator, low, high, max int) TypedValue return TypedValue{ T: tv.T, V: alloc.NewSlice( - sv.Base, // base - sv.Offset+low, // offset - high-low, // length - max-low, // maxcap + sv.Base, // base + sv.Offset+lowVal, // offset + highVal-lowVal, // length + maxVal-lowVal, // maxcap ), } default: diff --git a/misc/devdeps/deps.go b/misc/devdeps/deps.go index a011868e4c2..f7da2b10c12 100644 --- a/misc/devdeps/deps.go +++ b/misc/devdeps/deps.go @@ -15,7 +15,6 @@ import ( _ "golang.org/x/tools/cmd/goimports" // required for formatting, linting, pls. - _ "golang.org/x/tools/gopls" _ "mvdan.cc/gofumpt" // protoc, genproto diff --git a/misc/devdeps/go.mod b/misc/devdeps/go.mod index c07b82fd11d..d3b40b73b52 100644 --- a/misc/devdeps/go.mod +++ b/misc/devdeps/go.mod @@ -1,46 +1,42 @@ module github.com/gnolang/gno/misc/devdeps -go 1.22 - -toolchain go1.22.4 +go 1.22.1 require ( - github.com/golangci/golangci-lint v1.59.1 // sync with github action - golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34 - golang.org/x/tools/gopls v0.16.1 + github.com/campoy/embedmd v1.0.0 + github.com/golangci/golangci-lint v1.62.2 // sync with github action + golang.org/x/tools v0.27.0 google.golang.org/protobuf v1.35.1 moul.io/testman v1.5.0 - mvdan.cc/gofumpt v0.6.0 + mvdan.cc/gofumpt v0.7.0 ) -require github.com/campoy/embedmd v1.0.0 - require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect github.com/4meepo/tagalign v1.3.4 // indirect - github.com/Abirdcfly/dupword v0.0.14 // indirect - github.com/Antonboom/errname v0.1.13 // indirect - github.com/Antonboom/nilnil v0.1.9 // indirect - github.com/Antonboom/testifylint v1.3.1 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/Crocmagnon/fatcontext v0.2.2 // indirect + github.com/Abirdcfly/dupword v0.1.3 // indirect + github.com/Antonboom/errname v1.0.0 // indirect + github.com/Antonboom/nilnil v1.0.0 // indirect + github.com/Antonboom/testifylint v1.5.2 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/Crocmagnon/fatcontext v0.5.3 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect - github.com/alecthomas/go-check-sumtype v0.1.4 // indirect - github.com/alexkohler/nakedret/v2 v2.0.4 // indirect + github.com/alecthomas/go-check-sumtype v0.2.0 // indirect + github.com/alexkohler/nakedret/v2 v2.0.5 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/ashanbrown/forbidigo v1.6.0 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bkielbasa/cyclop v1.2.1 // indirect + github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect - github.com/bombsimon/wsl/v4 v4.2.1 // indirect - github.com/breml/bidichk v0.2.7 // indirect - github.com/breml/errchkjson v0.3.6 // indirect + github.com/bombsimon/wsl/v4 v4.4.1 // indirect + github.com/breml/bidichk v0.3.2 // indirect + github.com/breml/errchkjson v0.4.0 // indirect github.com/butuzov/ireturn v0.3.0 // indirect github.com/butuzov/mirror v1.2.0 // indirect github.com/catenacyber/perfsprint v0.7.1 // indirect @@ -48,19 +44,19 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect - github.com/ckaznocha/intrange v0.1.2 // indirect + github.com/ckaznocha/intrange v0.2.1 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect - github.com/daixiang0/gci v0.13.4 // indirect + github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/ettle/strcase v0.2.0 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.6 // indirect - github.com/go-critic/go-critic v0.11.4 // indirect + github.com/ghostiam/protogetter v0.3.8 // indirect + github.com/go-critic/go-critic v0.11.5 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -68,13 +64,14 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect - github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect + github.com/golangci/go-printf-func-name v0.1.0 // indirect + github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect github.com/golangci/misspell v0.6.0 // indirect github.com/golangci/modinfo v0.3.4 // indirect github.com/golangci/plugin-module-register v0.1.1 // indirect @@ -92,20 +89,18 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jgautheron/goconst v1.7.1 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect - github.com/jjti/go-spancheck v0.6.1 // indirect + github.com/jjti/go-spancheck v0.6.2 // indirect github.com/julz/importas v0.1.0 // indirect github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect - github.com/kisielk/errcheck v1.7.0 // indirect + github.com/kisielk/errcheck v1.8.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/kyoh86/exportloopref v0.1.11 // indirect - github.com/lasiar/canonicalheader v1.1.1 // indirect + github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/gomoddirectives v0.2.4 // indirect github.com/ldez/tagliatelle v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect - github.com/lufeee/execinquery v1.2.1 // indirect github.com/macabu/inamedparam v0.1.3 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/maratori/testableexamples v1.0.0 // indirect @@ -113,91 +108,91 @@ require ( github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.3.7 // indirect + github.com/mgechev/revive v1.5.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moricho/tparallel v0.3.1 // indirect + github.com/moricho/tparallel v0.3.2 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.16.2 // indirect + github.com/nunnatsa/ginkgolinter v0.18.3 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterbourgon/ff/v3 v3.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/polyfloyd/go-errorlint v1.5.2 // indirect + github.com/polyfloyd/go-errorlint v1.7.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/quasilyte/go-ruleguard v0.4.2 // indirect + github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/ryancurrah/gomodguard v1.3.2 // indirect + github.com/raeperd/recvcheck v0.1.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/ryancurrah/gomodguard v1.3.5 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.26.0 // indirect - github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 // indirect + github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect + github.com/securego/gosec/v2 v2.21.4 // indirect github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sivchari/tenv v1.7.1 // indirect - github.com/sonatard/noctx v0.0.2 // indirect + github.com/sivchari/tenv v1.12.1 // indirect + github.com/sonatard/noctx v0.1.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect - github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect - github.com/tetafro/godot v1.4.16 // indirect + github.com/tetafro/godot v1.4.18 // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect - github.com/timonwong/loggercheck v0.9.4 // indirect - github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect + github.com/timonwong/loggercheck v0.10.1 // indirect + github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.1.0 // indirect github.com/ultraware/whitespace v0.1.1 // indirect - github.com/uudashr/gocognit v1.1.2 // indirect + github.com/uudashr/gocognit v1.1.3 // indirect + github.com/uudashr/iface v1.2.1 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect github.com/yuin/goldmark v1.4.13 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.12.2 // indirect - go-simpler.org/sloglint v0.7.1 // indirect - go.uber.org/automaxprocs v1.5.3 // indirect + go-simpler.org/musttag v0.13.0 // indirect + go-simpler.org/sloglint v0.7.2 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect - golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/telemetry v0.0.0-20240607193123-221703e18637 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/vuln v1.0.4 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.18.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.4.7 // indirect + honnef.co/go/tools v0.5.1 // indirect moul.io/banner v1.0.1 // indirect moul.io/motd v1.0.0 // indirect moul.io/u v1.27.0 // indirect mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect - mvdan.cc/xurls/v2 v2.5.0 // indirect ) diff --git a/misc/devdeps/go.sum b/misc/devdeps/go.sum index e19e47d0c56..fcba3fba624 100644 --- a/misc/devdeps/go.sum +++ b/misc/devdeps/go.sum @@ -37,32 +37,32 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= -github.com/Abirdcfly/dupword v0.0.14 h1:3U4ulkc8EUo+CaT105/GJ1BQwtgyj6+VaBVbAX11Ba8= -github.com/Abirdcfly/dupword v0.0.14/go.mod h1:VKDAbxdY8YbKUByLGg8EETzYSuC4crm9WwI6Y3S0cLI= -github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= -github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= -github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= -github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= -github.com/Antonboom/testifylint v1.3.1 h1:Uam4q1Q+2b6H7gvk9RQFw6jyVDdpzIirFOOrbs14eG4= -github.com/Antonboom/testifylint v1.3.1/go.mod h1:NV0hTlteCkViPW9mSR4wEMfwp+Hs1T3dY60bkvSfhpM= +github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= +github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= +github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= +github.com/Antonboom/nilnil v1.0.0 h1:n+v+B12dsE5tbAqRODXmEKfZv9j2KcTBrp+LkoM4HZk= +github.com/Antonboom/nilnil v1.0.0/go.mod h1:fDJ1FSFoLN6yoG65ANb1WihItf6qt9PJVTn/s2IrcII= +github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= +github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.2.2 h1:OrFlsDdOj9hW/oBEJBNSuH7QWf+E9WPVHw+x52bXVbk= -github.com/Crocmagnon/fatcontext v0.2.2/go.mod h1:WSn/c/+MMNiD8Pri0ahRj0o9jVpeowzavOQplBJw6u0= +github.com/Crocmagnon/fatcontext v0.5.3 h1:zCh/wjc9oyeF+Gmp+V60wetm8ph2tlsxocgg/J0hOps= +github.com/Crocmagnon/fatcontext v0.5.3/go.mod h1:XoCQYY1J+XTfyv74qLXvNw4xFunr3L1wkopIIKG7wGM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 h1:sATXp1x6/axKxz2Gjxv8MALP0bXaNRfQinEwyfMcx8c= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0/go.mod h1:Nl76DrGNJTA1KJ0LePKBw/vznBX1EHbAZX8mwjR82nI= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= -github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/alecthomas/go-check-sumtype v0.2.0 h1:Bo+e4DFf3rs7ME9w/0SU/g6nmzJaphduP8Cjiz0gbwY= +github.com/alecthomas/go-check-sumtype v0.2.0/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -70,8 +70,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg= -github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= +github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= @@ -84,16 +84,16 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= -github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= +github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= -github.com/bombsimon/wsl/v4 v4.2.1 h1:Cxg6u+XDWff75SIFFmNsqnIOgob+Q9hG6y/ioKbRFiM= -github.com/bombsimon/wsl/v4 v4.2.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= -github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= -github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= -github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= -github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= +github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw= +github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= +github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= +github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= +github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= +github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0= github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= @@ -115,16 +115,16 @@ github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+U github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/ckaznocha/intrange v0.1.2 h1:3Y4JAxcMntgb/wABQ6e8Q8leMd26JbX2790lIss9MTI= -github.com/ckaznocha/intrange v0.1.2/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= +github.com/ckaznocha/intrange v0.2.1 h1:M07spnNEQoALOJhwrImSrJLaxwuiQK+hA2DeajBlwYk= +github.com/ckaznocha/intrange v0.2.1/go.mod h1:7NEhVyf8fzZO5Ds7CRaqPEm52Ut83hsTiL5zbER/HYk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= -github.com/daixiang0/gci v0.13.4 h1:61UGkmpoAcxHM2hhNkZEf5SzwQtWJXTSws7jaPyqwlw= -github.com/daixiang0/gci v0.13.4/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= +github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -136,22 +136,22 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk= -github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= -github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU= -github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc= +github.com/ghostiam/protogetter v0.3.8 h1:LYcXbYvybUyTIxN2Mj9h6rHrDZBDwZloPoKctWrFyJY= +github.com/ghostiam/protogetter v0.3.8/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= +github.com/go-critic/go-critic v0.11.5 h1:TkDTOn5v7EEngMxu8KbuFqFR43USaaH8XRJLz1jhVYA= +github.com/go-critic/go-critic v0.11.5/go.mod h1:wu6U7ny9PiaHaZHcvMDmdysMqvDem162Rh3zWTrqk8M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -161,8 +161,10 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -185,14 +187,14 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -226,10 +228,12 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/golangci-lint v1.59.1 h1:CRRLu1JbhK5avLABFJ/OHVSQ0Ie5c4ulsOId1h3TTks= -github.com/golangci/golangci-lint v1.59.1/go.mod h1:jX5Oif4C7P0j9++YB2MMJmoNrb01NJ8ITqKWNLewThg= +github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= +github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= +github.com/golangci/golangci-lint v1.62.2 h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw= +github.com/golangci/golangci-lint v1.62.2/go.mod h1:ILWWyeFUrctpHVGMa1dg2xZPKoMUTc5OIMgW7HZr34g= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= @@ -266,11 +270,9 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= -github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= @@ -299,16 +301,12 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA= -github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= -github.com/jjti/go-spancheck v0.6.1 h1:ZK/wE5Kyi1VX3PJpUO2oEgeoI4FWOUm7Shb2Gbv5obI= -github.com/jjti/go-spancheck v0.6.1/go.mod h1:vF1QkOO159prdo6mHRxak2CpzDpHAfKiPUDP/NeRnX8= +github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= +github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -323,8 +321,8 @@ github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSX github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= -github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= +github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= +github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= @@ -345,16 +343,14 @@ github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCT github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= -github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I= -github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0= +github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= +github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= -github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -372,12 +368,13 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE= -github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA= +github.com/mgechev/revive v1.5.1 h1:hE+QPeq0/wIzJwOphdVyUJ82njdd8Khp4fUIHGZHW3M= +github.com/mgechev/revive v1.5.1/go.mod h1:lC9AhkJIBs5zwx8wkudyHrU+IJkrEKmpCmGMnIJPk4o= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -387,8 +384,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= -github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= +github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= @@ -397,14 +394,14 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= -github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= +github.com/nunnatsa/ginkgolinter v0.18.3 h1:WgS7X3zzmni3vwHSBhvSgqrRgUecN6PQUcfB0j1noDw= +github.com/nunnatsa/ginkgolinter v0.18.3/go.mod h1:BE1xyB/PNtXXG1azrvrqJW5eFH0hSRylNzFy8QHPwzs= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.17.3 h1:oJcvKpIb7/8uLpDDtnQuf18xVnwKp8DTD7DQ6gTd/MU= -github.com/onsi/ginkgo/v2 v2.17.3/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -415,8 +412,8 @@ github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0= github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24= github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= @@ -427,8 +424,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polyfloyd/go-errorlint v1.5.2 h1:SJhVik3Umsjh7mte1vE0fVZ5T1gznasQG3PV7U5xFdA= -github.com/polyfloyd/go-errorlint v1.5.2/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= +github.com/polyfloyd/go-errorlint v1.7.0 h1:Zp6lzCK4hpBDj8y8a237YK4EPrMXQWvOe3nGoH4pFrU= +github.com/polyfloyd/go-errorlint v1.7.0/go.mod h1:dGWKu85mGHnegQ2SWpEybFityCg3j7ZbwsVUxAOk9gY= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -453,8 +450,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs= -github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= @@ -463,12 +460,17 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/raeperd/recvcheck v0.1.2 h1:SjdquRsRXJc26eSonWIo8b7IMtKD3OAT2Lb5G3ZX1+4= +github.com/raeperd/recvcheck v0.1.2/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.3.2 h1:CuG27ulzEB1Gu5Dk5gP8PFxSOZ3ptSdP5iI/3IXxM18= -github.com/ryancurrah/gomodguard v1.3.2/go.mod h1:LqdemiFomEjcxOqirbQCb3JFvSxH2JUYMerTFd3sF2o= +github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= +github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= @@ -477,10 +479,10 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.26.0 h1:LONR2hNVKxRmzIrZR0PhSF3mhCAzvnr+DcUiHgREfXE= -github.com/sashamelentyev/usestdlibvars v1.26.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 h1:rnO6Zp1YMQwv8AyxzuwsVohljJgp4L0ZqiCgtACsPsc= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9/go.mod h1:dg7lPlu/xK/Ut9SedURCoZbVCR4yC7fM65DtH9/CDHs= +github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI= +github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/securego/gosec/v2 v2.21.4 h1:Le8MSj0PDmOnHJgUATjD96PaXRvCpKC+DGJvwyy0Mlk= +github.com/securego/gosec/v2 v2.21.4/go.mod h1:Jtb/MwRQfRxCXyCm1rfM1BEiiiTfUOdyzzAhlr6lUTA= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= @@ -493,18 +495,18 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= -github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= -github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= -github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= +github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= +github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= +github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= +github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -530,12 +532,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= @@ -543,22 +543,24 @@ github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.4.16 h1:4ChfhveiNLk4NveAZ9Pu2AN8QZ2nkUGFuadM9lrr5D0= -github.com/tetafro/godot v1.4.16/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tetafro/godot v1.4.18 h1:ouX3XGiziKDypbpXqShBfnNLTSjR8r3/HVzrtJ+bHlI= +github.com/tetafro/godot v1.4.18/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= -github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= -github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= -github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs= -github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= +github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4= +github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= -github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= +github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= +github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= +github.com/uudashr/iface v1.2.1 h1:vHHyzAUmWZ64Olq6NZT3vg/z1Ws56kyPdBOd5kTXDF8= +github.com/uudashr/iface v1.2.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -579,18 +581,18 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs= -go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM= -go-simpler.org/sloglint v0.7.1 h1:qlGLiqHbN5islOxjeLXoPtUdZXb669RW+BDQ+xOSNoU= -go-simpler.org/sloglint v0.7.1/go.mod h1:OlaVDRh/FKKd4X4sIMbsz8st97vomydceL146Fthh/c= +go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= +go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= +go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= +go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= @@ -617,12 +619,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f h1:WTyX8eCCyfdqiPYkRGm0MqElSfYFH3yR1+rl/mct9sA= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -652,8 +654,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -692,8 +694,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -713,8 +715,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -760,7 +762,6 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -769,10 +770,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240607193123-221703e18637 h1:3Wt8mZlbFwG8llny+t18kh7AXxyWePFycXMuVdHxnyM= -golang.org/x/telemetry v0.0.0-20240607193123-221703e18637/go.mod h1:n38mvGdgc4dA684EC4NwQwoPKSw4jyKw8/DgZHDA1Dk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -789,8 +788,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -851,18 +850,13 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34 h1:Kd+Z5Pm6uwYx3T2KEkeHMHUMZxDPb/q6b1m+zEcy62c= -golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools/gopls v0.16.1 h1:1hO/dCeUvjEYx3V0rVvCtOkwnpEpqS29paE+Jw4dcAc= -golang.org/x/tools/gopls v0.16.1/go.mod h1:Mwg8NfkbmP57kHtr/qsiU1+7kyEpuCvlPs7MH6sr988= -golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= -golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -972,8 +966,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= -honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= moul.io/banner v1.0.1 h1:+WsemGLhj2pOajw2eR5VYjLhOIqs0XhIRYchzTyMLk0= moul.io/banner v1.0.1/go.mod h1:XwvIGKkhKRKyN1vIdmR5oaKQLIkMhkMqrsHpS94QzAU= moul.io/godev v1.7.0/go.mod h1:5lgSpI1oH7xWpLl2Ew/Nsgk8DiNM6FzN9WV9+lgW8RQ= @@ -984,12 +978,10 @@ moul.io/testman v1.5.0/go.mod h1:b4/5+lMsMDJtwuh25Cr0eVJ5Y4B2lSPfkzDtfct070g= moul.io/u v1.6.0/go.mod h1:yd3/IoYRIJaZWAJV2rYHvM2EPp/Pp0zSNraB5IPX+hw= moul.io/u v1.27.0 h1:rF0p184mludn2DzL0unA8Gf/mFWMBerdqOh8cyuQYzQ= moul.io/u v1.27.0/go.mod h1:ggYDXxUjoHpfDsMPD3STqkUZTyA741PZiQhSd+7kRnA= -mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= -mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/tm2/pkg/bft/blockchain/pool.go b/tm2/pkg/bft/blockchain/pool.go index 5a82eb4d1d6..b610a0c0e7a 100644 --- a/tm2/pkg/bft/blockchain/pool.go +++ b/tm2/pkg/bft/blockchain/pool.go @@ -330,13 +330,13 @@ func (pool *BlockPool) removePeer(peerID p2p.ID) { // If no peers are left, maxPeerHeight is set to 0. func (pool *BlockPool) updateMaxPeerHeight() { - var max int64 + var maxVal int64 for _, peer := range pool.peers { - if peer.height > max { - max = peer.height + if peer.height > maxVal { + maxVal = peer.height } } - pool.maxPeerHeight = max + pool.maxPeerHeight = maxVal } // Pick an available peer with at least the given minHeight. diff --git a/tm2/pkg/bft/mempool/clist_mempool.go b/tm2/pkg/bft/mempool/clist_mempool.go index 2cad23c68e7..a2bf4301e63 100644 --- a/tm2/pkg/bft/mempool/clist_mempool.go +++ b/tm2/pkg/bft/mempool/clist_mempool.go @@ -505,12 +505,12 @@ func (mem *CListMempool) ReapMaxBytesMaxGas(maxDataBytes, maxGas int64) types.Tx return txs } -func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { +func (mem *CListMempool) ReapMaxTxs(maxVal int) types.Txs { mem.mtx.Lock() defer mem.mtx.Unlock() - if max < 0 { - max = mem.txs.Len() + if maxVal < 0 { + maxVal = mem.txs.Len() } for atomic.LoadInt32(&mem.rechecking) > 0 { @@ -518,8 +518,8 @@ func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { time.Sleep(time.Millisecond * 10) } - txs := make([]types.Tx, 0, min(mem.txs.Len(), max)) - for e := mem.txs.Front(); e != nil && len(txs) <= max; e = e.Next() { + txs := make([]types.Tx, 0, min(mem.txs.Len(), maxVal)) + for e := mem.txs.Front(); e != nil && len(txs) <= maxVal; e = e.Next() { memTx := e.Value.(*mempoolTx) txs = append(txs, memTx.tx) } diff --git a/tm2/pkg/bft/mempool/mempool.go b/tm2/pkg/bft/mempool/mempool.go index 6f822eb99ff..482d8dd2d42 100644 --- a/tm2/pkg/bft/mempool/mempool.go +++ b/tm2/pkg/bft/mempool/mempool.go @@ -30,7 +30,7 @@ type Mempool interface { // ReapMaxTxs reaps up to max transactions from the mempool. // If max is negative, there is no cap on the size of all returned // transactions (~ all available transactions). - ReapMaxTxs(max int) types.Txs + ReapMaxTxs(maxVal int) types.Txs // Lock locks the mempool. The consensus must be able to hold lock to safely update. Lock() diff --git a/tm2/pkg/bft/rpc/core/blocks.go b/tm2/pkg/bft/rpc/core/blocks.go index 53ed25ade11..9ca4e05a46f 100644 --- a/tm2/pkg/bft/rpc/core/blocks.go +++ b/tm2/pkg/bft/rpc/core/blocks.go @@ -421,11 +421,11 @@ func getHeight(currentHeight int64, heightPtr *int64) (int64, error) { return getHeightWithMin(currentHeight, heightPtr, 1) } -func getHeightWithMin(currentHeight int64, heightPtr *int64, min int64) (int64, error) { +func getHeightWithMin(currentHeight int64, heightPtr *int64, minVal int64) (int64, error) { if heightPtr != nil { height := *heightPtr - if height < min { - return 0, fmt.Errorf("height must be greater than or equal to %d", min) + if height < minVal { + return 0, fmt.Errorf("height must be greater than or equal to %d", minVal) } if height > currentHeight { return 0, fmt.Errorf("height must be less than or equal to the current blockchain height") diff --git a/tm2/pkg/bft/rpc/core/blocks_test.go b/tm2/pkg/bft/rpc/core/blocks_test.go index 550cc1542c9..dd55784ada0 100644 --- a/tm2/pkg/bft/rpc/core/blocks_test.go +++ b/tm2/pkg/bft/rpc/core/blocks_test.go @@ -11,11 +11,11 @@ func TestBlockchainInfo(t *testing.T) { t.Parallel() cases := []struct { - min, max int64 - height int64 - limit int64 - resultLength int64 - wantErr bool + minVal, maxVal int64 + height int64 + limit int64 + resultLength int64 + wantErr bool }{ // min > max {0, 0, 0, 10, 0, true}, // min set to 1 @@ -46,12 +46,12 @@ func TestBlockchainInfo(t *testing.T) { for i, c := range cases { caseString := fmt.Sprintf("test %d failed", i) - min, max, err := filterMinMax(c.height, c.min, c.max, c.limit) + minVal, maxVal, err := filterMinMax(c.height, c.minVal, c.maxVal, c.limit) if c.wantErr { require.Error(t, err, caseString) } else { require.NoError(t, err, caseString) - require.Equal(t, 1+max-min, c.resultLength, caseString) + require.Equal(t, 1+maxVal-minVal, c.resultLength, caseString) } } } @@ -62,7 +62,7 @@ func TestGetHeight(t *testing.T) { cases := []struct { currentHeight int64 heightPtr *int64 - min int64 + minVal int64 res int64 wantErr bool }{ @@ -79,7 +79,7 @@ func TestGetHeight(t *testing.T) { for i, c := range cases { caseString := fmt.Sprintf("test %d failed", i) - res, err := getHeightWithMin(c.currentHeight, c.heightPtr, c.min) + res, err := getHeightWithMin(c.currentHeight, c.heightPtr, c.minVal) if c.wantErr { require.Error(t, err, caseString) } else { diff --git a/tm2/pkg/bft/rpc/lib/server/http_server_test.go b/tm2/pkg/bft/rpc/lib/server/http_server_test.go index 6c6d9ad14d6..f089d262a71 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server_test.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server_test.go @@ -22,28 +22,28 @@ import ( func TestMaxOpenConnections(t *testing.T) { t.Parallel() - const max = 5 // max simultaneous connections + const maxVal = 5 // max simultaneous connections // Start the server. var open int32 mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if n := atomic.AddInt32(&open, 1); n > int32(max) { - t.Errorf("%d open connections, want <= %d", n, max) + if n := atomic.AddInt32(&open, 1); n > int32(maxVal) { + t.Errorf("%d open connections, want <= %d", n, maxVal) } defer atomic.AddInt32(&open, -1) time.Sleep(10 * time.Millisecond) fmt.Fprint(w, "some body") }) config := DefaultConfig() - config.MaxOpenConnections = max + config.MaxOpenConnections = maxVal l, err := Listen("tcp://127.0.0.1:0", config) require.NoError(t, err) defer l.Close() go StartHTTPServer(l, mux, log.NewTestingLogger(t), config) // Make N GET calls to the server. - attempts := max * 2 + attempts := maxVal * 2 var wg sync.WaitGroup var failed int32 for i := 0; i < attempts; i++ { diff --git a/tm2/pkg/bft/types/evidence.go b/tm2/pkg/bft/types/evidence.go index c11021e3976..85b08df6ba9 100644 --- a/tm2/pkg/bft/types/evidence.go +++ b/tm2/pkg/bft/types/evidence.go @@ -38,8 +38,8 @@ type EvidenceOverflowError struct { } // NewErrEvidenceOverflow returns a new EvidenceOverflowError where got > max. -func NewErrEvidenceOverflow(max, got int64) *EvidenceOverflowError { - return &EvidenceOverflowError{max, got} +func NewErrEvidenceOverflow(maxVal, got int64) *EvidenceOverflowError { + return &EvidenceOverflowError{maxVal, got} } // Error returns a string representation of the error. diff --git a/tm2/pkg/bft/types/genesis.go b/tm2/pkg/bft/types/genesis.go index c03f7acc09e..b927b7f8f0c 100644 --- a/tm2/pkg/bft/types/genesis.go +++ b/tm2/pkg/bft/types/genesis.go @@ -179,7 +179,7 @@ func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) { } genDoc, err := GenesisDocFromJSON(jsonBlob) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("Error reading GenesisDoc at %v", genDocFile)) + return nil, errors.Wrapf(err, "Error reading GenesisDoc at %v", genDocFile) } return genDoc, nil } diff --git a/tm2/pkg/bft/types/validator_set.go b/tm2/pkg/bft/types/validator_set.go index 80ed994ca39..c5dc5be1291 100644 --- a/tm2/pkg/bft/types/validator_set.go +++ b/tm2/pkg/bft/types/validator_set.go @@ -162,17 +162,17 @@ func computeMaxMinPriorityDiff(vals *ValidatorSet) int64 { if vals.IsNilOrEmpty() { panic("empty validator set") } - max := int64(math.MinInt64) - min := int64(math.MaxInt64) + maxVal := int64(math.MinInt64) + minVal := int64(math.MaxInt64) for _, v := range vals.Validators { - if v.ProposerPriority < min { - min = v.ProposerPriority + if v.ProposerPriority < minVal { + minVal = v.ProposerPriority } - if v.ProposerPriority > max { - max = v.ProposerPriority + if v.ProposerPriority > maxVal { + maxVal = v.ProposerPriority } } - diff := max - min + diff := maxVal - minVal if diff < 0 { return -1 * diff } else { diff --git a/tm2/pkg/bft/types/vote_set.go b/tm2/pkg/bft/types/vote_set.go index bf6200bff15..496b9b37d60 100644 --- a/tm2/pkg/bft/types/vote_set.go +++ b/tm2/pkg/bft/types/vote_set.go @@ -167,7 +167,7 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { if (vote.Height != voteSet.height) || (vote.Round != voteSet.round) || (vote.Type != voteSet.type_) { - return false, errors.Wrap(ErrVoteUnexpectedStep, "Expected %d/%d/%d, but got %d/%d/%d", + return false, errors.Wrapf(ErrVoteUnexpectedStep, "Expected %d/%d/%d, but got %d/%d/%d", voteSet.height, voteSet.round, voteSet.type_, vote.Height, vote.Round, vote.Type) } @@ -175,13 +175,13 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { // Ensure that signer is a validator. lookupAddr, val := voteSet.valSet.GetByIndex(valIndex) if val == nil { - return false, errors.Wrap(ErrVoteInvalidValidatorIndex, + return false, errors.Wrapf(ErrVoteInvalidValidatorIndex, "Cannot find validator %d in valSet of size %d", valIndex, voteSet.valSet.Size()) } // Ensure that the signer has the right address. if valAddr != lookupAddr { - return false, errors.Wrap(ErrVoteInvalidValidatorAddress, + return false, errors.Wrapf(ErrVoteInvalidValidatorAddress, "vote.ValidatorAddress (%X) does not match address (%X) for vote.ValidatorIndex (%d)\nEnsure the genesis file is correct across all validators.", valAddr, lookupAddr, valIndex) } @@ -191,12 +191,12 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { if bytes.Equal(existing.Signature, vote.Signature) { return false, nil // duplicate } - return false, errors.Wrap(ErrVoteNonDeterministicSignature, "Existing vote: %v; New vote: %v", existing, vote) + return false, errors.Wrapf(ErrVoteNonDeterministicSignature, "Existing vote: %v; New vote: %v", existing, vote) } // Check signature. if err := vote.Verify(voteSet.chainID, val.PubKey); err != nil { - return false, errors.Wrap(err, "Failed to verify vote with ChainID %s and PubKey %s", voteSet.chainID, val.PubKey) + return false, errors.Wrapf(err, "Failed to verify vote with ChainID %s and PubKey %s", voteSet.chainID, val.PubKey) } // Add vote and get conflicting vote if any. diff --git a/tm2/pkg/bft/wal/wal.go b/tm2/pkg/bft/wal/wal.go index 2424f45dfd2..09fed44b2b1 100644 --- a/tm2/pkg/bft/wal/wal.go +++ b/tm2/pkg/bft/wal/wal.go @@ -278,8 +278,8 @@ func (wal *baseWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd // NOTE: starting from the last file in the group because we're usually // searching for the last height. See replay.go - min, max := wal.group.MinIndex(), wal.group.MaxIndex() - wal.Logger.Info("Searching for height", "height", height, "min", min, "max", max) + minVal, maxVal := wal.group.MinIndex(), wal.group.MaxIndex() + wal.Logger.Info("Searching for height", "height", height, "min", minVal, "max", maxVal) var ( mode = WALSearchModeBackwards @@ -293,18 +293,18 @@ func (wal *baseWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd } OUTER_LOOP: - for min <= max { + for minVal <= maxVal { var index int // set index depending on mode. switch mode { case WALSearchModeBackwards: - index = max + backoff + idxoff - if max < index { + index = maxVal + backoff + idxoff + if maxVal < index { // (max+backoff)+ doesn't contain any height. // adjust max & backoff accordingly. idxoff = 0 - max = max + backoff - 1 + maxVal = maxVal + backoff - 1 if backoff == 0 { backoff = -1 } else { @@ -312,16 +312,16 @@ OUTER_LOOP: } continue OUTER_LOOP } - if index < min { + if index < minVal { panic("should not happen") } case WALSearchModeBinary: - index = (min+max+1)/2 + idxoff - if max < index { + index = (minVal+maxVal+1)/2 + idxoff + if maxVal < index { // ((min+max+1)/2)+ doesn't contain any height. // adjust max & binary search accordingly. idxoff = 0 - max = (min+max+1)/2 - 1 + maxVal = (minVal+maxVal+1)/2 - 1 continue OUTER_LOOP } } @@ -360,24 +360,24 @@ OUTER_LOOP: case WALSearchModeBackwards: idxoff = 0 if backoff == 0 { - max-- + maxVal-- backoff = -1 } else { - max += backoff + maxVal += backoff backoff *= 2 } // convert to binary search if backoff is too big. // max+backoff would work but max+(backoff*2) is smoother. - if max+(backoff*2) <= min { + if maxVal+(backoff*2) <= minVal { wal.Logger.Info("Converting to binary search", - "height", height, "min", min, - "max", max, "backoff", backoff) + "height", height, "min", minVal, + "max", maxVal, "backoff", backoff) backoff = 0 mode = WALSearchModeBinary } case WALSearchModeBinary: idxoff = 0 - max = (min+max+1)/2 - 1 + maxVal = (minVal+maxVal+1)/2 - 1 } dec.Close() continue OUTER_LOOP @@ -398,21 +398,21 @@ OUTER_LOOP: } else { // convert to binary search with index as new min. wal.Logger.Info("Converting to binary search with new min", - "height", height, "min", min, - "max", max, "backoff", backoff) + "height", height, "min", minVal, + "max", maxVal, "backoff", backoff) idxoff = 0 backoff = 0 - min = index + minVal = index mode = WALSearchModeBinary dec.Close() continue OUTER_LOOP } case WALSearchModeBinary: - if index < max { + if index < maxVal { // maybe in @index, but first try binary search // between @index and max. idxoff = 0 - min = index + minVal = index dec.Close() continue OUTER_LOOP } else { // index == max diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index 7e67392ebe7..0801fcfe227 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -208,11 +208,11 @@ func ExecSignAndBroadcast( return errors.Wrap(err, "broadcast tx") } if bres.CheckTx.IsErr() { - return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + return errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) } if bres.DeliverTx.IsErr() { io.Println("TX HASH: ", base64.StdEncoding.EncodeToString(bres.Hash)) - return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + return errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) } io.Println(string(bres.DeliverTx.Data)) diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index ea3d0546fa0..7f1e152c79c 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -298,7 +298,7 @@ func (kb dbKeybase) ExportPrivateKeyObject(nameOrBech32 string, passphrase strin func (kb dbKeybase) Export(nameOrBech32 string) (astr string, err error) { info, err := kb.GetByNameOrAddress(nameOrBech32) if err != nil { - return "", errors.Wrap(err, "getting info for name %s", nameOrBech32) + return "", errors.Wrapf(err, "getting info for name %s", nameOrBech32) } bz := kb.db.Get(infoKey(info.GetName())) if bz == nil { @@ -313,7 +313,7 @@ func (kb dbKeybase) Export(nameOrBech32 string) (astr string, err error) { func (kb dbKeybase) ExportPubKey(nameOrBech32 string) (astr string, err error) { info, err := kb.GetByNameOrAddress(nameOrBech32) if err != nil { - return "", errors.Wrap(err, "getting info for name %s", nameOrBech32) + return "", errors.Wrapf(err, "getting info for name %s", nameOrBech32) } return armor.ArmorPubKeyBytes(info.GetPubKey().Bytes()), nil } diff --git a/tm2/pkg/crypto/merkle/proof_key_path.go b/tm2/pkg/crypto/merkle/proof_key_path.go index 278f782833c..469a69bf2bc 100644 --- a/tm2/pkg/crypto/merkle/proof_key_path.go +++ b/tm2/pkg/crypto/merkle/proof_key_path.go @@ -96,13 +96,13 @@ func KeyPathToKeys(path string) (keys [][]byte, err error) { hexPart := part[2:] key, err := hex.DecodeString(hexPart) if err != nil { - return nil, errors.Wrap(err, "decoding hex-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding hex-encoded part #%d: /%s", i, part) } keys[i] = key } else { key, err := url.PathUnescape(part) if err != nil { - return nil, errors.Wrap(err, "decoding url-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding url-encoded part #%d: /%s", i, part) } keys[i] = []byte(key) // TODO Test this with random bytes, I'm not sure that it works for arbitrary bytes... } diff --git a/tm2/pkg/crypto/secp256k1/secp256k1.go b/tm2/pkg/crypto/secp256k1/secp256k1.go index 03f51f5ebf9..c9bb3f39c26 100644 --- a/tm2/pkg/crypto/secp256k1/secp256k1.go +++ b/tm2/pkg/crypto/secp256k1/secp256k1.go @@ -8,7 +8,7 @@ import ( "math/big" secp256k1 "github.com/btcsuite/btcd/btcec/v2" - "golang.org/x/crypto/ripemd160" + "golang.org/x/crypto/ripemd160" //nolint:gosec "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -124,8 +124,8 @@ func (pubKey PubKeySecp256k1) Address() crypto.Address { hasherSHA256.Write(pubKey[:]) // does not error sha := hasherSHA256.Sum(nil) - hasherRIPEMD160 := ripemd160.New() - hasherRIPEMD160.Write(sha) // does not error + hasherRIPEMD160 := ripemd160.New() //nolint:gosec + hasherRIPEMD160.Write(sha) // does not error return crypto.AddressFromBytes(hasherRIPEMD160.Sum(nil)) } diff --git a/tm2/pkg/errors/errors.go b/tm2/pkg/errors/errors.go index c72d9c64680..1b40c903c41 100644 --- a/tm2/pkg/errors/errors.go +++ b/tm2/pkg/errors/errors.go @@ -8,19 +8,26 @@ import ( // ---------------------------------------- // Convenience method. -func Wrap(cause interface{}, format string, args ...interface{}) Error { +func Wrap(cause interface{}, msg string) Error { if causeCmnError, ok := cause.(*cmnError); ok { //nolint:gocritic - msg := fmt.Sprintf(format, args...) return causeCmnError.Stacktrace().Trace(1, msg) } else if cause == nil { - return newCmnError(FmtError{format, args}).Stacktrace() + return newCmnError(FmtError{format: msg, args: []interface{}{}}).Stacktrace() } else { // NOTE: causeCmnError is a typed nil here. - msg := fmt.Sprintf(format, args...) return newCmnError(cause).Stacktrace().Trace(1, msg) } } +func Wrapf(cause interface{}, format string, args ...interface{}) Error { + if cause == nil { + return newCmnError(FmtError{format, args}).Stacktrace() + } + + msg := fmt.Sprintf(format, args...) + return Wrap(cause, msg) +} + func Cause(err error) error { if cerr, ok := err.(*cmnError); ok { return cerr.Data().(error) diff --git a/tm2/pkg/errors/errors_test.go b/tm2/pkg/errors/errors_test.go index 21115c21862..ab7a7086ad4 100644 --- a/tm2/pkg/errors/errors_test.go +++ b/tm2/pkg/errors/errors_test.go @@ -35,7 +35,7 @@ func TestErrorPanic(t *testing.T) { func TestWrapSomething(t *testing.T) { t.Parallel() - err := Wrap("something", "formatter%v%v", 0, 1) + err := Wrapf("something", "formatter%v%v", 0, 1) assert.Equal(t, "something", err.Data()) assert.Equal(t, "something", fmt.Sprintf("%v", err)) @@ -46,7 +46,7 @@ func TestWrapSomething(t *testing.T) { func TestWrapNothing(t *testing.T) { t.Parallel() - err := Wrap(nil, "formatter%v%v", 0, 1) + err := Wrapf(nil, "formatter%v%v", 0, 1) assert.Equal(t, FmtError{"formatter%v%v", []interface{}{0, 1}}, diff --git a/tm2/pkg/iavl/proof_range.go b/tm2/pkg/iavl/proof_range.go index ea6bce24fc0..0ce8ebdf057 100644 --- a/tm2/pkg/iavl/proof_range.go +++ b/tm2/pkg/iavl/proof_range.go @@ -273,7 +273,7 @@ func (proof *RangeProof) _computeRootHash() (rootHash []byte, treeEnd bool, err return nil, treeEnd, false, errors.Wrap(err, "recursive COMPUTEHASH call") } if !bytes.Equal(derivedRoot, lpath.Right) { - return nil, treeEnd, false, errors.Wrap(ErrInvalidRoot, "intermediate root hash %X doesn't match, got %X", lpath.Right, derivedRoot) + return nil, treeEnd, false, errors.Wrapf(ErrInvalidRoot, "intermediate root hash %X doesn't match, got %X", lpath.Right, derivedRoot) } if done { return hash, treeEnd, true, nil diff --git a/tm2/pkg/os/os.go b/tm2/pkg/os/os.go index f0e5825cb14..63601ded92a 100644 --- a/tm2/pkg/os/os.go +++ b/tm2/pkg/os/os.go @@ -33,7 +33,7 @@ func Kill() error { } func Exit(s string) { - fmt.Printf(s + "\n") + fmt.Print(s + "\n") os.Exit(1) } diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index b2de68e1ae3..317f34e496b 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -203,7 +203,7 @@ func (sw *Switch) OnStart() error { for _, reactor := range sw.reactors { err := reactor.Start() if err != nil { - return errors.Wrap(err, "failed to start %v", reactor) + return errors.Wrapf(err, "failed to start %v", reactor) } } diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index 4f36757efc0..6457b193a6b 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -658,7 +658,7 @@ func ParseCoin(coinStr string) (coin Coin, err error) { amount, err := strconv.ParseInt(amountStr, 10, 64) if err != nil { - return Coin{}, errors.Wrap(err, "failed to parse coin amount: %s", amountStr) + return Coin{}, errors.Wrapf(err, "failed to parse coin amount: %s", amountStr) } if err := validateDenom(denomStr); err != nil { diff --git a/tm2/pkg/std/gasprice.go b/tm2/pkg/std/gasprice.go index 5acc934fb71..f68ee190e41 100644 --- a/tm2/pkg/std/gasprice.go +++ b/tm2/pkg/std/gasprice.go @@ -19,11 +19,11 @@ func ParseGasPrice(gasprice string) (GasPrice, error) { } price, err := ParseCoin(parts[0]) if err != nil { - return GasPrice{}, errors.Wrap(err, "invalid gas price: %s (invalid price)", gasprice) + return GasPrice{}, errors.Wrapf(err, "invalid gas price: %s (invalid price)", gasprice) } gas, err := ParseCoin(parts[1]) if err != nil { - return GasPrice{}, errors.Wrap(err, "invalid gas price: %s (invalid gas denom)", gasprice) + return GasPrice{}, errors.Wrapf(err, "invalid gas price: %s (invalid gas denom)", gasprice) } if gas.Denom != "gas" { return GasPrice{}, errors.New("invalid gas price: %s (invalid gas denom)", gasprice) @@ -43,7 +43,7 @@ func ParseGasPrices(gasprices string) (res []GasPrice, err error) { for i, part := range parts { res[i], err = ParseGasPrice(part) if err != nil { - return nil, errors.Wrap(err, "invalid gas prices: %s", gasprices) + return nil, errors.Wrapf(err, "invalid gas prices: %s", gasprices) } } return res, nil diff --git a/tm2/pkg/store/cache/store_test.go b/tm2/pkg/store/cache/store_test.go index 1caf51ea52c..1cb1d0b60d9 100644 --- a/tm2/pkg/store/cache/store_test.go +++ b/tm2/pkg/store/cache/store_test.go @@ -359,12 +359,12 @@ func TestCacheKVMergeIteratorRandom(t *testing.T) { truth := memdb.NewMemDB() start, end := 25, 975 - max := 1000 + maxVal := 1000 setRange(st, truth, start, end) // do an op, test the iterator for i := 0; i < 2000; i++ { - doRandomOp(st, truth, max) + doRandomOp(st, truth, maxVal) assertIterateDomainCompare(t, st, truth) } } From 93a30b7268600657bec095d2d5f1ec85c6359e4a Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:51:35 +0100 Subject: [PATCH 293/345] docs: add help links on github-bot rules (#3275) Add help links for each automated github-bot rules.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- contribs/github-bot/internal/check/comment.go | 2 +- contribs/github-bot/internal/config/config.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contribs/github-bot/internal/check/comment.go b/contribs/github-bot/internal/check/comment.go index 434df8f9e76..297395ffe4b 100644 --- a/contribs/github-bot/internal/check/comment.go +++ b/contribs/github-bot/internal/check/comment.go @@ -28,7 +28,7 @@ var ( // Regex for capturing only the checkboxes. checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) // Regex used to capture markdown links. - markdownLink = regexp.MustCompile(`\[(.*)\]\(.*\)`) + markdownLink = regexp.MustCompile(`\[(.*)\]\([^)]*\)`) ) // These structures contain the necessary information to generate diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index ac1d185f759..c1d89e4cde5 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -27,12 +27,12 @@ type ManualCheck struct { func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { auto := []AutomaticCheck{ { - Description: "Maintainers must be able to edit this pull request", + Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", If: c.Always(), Then: r.MaintainerCanModify(), }, { - Description: "The pull request head branch must be up-to-date with its base", + Description: "The pull request head branch must be up-to-date with its base ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/keeping-your-pull-request-in-sync-with-the-base-branch))", If: c.Always(), Then: r.UpToDateWith(gh, r.PR_BASE), }, From f30b8816ceb28015de303bafc97a89b9cd69ac96 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:54:05 +0100 Subject: [PATCH 294/345] ci: make github-bot ignore events emitted by codecov (#3274) It's a bit of a "push and pray" since it would take too long to set up to reproduce this on my test repo/org, but I've triple-checked the documentation, the previous runs, etc., and it should be fine. Related to https://github.com/gnolang/gno/issues/3238#issuecomment-2516995329
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- .github/workflows/bot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index cbfec5730fc..15ad9c6aa04 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -33,8 +33,8 @@ jobs: # handle the parallel processing of the pull-requests define-prs-matrix: name: Define PRs matrix - # Prevent bot from retriggering itself - if: ${{ github.actor != vars.GH_BOT_LOGIN }} + # Prevent bot from retriggering itself and ignore event emitted by codecov + if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != "codecov[bot]" }} runs-on: ubuntu-latest permissions: pull-requests: read From f9c4f2aa9395c27a14e9d351ae4f2338867500ce Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Fri, 6 Dec 2024 14:24:31 +0100 Subject: [PATCH 295/345] fix: In keybase writeInfo, enforce one name for address (#3221) The `Keybase` interface was written with the assumption of a one-to-one correspondence between a key's name and address. But this needs to be enforced by the code. PR https://github.com/gnolang/gno/pull/2685 updated `writeInfo` for the case of inserting a new key (address) with the same name as an existing key with a different address. In this case, `writeInfo` uses the name to look up the existing address and deletes the address entry. This PR does the same for the other case: Inserting a key with the same address as an existing key, but a new name. In this case, `writeInfo` uses the address to look up the existing key's name, and deletes the name entry. This is not a breaking change because none of the production code expects to add a key a second time with the same address but a different name. We update the `Keybase` doc comments to this effect. However, some of the tests in keybase_test.go make this assumption, so this PR fixes them: * `TestSignVerify` creates a key with name n2 and exports its public key. It wants to re-import the public key with name n3 and get the public key to check that it is the same public key as n2. We change this test to re-import the public key into a fresh in-memory key store. * `TestExportImportPubKey` creates a key with name "john", exports its public key, then re-imports it with the new name "john-pubkey-only" (but the same address). The current test checks that the key can still be fetched under the old name "john". But this breaks the one-to-one correspondence of key name and address. So the test is changed to confirm that the key with the old name is replaced by the new name.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
Signed-off-by: Jeff Thompson --- tm2/pkg/crypto/keys/keybase.go | 9 ++++++++- tm2/pkg/crypto/keys/keybase_test.go | 15 ++++++++------- tm2/pkg/crypto/keys/types.go | 3 +++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 7f1e152c79c..23c4237151a 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -523,10 +523,17 @@ func (kb dbKeybase) writeInfo(name string, info Info) error { kb.db.DeleteSync(addrKey(oldInfo.GetAddress())) } + addressKey := addrKey(info.GetAddress()) + nameKeyForAddress := kb.db.Get(addressKey) + if len(nameKeyForAddress) > 0 { + // Enforce 1-to-1 name to address. Remove the info by the old name with the same address + kb.db.DeleteSync(nameKeyForAddress) + } + serializedInfo := writeInfo(info) kb.db.SetSync(key, serializedInfo) // store a pointer to the infokey by address for fast lookup - kb.db.SetSync(addrKey(info.GetAddress()), key) + kb.db.SetSync(addressKey, key) return nil } diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index bfb21b46fad..25306e62635 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -149,11 +149,12 @@ func TestSignVerify(t *testing.T) { i2, err := cstore.CreateAccount(n2, mn2, bip39Passphrase, p2, 0, 0) require.Nil(t, err) - // Import a public key + // Import a public key into a new store armor, err := cstore.ExportPubKey(n2) require.Nil(t, err) - cstore.ImportPubKey(n3, armor) - i3, err := cstore.GetByName(n3) + cstore2 := NewInMemory() + cstore2.ImportPubKey(n3, armor) + i3, err := cstore2.GetByName(n3) require.NoError(t, err) require.Equal(t, i3.GetName(), n3) @@ -174,6 +175,7 @@ func TestSignVerify(t *testing.T) { s21, pub2, err := cstore.Sign(n2, p2, d1) require.Nil(t, err) require.Equal(t, i2.GetPubKey(), pub2) + require.Equal(t, i3.GetPubKey(), pub2) s22, pub2, err := cstore.Sign(n2, p2, d2) require.Nil(t, err) @@ -282,11 +284,10 @@ func TestExportImportPubKey(t *testing.T) { require.NoError(t, err) // Compare the public keys require.True(t, john.GetPubKey().Equals(john2.GetPubKey())) - // Ensure the original key hasn't changed - john, err = cstore.GetByName("john") + // Ensure that storing with the address of "john-pubkey-only" removed the entry for "john" + has, err := cstore.HasByName("john") require.NoError(t, err) - require.Equal(t, john.GetPubKey().Address(), addr) - require.Equal(t, john.GetName(), "john") + require.False(t, has) // Ensure keys cannot be overwritten err = cstore.ImportPubKey("john-pubkey-only", armor) diff --git a/tm2/pkg/crypto/keys/types.go b/tm2/pkg/crypto/keys/types.go index 3865951168e..bdaf39caa54 100644 --- a/tm2/pkg/crypto/keys/types.go +++ b/tm2/pkg/crypto/keys/types.go @@ -27,10 +27,12 @@ type Keybase interface { // CreateAccount creates an account based using the BIP44 path (44'/118'/{account}'/0/{index} // Encrypt the key to disk using encryptPasswd. + // If an account exists with the same address but a different name, it is replaced by the new name. // See https://github.com/tendermint/classic/sdk/issues/2095 CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) // Like CreateAccount but from general bip44 params. + // If an account exists with the same address but a different name, it is replaced by the new name. CreateAccountBip44(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (Info, error) // CreateLedger creates, stores, and returns a new Ledger key reference @@ -43,6 +45,7 @@ type Keybase interface { CreateMulti(name string, pubkey crypto.PubKey) (info Info, err error) // The following operations will *only* work on locally-stored keys + // In all import operations, if an account exists with the same address but a different name, it is replaced by the new name. Rotate(name, oldpass string, getNewpass func() (string, error)) error Import(name string, armor string) (err error) ImportPrivKey(name, armor, decryptPassphrase, encryptPassphrase string) error From 6743b8de3b0ff4c54eb6395d859f7e8a67230fe5 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:45:28 +0100 Subject: [PATCH 296/345] ci: fix github-bot workflow (#3286) Fix the github-bot workflow by replacing double quotes by simple ones, see https://github.com/gnolang/gno/actions/runs/12201415150/workflow#L37
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- .github/workflows/bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 15ad9c6aa04..644540c1aaf 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -34,7 +34,7 @@ jobs: define-prs-matrix: name: Define PRs matrix # Prevent bot from retriggering itself and ignore event emitted by codecov - if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != "codecov[bot]" }} + if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != 'codecov[bot]' }} runs-on: ubuntu-latest permissions: pull-requests: read From 3691956600eb1c7fe17a4d4e02c23a5561ee81a0 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:59:27 +0100 Subject: [PATCH 297/345] chore: refactor txlink in order to extend it (#3289) Signed-off-by: moul <94029+moul@users.noreply.github.com>
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/helplink/helplink.gno | 2 +- examples/gno.land/p/moul/txlink/txlink.gno | 12 ++++++------ examples/gno.land/p/moul/txlink/txlink_test.gno | 4 ++-- examples/gno.land/r/demo/boards/board.gno | 2 +- examples/gno.land/r/demo/boards/post.gno | 6 +++--- examples/gno.land/r/docs/adder/adder.gno | 2 +- examples/gno.land/r/gov/dao/v2/render.gno | 6 +++--- examples/gno.land/r/leon/hof/render.gno | 10 +++++----- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/gno.land/p/moul/helplink/helplink.gno b/examples/gno.land/p/moul/helplink/helplink.gno index 0c18f5d0360..14b44622a1e 100644 --- a/examples/gno.land/p/moul/helplink/helplink.gno +++ b/examples/gno.land/p/moul/helplink/helplink.gno @@ -70,7 +70,7 @@ func (r Realm) Func(title string, fn string, args ...string) string { // arguments. func (r Realm) FuncURL(fn string, args ...string) string { tlr := txlink.Realm(r) - return tlr.URL(fn, args...) + return tlr.Call(fn, args...) } // Home returns the base help URL for the specified realm. diff --git a/examples/gno.land/p/moul/txlink/txlink.gno b/examples/gno.land/p/moul/txlink/txlink.gno index 4705161578c..65edda6911e 100644 --- a/examples/gno.land/p/moul/txlink/txlink.gno +++ b/examples/gno.land/p/moul/txlink/txlink.gno @@ -5,7 +5,7 @@ // flexible arguments, allowing users to build dynamic links that integrate // seamlessly with various Gno clients. // -// The primary function, URL, is designed to produce markdown links for +// The primary function, Call, is designed to produce markdown links for // transaction functions in the current "relative realm". By specifying a custom // Realm, you can generate links that either use the current realm path or a // fully qualified path for another realm. @@ -21,10 +21,10 @@ import ( const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) -// URL returns a URL for the specified function with optional key-value +// Call returns a URL for the specified function with optional key-value // arguments, for the current realm. -func URL(fn string, args ...string) string { - return Realm("").URL(fn, args...) +func Call(fn string, args ...string) string { + return Realm("").Call(fn, args...) } // Realm represents a specific realm for generating tx links. @@ -48,9 +48,9 @@ func (r Realm) prefix() string { return "https://" + string(r) } -// URL returns a URL for the specified function with optional key-value +// Call returns a URL for the specified function with optional key-value // arguments. -func (r Realm) URL(fn string, args ...string) string { +func (r Realm) Call(fn string, args ...string) string { // Start with the base query url := r.prefix() + "$help&func=" + fn diff --git a/examples/gno.land/p/moul/txlink/txlink_test.gno b/examples/gno.land/p/moul/txlink/txlink_test.gno index a598a06b154..61b532270d4 100644 --- a/examples/gno.land/p/moul/txlink/txlink_test.gno +++ b/examples/gno.land/p/moul/txlink/txlink_test.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/urequire" ) -func TestURL(t *testing.T) { +func TestCall(t *testing.T) { tests := []struct { fn string args []string @@ -30,7 +30,7 @@ func TestURL(t *testing.T) { for _, tt := range tests { title := tt.fn t.Run(title, func(t *testing.T) { - got := tt.realm.URL(tt.fn, tt.args...) + got := tt.realm.Call(tt.fn, tt.args...) urequire.Equal(t, tt.want, got) }) } diff --git a/examples/gno.land/r/demo/boards/board.gno b/examples/gno.land/r/demo/boards/board.gno index 79b27da84b2..9b9fb730c68 100644 --- a/examples/gno.land/r/demo/boards/board.gno +++ b/examples/gno.land/r/demo/boards/board.gno @@ -135,5 +135,5 @@ func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string } func (board *Board) GetPostFormURL() string { - return txlink.URL("CreateThread", "bid", board.id.String()) + return txlink.Call("CreateThread", "bid", board.id.String()) } diff --git a/examples/gno.land/r/demo/boards/post.gno b/examples/gno.land/r/demo/boards/post.gno index 95d4b2977ba..c6e23cd59d0 100644 --- a/examples/gno.land/r/demo/boards/post.gno +++ b/examples/gno.land/r/demo/boards/post.gno @@ -156,7 +156,7 @@ func (post *Post) GetURL() string { } func (post *Post) GetReplyFormURL() string { - return txlink.URL("CreateReply", + return txlink.Call("CreateReply", "bid", post.board.id.String(), "threadid", post.threadID.String(), "postid", post.id.String(), @@ -164,14 +164,14 @@ func (post *Post) GetReplyFormURL() string { } func (post *Post) GetRepostFormURL() string { - return txlink.URL("CreateRepost", + return txlink.Call("CreateRepost", "bid", post.board.id.String(), "postid", post.id.String(), ) } func (post *Post) GetDeleteFormURL() string { - return txlink.URL("DeletePost", + return txlink.Call("DeletePost", "bid", post.board.id.String(), "threadid", post.threadID.String(), "postid", post.id.String(), diff --git a/examples/gno.land/r/docs/adder/adder.gno b/examples/gno.land/r/docs/adder/adder.gno index cd96d241692..33e971c7c0b 100644 --- a/examples/gno.land/r/docs/adder/adder.gno +++ b/examples/gno.land/r/docs/adder/adder.gno @@ -27,7 +27,7 @@ func Render(path string) string { result += "Last Updated: " + formatTimestamp(lastUpdate) + "\n\n" // Generate a transaction link to call Add with 42 as the default parameter - txLink := txlink.URL("Add", "n", "42") + txLink := txlink.Call("Add", "n", "42") result += "[Increase Number](" + txLink + ")\n" return result diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno index 4cca397e851..57b7b601523 100644 --- a/examples/gno.land/r/gov/dao/v2/render.gno +++ b/examples/gno.land/r/gov/dao/v2/render.gno @@ -111,9 +111,9 @@ func renderActionBar(p dao.Proposal, idx int) string { out += "### Actions\n\n" if p.Status() == dao.Active { out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]", - txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"), - txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"), - txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"), ) } else { out += "The voting period for this proposal is over." diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index 6b06ef04051..b4d51d03362 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -64,12 +64,12 @@ func (i Item) Render(dashboard bool) string { out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n", - i.upvote.Size(), txlink.URL("Upvote", "pkgpath", i.pkgpath), - i.downvote.Size(), txlink.URL("Downvote", "pkgpath", i.pkgpath), + i.upvote.Size(), txlink.Call("Upvote", "pkgpath", i.pkgpath), + i.downvote.Size(), txlink.Call("Downvote", "pkgpath", i.pkgpath), ) if dashboard { - out += ufmt.Sprintf("[Delete](%s)", txlink.URL("Delete", "pkgpath", i.pkgpath)) + out += ufmt.Sprintf("[Delete](%s)", txlink.Call("Delete", "pkgpath", i.pkgpath)) } return out @@ -83,9 +83,9 @@ func renderDashboard() string { out += ufmt.Sprintf("Exhibition admin: %s\n\n", owner.Owner().String()) if !exhibition.IsPaused() { - out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.URL("Pause")) + out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.Call("Pause")) } else { - out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.URL("Unpause")) + out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.Call("Unpause")) } out += "---\n\n" From ac899c850d7b234e83d2dd56c3eb7ee1c847c8a0 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 6 Dec 2024 22:13:24 +0100 Subject: [PATCH 298/345] feat: support custom VM domain (#2911) Introducing the concept of "ChainDomain," a local primary domain for packages. The next step, which will be another PR, is to ensure that we can launch a gnoland/gnodev instance while importing a local folder or using a genesis to preload packages from other domains. This will allow users to add packages only to the primary domain while accessing packages from multiple domains. The result will be a preview of the upcoming IBC era, where a single chain can add packages only to its domain but can fetch missing dependencies from other registered zones. - [x] gnovm unaware of gno.land, just accepting valid domains - [x] vmkeeper initialized with a domain - [x] Stdlib to know the current primary domain + new std.ChainDomain - [x] new unit tests around custom domains Depends on #2910 Depends on https://github.com/gnolang/gno/pull/3003 Blocks a new PR that will add multidomain support. Related with https://github.com/gnolang/gno/issues/2904#issuecomment-2395011435 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: n0izn0iz Co-authored-by: Mikael VALLENET Co-authored-by: Morgan --- contribs/gnodev/cmd/gnodev/main.go | 25 +++++++---- contribs/gnodev/cmd/gnodev/setup_node.go | 2 +- contribs/gnodev/pkg/dev/node.go | 13 +++--- contribs/gnodev/pkg/dev/node_test.go | 6 +-- .../cmd/gnoland/testdata/addpkg_domain.txtar | 15 +++++++ .../cmd/gnoland/testdata/genesis_params.txtar | 18 +++++++- .../cmd/gnoland/testdata/simulate_gas.txtar | 4 +- gno.land/genesis/genesis_params.toml | 2 +- gno.land/pkg/gnoland/node_inmemory.go | 9 +++- gno.land/pkg/sdk/vm/gas_test.go | 4 +- gno.land/pkg/sdk/vm/keeper.go | 45 ++++++++++++------- gno.land/pkg/sdk/vm/keeper_test.go | 39 +++++++++++++++- gno.land/pkg/sdk/vm/msgs.go | 4 +- gno.land/pkg/sdk/vm/params.go | 20 +++++++++ gnovm/cmd/gno/test.go | 2 +- gnovm/memfile.go | 2 +- gnovm/memfile_test.go | 4 +- gnovm/pkg/gnolang/helpers.go | 30 ++++++------- gnovm/pkg/test/test.go | 1 + gnovm/stdlibs/generated.go | 20 +++++++++ gnovm/stdlibs/std/context.go | 1 + gnovm/stdlibs/std/native.gno | 5 ++- gnovm/stdlibs/std/native.go | 4 ++ gnovm/tests/files/std5.gno | 4 +- gnovm/tests/files/std8.gno | 4 +- gnovm/tests/files/zrealm_natbind1_stdlibs.gno | 16 +++++++ 26 files changed, 231 insertions(+), 68 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/addpkg_domain.txtar create mode 100644 gno.land/pkg/sdk/vm/params.go create mode 100644 gnovm/tests/files/zrealm_natbind1_stdlibs.gno diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 2c694b608bb..c9d6487d753 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -61,18 +61,20 @@ type devCfg struct { webRemoteHelperAddr string // Node Configuration - minimal bool - verbose bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - serverMode bool - unsafeAPI bool + minimal bool + verbose bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + chainDomain string + serverMode bool + unsafeAPI bool } var defaultDevOptions = &devCfg{ chainId: "dev", + chainDomain: "gno.land", maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", @@ -203,6 +205,13 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "set node ChainID", ) + fs.StringVar( + &c.chainDomain, + "chain-domain", + defaultDevOptions.chainDomain, + "set node ChainDomain", + ) + fs.BoolVar( &c.noWatch, "no-watch", diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index a2b1970d0ef..eaeb89b7e95 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -57,7 +57,7 @@ func setupDevNodeConfig( balances gnoland.Balances, pkgspath []gnodev.PackagePath, ) *gnodev.NodeConfig { - config := gnodev.DefaultNodeConfig(cfg.root) + config := gnodev.DefaultNodeConfig(cfg.root, cfg.chainDomain) config.Logger = logger config.Emitter = emitter diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index e0ed64aad36..0502c03c86f 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -43,9 +43,10 @@ type NodeConfig struct { NoReplay bool MaxGasPerBlock int64 ChainID string + ChainDomain string } -func DefaultNodeConfig(rootdir string) *NodeConfig { +func DefaultNodeConfig(rootdir, domain string) *NodeConfig { tmc := gnoland.NewDefaultTMConfig(rootdir) tmc.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 tmc.Consensus.WALDisabled = true @@ -65,6 +66,7 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { DefaultDeployer: defaultDeployer, BalancesList: balances, ChainID: tmc.ChainID(), + ChainDomain: domain, TMConfig: tmc, SkipFailingGenesisTxs: true, MaxGasPerBlock: 10_000_000_000, @@ -487,7 +489,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) } // Setup node config - nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) + nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, n.config.ChainDomain, genesis) nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). nodeConfig.CacheStdlibLoad = true @@ -566,10 +568,10 @@ func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result return } -func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { +func newNodeConfig(tmc *tmcfg.Config, chainid, chaindomain string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { // Create Mocked Identity pv := gnoland.NewMockedPrivValidator() - genesis := gnoland.NewDefaultGenesisConfig(chainid) + genesis := gnoland.NewDefaultGenesisConfig(chainid, chaindomain) genesis.AppState = appstate // Add self as validator @@ -583,10 +585,11 @@ func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesi }, } - return &gnoland.InMemoryNodeConfig{ + cfg := &gnoland.InMemoryNodeConfig{ PrivValidator: pv, TMConfig: tmc, Genesis: genesis, VMOutput: os.Stdout, } + return cfg } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index e05e5a996fa..4a4acc232b9 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -38,7 +38,7 @@ func TestNewNode_NoPackages(t *testing.T) { logger := log.NewTestingLogger(t) // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.Logger = logger node, err := NewDevNode(ctx, cfg) require.NoError(t, err) @@ -66,7 +66,7 @@ func Render(_ string) string { return "foo" } logger := log.NewTestingLogger(t) // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.PackagesPathList = []PackagePath{pkgpath} cfg.Logger = logger node, err := NewDevNode(ctx, cfg) @@ -475,7 +475,7 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { } func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.PackagesPathList = pkgslist return cfg } diff --git a/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar b/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar new file mode 100644 index 00000000000..25e4fe0d3a3 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar @@ -0,0 +1,15 @@ +gnoland start + +# addpkg with anotherdomain.land +! gnokey maketx addpkg -pkgdir $WORK -pkgpath anotherdomain.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout 'TX HASH:' +stderr 'invalid package path' +stderr 'invalid domain: anotherdomain.land/r/foobar/bar' + +# addpkg with gno.land +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout 'OK!' + +-- bar.gno -- +package bar +func Render(path string) string { return "hello" } diff --git a/gno.land/cmd/gnoland/testdata/genesis_params.txtar b/gno.land/cmd/gnoland/testdata/genesis_params.txtar index 43ecd8ccacb..d09ededf78a 100644 --- a/gno.land/cmd/gnoland/testdata/genesis_params.txtar +++ b/gno.land/cmd/gnoland/testdata/genesis_params.txtar @@ -1,14 +1,28 @@ -# test for https://github.com/gnolang/gno/pull/3003 +# Test for #3003, #2911. gnoland start +# Query and validate official parameters. +# These parameters should ideally be tested in a txtar format to ensure that a +# default initialization of "gnoland" provides the expected default values. + +# Verify the default chain domain parameter for Gno.land +gnokey query params/vm/gno.land/r/sys/params.vm.chain_domain.string +stdout 'data: "gno.land"$' + +# Test custom parameters to confirm they return the expected values and types. + gnokey query params/vm/gno.land/r/sys/params.test.foo.string stdout 'data: "bar"$' + gnokey query params/vm/gno.land/r/sys/params.test.foo.int64 stdout 'data: "-1337"' + gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64 stdout 'data: "42"' + gnokey query params/vm/gno.land/r/sys/params.test.foo.bool stdout 'data: true' -# XXX: bytes + +# TODO: Consider adding a test case for a byte array parameter diff --git a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar index cd58b4ccc8f..9d3c8abe69f 100644 --- a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar +++ b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar @@ -6,11 +6,11 @@ gnoland start # simulate only gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 -stdout 'GAS USED: 50299' +stdout 'GAS USED: 51299' # simulate skip gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 -stdout 'GAS USED: 50299' # same as simulate only +stdout 'GAS USED: 51299' # same as simulate only -- package/package.gno -- diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml index 5f4d9c5015c..fb080024624 100644 --- a/gno.land/genesis/genesis_params.toml +++ b/gno.land/genesis/genesis_params.toml @@ -8,7 +8,7 @@ ## gnovm ["gno.land/r/sys/params.vm"] - # TODO: chain_domain.string = "gno.land" + chain_domain.string = "gno.land" # TODO: max_gas.int64 = 100_000_000 # TODO: chain_tz.string = "UTC" # TODO: default_storage_allowance.string = "" diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 426a8c780c7..f42166411c8 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -36,7 +36,11 @@ func NewMockedPrivValidator() bft.PrivValidator { } // NewDefaultGenesisConfig creates a default configuration for an in-memory node. -func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { +func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc { + // custom chain domain + var domainParam Param + _ = domainParam.Parse("gno.land/r/sys/params.vm.chain_domain.string=" + chaindomain) + return &bft.GenesisDoc{ GenesisTime: time.Now(), ChainID: chainid, @@ -46,6 +50,9 @@ func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { AppState: &GnoGenesisState{ Balances: []Balance{}, Txs: []TxWithMetadata{}, + Params: []Param{ + domainParam, + }, }, } } diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 3a11d97c235..677d86a0331 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -75,7 +75,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(92825), gasDeliver) + assert.Equal(t, int64(93825), gasDeliver) } // Enough gas for a failed transaction. @@ -95,7 +95,7 @@ func TestAddPkgDeliverTxFailed(t *testing.T) { gasDeliver := gctx.GasMeter().GasConsumed() assert.False(t, res.IsOK()) - assert.Equal(t, int64(2231), gasDeliver) + assert.Equal(t, int64(3231), gasDeliver) } // Not enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 52eff20ea95..e4f7a8543a7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -85,6 +85,7 @@ func NewVMKeeper( bank: bank, prmk: prmk, } + return vmk } @@ -192,6 +193,7 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { } m := gno.NewMachineWithOptions(gno.MachineOptions{ + // XXX: gno.land, vm.domain, other? PkgPath: "gno.land/r/stdlibs/" + pkgPath, // PkgPath: pkgPath, XXX why? Store: store, @@ -226,20 +228,22 @@ func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore } // Namespace can be either a user or crypto address. -var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) - -const sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" +var reNamespace = regexp.MustCompile(`^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/(?:r|p)/([\.~_a-zA-Z0-9]+)`) // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { - var sysUsersPkg string - vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) + sysUsersPkg := vm.getSysUsersPkgParam(ctx) if sysUsersPkg == "" { return nil } + chainDomain := vm.getChainDomainParam(ctx) store := vm.getGnoTransactionStore(ctx) + if !strings.HasPrefix(pkgPath, chainDomain+"/") { + return ErrInvalidPkgPath(pkgPath) // no match + } + match := reNamespace.FindStringSubmatch(pkgPath) switch len(match) { case 0: @@ -248,9 +252,6 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add default: panic("invalid pattern while matching pkgpath") } - if len(match) != 2 { - return ErrInvalidPkgPath(pkgPath) - } username := match[1] // if `sysUsersPkg` does not exist -> skip validation. @@ -263,6 +264,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add pkgAddr := gno.DerivePkgAddr(pkgPath) msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: creator.Bech32(), @@ -320,6 +322,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { memPkg := msg.Package deposit := msg.Deposit gnostore := vm.getGnoTransactionStore(ctx) + chainDomain := vm.getChainDomainParam(ctx) // Validate arguments. if creator.IsZero() { @@ -332,6 +335,9 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { if err := msg.Package.Validate(); err != nil { return ErrInvalidPkgPath(err.Error()) } + if !strings.HasPrefix(pkgPath, chainDomain+"/") { + return ErrInvalidPkgPath("invalid domain: " + pkgPath) + } if pv := gnostore.GetPackage(pkgPath, false); pv != nil { return ErrPkgAlreadyExists("package already exists: " + pkgPath) } @@ -363,6 +369,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { // Parse and run the files, construct *PV. msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: creator.Bech32(), @@ -461,8 +468,10 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { // Make context. // NOTE: if this is too expensive, // could it be safely partially memoized? + chainDomain := vm.getChainDomainParam(ctx) msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: caller.Bech32(), @@ -531,11 +540,12 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { gnostore := vm.getGnoTransactionStore(ctx) send := msg.Send memPkg := msg.Package + chainDomain := vm.getChainDomainParam(ctx) // coerce path to right one. // the path in the message must be "" or the following path. // this is already checked in MsgRun.ValidateBasic - memPkg.Path = "gno.land/r/" + msg.Caller.String() + "/run" + memPkg.Path = chainDomain + "/r/" + msg.Caller.String() + "/run" // Validate arguments. callerAcc := vm.acck.GetAccount(ctx, caller) @@ -561,6 +571,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { // Parse and run the files, construct *PV. msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: caller.Bech32(), @@ -722,10 +733,12 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res return "", err } // Construct new machine. + chainDomain := vm.getChainDomainParam(ctx) msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), + ChainID: ctx.ChainID(), + ChainDomain: chainDomain, + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), // OrigCaller: caller, // OrigSend: send, // OrigSendSpent: nil, @@ -788,10 +801,12 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string return "", err } // Construct new machine. + chainDomain := vm.getChainDomainParam(ctx) msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), + ChainID: ctx.ChainID(), + ChainDomain: chainDomain, + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), // OrigCaller: caller, // OrigSend: jsend, // OrigSendSpent: nil, diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 9afbb3de551..f8144988c44 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -22,7 +22,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/types" ) -var coinsString = ugnot.ValueString(10000000) +var coinsString = ugnot.ValueString(10_000_000) func TestVMKeeperAddPackage(t *testing.T) { env := setupTestEnv() @@ -68,6 +68,43 @@ func Echo() string { return "hello world" } assert.Equal(t, expected, memFile.Body) } +func TestVMKeeperAddPackage_InvalidDomain(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) + + // Create test package. + files := []*gnovm.MemFile{ + { + Name: "test.gno", + Body: `package test +func Echo() string {return "hello world"}`, + }, + } + pkgPath := "anotherdomain.land/r/test" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + + err := env.vmk.AddPackage(ctx, msg1) + + assert.Error(t, err, ErrInvalidPkgPath("invalid domain: anotherdomain.land/r/test")) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + + err = env.vmk.AddPackage(ctx, msg1) + assert.Error(t, err, ErrInvalidPkgPath("invalid domain: anotherdomain.land/r/test")) + + // added package is formatted + store := env.vmk.getGnoTransactionStore(ctx) + memFile := store.GetMemFile("gno.land/r/test", "test.gno") + assert.Nil(t, memFile) +} + // Sending total send amount succeeds. func TestVMKeeperOrigSend1(t *testing.T) { env := setupTestEnv() diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index d5b82067a98..1ce648acb19 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -186,8 +186,8 @@ func (msg MsgRun) ValidateBasic() error { } // Force memPkg path to the reserved run path. - wantPath := "gno.land/r/" + msg.Caller.String() + "/run" - if path := msg.Package.Path; path != "" && path != wantPath { + wantSuffix := "/r/" + msg.Caller.String() + "/run" + if path := msg.Package.Path; path != "" && !strings.HasSuffix(path, wantSuffix) { return ErrInvalidPkgPath(fmt.Sprintf("invalid pkgpath for MsgRun: %q", path)) } diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go new file mode 100644 index 00000000000..248fb8a81fb --- /dev/null +++ b/gno.land/pkg/sdk/vm/params.go @@ -0,0 +1,20 @@ +package vm + +import "github.com/gnolang/gno/tm2/pkg/sdk" + +const ( + sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" + chainDomainParamPath = "gno.land/r/sys/params.chain_domain.string" +) + +func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string { + chainDomain := "gno.land" // default + vm.prmk.GetString(ctx, chainDomainParamPath, &chainDomain) + return chainDomain +} + +func (vm *VMKeeper) getSysUsersPkgParam(ctx sdk.Context) string { + var sysUsersPkg string + vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) + return sysUsersPkg +} diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 04a3808718d..511a704dd7d 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -205,7 +205,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { if gnoPkgPath == "" { // unable to read pkgPath from gno.mod, generate a random realm path io.ErrPrintfln("--- WARNING: unable to read package path from gno.mod or gno root directory; try creating a gno.mod file") - gnoPkgPath = gno.RealmPathPrefix + strings.ToLower(random.RandStr(8)) + gnoPkgPath = "gno.land/r/" + strings.ToLower(random.RandStr(8)) // XXX: gno.land hardcoded for convenience. } } diff --git a/gnovm/memfile.go b/gnovm/memfile.go index a37bba6ef3d..6988c893dd7 100644 --- a/gnovm/memfile.go +++ b/gnovm/memfile.go @@ -41,7 +41,7 @@ const pathLengthLimit = 256 var ( rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) - rePkgOrRlmPath = regexp.MustCompile(`^gno\.land\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`) + rePkgOrRlmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`) reFileName = regexp.MustCompile(`^([a-zA-Z0-9_]*\.[a-z0-9_\.]*|LICENSE|README)$`) ) diff --git a/gnovm/memfile_test.go b/gnovm/memfile_test.go index c93c251b0e7..5ef70e9e868 100644 --- a/gnovm/memfile_test.go +++ b/gnovm/memfile_test.go @@ -158,13 +158,13 @@ func TestMemPackage_Validate(t *testing.T) { "invalid package/realm path", }, { - "Invalid path", + "Custom domain", &MemPackage{ Name: "hey", Path: "github.com/p/path/path", Files: []*MemFile{{Name: "a.gno"}}, }, - "invalid package/realm path", + "", }, { "Special character", diff --git a/gnovm/pkg/gnolang/helpers.go b/gnovm/pkg/gnolang/helpers.go index d3a8485ee17..ddc1fd2fa55 100644 --- a/gnovm/pkg/gnolang/helpers.go +++ b/gnovm/pkg/gnolang/helpers.go @@ -10,22 +10,21 @@ import ( // ---------------------------------------- // Functions centralizing definitions -// RealmPathPrefix is the prefix used to identify pkgpaths which are meant to -// be realms and as such to have their state persisted. This is used by [IsRealmPath]. -const ( - RealmPathPrefix = "gno.land/r/" - PackagePathPrefix = "gno.land/p/" +// ReRealmPath and RePackagePath are the regexes used to identify pkgpaths which are meant to +// be realms with persisted states and pure packages. +var ( + ReRealmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_/]+`) + RePackagePath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/p/[a-z0-9_/]+`) ) // ReGnoRunPath is the path used for realms executed in maketx run. -// These are not considered realms, as an exception to the RealmPathPrefix rule. -var ReGnoRunPath = regexp.MustCompile(`^gno\.land/r/g[a-z0-9]+/run$`) +// These are not considered realms, as an exception to the ReRealmPathPrefix rule. +var ReGnoRunPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/g[a-z0-9]+/run$`) // IsRealmPath determines whether the given pkgpath is for a realm, and as such // should persist the global state. func IsRealmPath(pkgPath string) bool { - return strings.HasPrefix(pkgPath, RealmPathPrefix) && - // MsgRun pkgPath aren't realms + return ReRealmPath.MatchString(pkgPath) && !ReGnoRunPath.MatchString(pkgPath) } @@ -33,16 +32,17 @@ func IsRealmPath(pkgPath string) bool { // It only considers "pure" those starting with gno.land/p/, so it returns false for // stdlib packages and MsgRun paths. func IsPurePackagePath(pkgPath string) bool { - return strings.HasPrefix(pkgPath, PackagePathPrefix) + return RePackagePath.MatchString(pkgPath) } // IsStdlib determines whether s is a pkgpath for a standard library. func IsStdlib(s string) bool { - // NOTE(morgan): this is likely to change in the future as we add support for - // IBC/ICS and we allow import paths to other chains. It might be good to - // (eventually) follow the same rule as Go, which is: does the first - // element of the import path contain a dot? - return !strings.HasPrefix(s, "gno.land/") + idx := strings.IndexByte(s, '/') + if idx < 0 { + // If no '/' is found, consider the whole string + return strings.IndexByte(s, '.') < 0 + } + return strings.IndexByte(s[:idx+1], '.') < 0 } // ---------------------------------------- diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 5de37a68405..3ea3d4bc9bd 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -53,6 +53,7 @@ func Context(pkgPath string, send std.Coins) *teststd.TestExecContext { } ctx := stdlibs.ExecContext{ ChainID: "dev", + ChainDomain: "tests.gno.land", Height: DefaultHeight, Timestamp: DefaultTimestamp, OrigCaller: DefaultCaller, diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index a2d82b0bc60..67b492a34b2 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -468,6 +468,26 @@ var nativeFuncs = [...]NativeFunc{ )) }, }, + { + "std", + "GetChainDomain", + []gno.FieldTypeExpr{}, + []gno.FieldTypeExpr{ + {Name: gno.N("r0"), Type: gno.X("string")}, + }, + true, + func(m *gno.Machine) { + r0 := libs_std.GetChainDomain( + m, + ) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, { "std", "GetHeight", diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index 01e763ab82e..a8ef500c346 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -9,6 +9,7 @@ import ( type ExecContext struct { ChainID string + ChainDomain string Height int64 Timestamp int64 // seconds TimestampNano int64 // nanoseconds, only used for testing. diff --git a/gnovm/stdlibs/std/native.gno b/gnovm/stdlibs/std/native.gno index 5421e231de2..0dcde1148e1 100644 --- a/gnovm/stdlibs/std/native.gno +++ b/gnovm/stdlibs/std/native.gno @@ -10,8 +10,9 @@ func AssertOriginCall() // injected // MsgRun. func IsOriginCall() bool // injected -func GetChainID() string // injected -func GetHeight() int64 // injected +func GetChainID() string // injected +func GetChainDomain() string // injected +func GetHeight() int64 // injected func GetOrigSend() Coins { den, amt := origSend() diff --git a/gnovm/stdlibs/std/native.go b/gnovm/stdlibs/std/native.go index 3fe5fbb9889..fb181d9be31 100644 --- a/gnovm/stdlibs/std/native.go +++ b/gnovm/stdlibs/std/native.go @@ -27,6 +27,10 @@ func GetChainID(m *gno.Machine) string { return GetContext(m).ChainID } +func GetChainDomain(m *gno.Machine) string { + return GetContext(m).ChainDomain +} + func GetHeight(m *gno.Machine) int64 { return GetContext(m).Height } diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 54cfb7846ab..2baba6b5005 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,10 +13,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) -// std/native.gno:44 +// std/native.gno:45 // main() // main/files/std5.gno:10 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index 27545f267ce..4f749c3a6e1 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,10 +23,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) -// std/native.gno:44 +// std/native.gno:45 // fn() // main/files/std8.gno:16 // testutils.WrapCall(inner) diff --git a/gnovm/tests/files/zrealm_natbind1_stdlibs.gno b/gnovm/tests/files/zrealm_natbind1_stdlibs.gno new file mode 100644 index 00000000000..f44b6ab4fcf --- /dev/null +++ b/gnovm/tests/files/zrealm_natbind1_stdlibs.gno @@ -0,0 +1,16 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "std" +) + +func main() { + println(std.GetChainDomain()) +} + +// Output: +// tests.gno.land + +// Realm: +// switchrealm["gno.land/r/test"] From 0a2c447558f8eda925ab3d8f2f6ee4938c7fd003 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:13:39 +0100 Subject: [PATCH 299/345] feat: add r/docs/img_embed (#3241) Embedding an image. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/docs/docs.gno | 2 ++ examples/gno.land/r/docs/img_embed/gno.mod | 1 + examples/gno.land/r/docs/img_embed/img_embed.gno | 10 ++++++++++ 3 files changed, 13 insertions(+) create mode 100644 examples/gno.land/r/docs/img_embed/gno.mod create mode 100644 examples/gno.land/r/docs/img_embed/img_embed.gno diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index f796f07bf4a..57d020cd737 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -12,7 +12,9 @@ Explore various examples to learn more about Gno functionality and usage. - [Adder](/r/docs/adder) - An interactive example to update a number with transactions. - [Source](/r/docs/source) - View realm source code. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. +- [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - ... + ## Other resources diff --git a/examples/gno.land/r/docs/img_embed/gno.mod b/examples/gno.land/r/docs/img_embed/gno.mod new file mode 100644 index 00000000000..784914baef5 --- /dev/null +++ b/examples/gno.land/r/docs/img_embed/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/img_embed diff --git a/examples/gno.land/r/docs/img_embed/img_embed.gno b/examples/gno.land/r/docs/img_embed/img_embed.gno new file mode 100644 index 00000000000..b65512d1968 --- /dev/null +++ b/examples/gno.land/r/docs/img_embed/img_embed.gno @@ -0,0 +1,10 @@ +package image_embed + +// Render displays a title and an embedded image from Imgur +func Render(path string) string { + return `# Image Embed Example + +Here’s an example of embedding an image in a Gno realm: + +![Example Image](https://i.imgur.com/So4rBPB.jpeg)` +} From 5f7216d7034fbdf3c9756579b6d2f454afde727a Mon Sep 17 00:00:00 2001 From: Bilog WEB3 <155262265+Bilogweb3@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:58:36 +0100 Subject: [PATCH 300/345] chore: correct typos docs (#3295) I reviewed the entire repository, no more typos found in docs. Hope this helps streamline the project! Best regards, Bilogweb3 --- misc/deployments/test4.gno.land/README.md | 4 ++-- misc/deployments/test5.gno.land/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/deployments/test4.gno.land/README.md b/misc/deployments/test4.gno.land/README.md index 6277ea996ec..7a3a7d06b28 100644 --- a/misc/deployments/test4.gno.land/README.md +++ b/misc/deployments/test4.gno.land/README.md @@ -4,7 +4,7 @@ This deployment folder contains minimal information needed to launch a full test ## `genesis.json` -The initial `genesis.json` validator set is consisted of 3 entities (7 validators in total): +The initial `genesis.json` validator set consisted of 3 entities (7 validators in total): - Gno Core - the gno core team (**4 validators**) - Gno DevX - the gno devX team (**2 validators**) @@ -37,4 +37,4 @@ Some configuration params are required, while others are advised to be set. - `rpc.laddr` - the JSON-RPC listen address, **specific to every node deployment**. - `telemetry.enabled` - flag indicating if telemetry should be turned on. **Advised to be `true`**. - `telemetry.exporter_endpoint` - endpoint for the otel exported. ⚠️ **Required if `telemetry.enabled=true`** ⚠️. -- `telemetry.service_instance_id` - unique ID of the node telemetry instance, **specific to every node deployment**. \ No newline at end of file +- `telemetry.service_instance_id` - unique ID of the node telemetry instance, **specific to every node deployment**. diff --git a/misc/deployments/test5.gno.land/README.md b/misc/deployments/test5.gno.land/README.md index 3dcbf79f2ec..40da91f3b74 100644 --- a/misc/deployments/test5.gno.land/README.md +++ b/misc/deployments/test5.gno.land/README.md @@ -9,7 +9,7 @@ The initial `genesis.json` validator set is consisted of 6 entities (17 validato - Gno Core - the gno core team (**6 validators**) - Gno DevX - the gno devX team (**4 validators**) - AiB - the AiB DevOps team (**3 validators**) -- Onbloc - the [Onbloc](https://onbloc.xyz/) team (**2 validator**) +- Onbloc - the [Onbloc](https://onbloc.xyz/) team (**2 validators**) - Teritori - the [Teritori](https://teritori.com/) team (**1 validator**) - Berty - the [Berty](https://berty.tech/) team (**1 validator**) From 79c9b04b845a6e0e30e657c4d611a53babca3055 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Sat, 7 Dec 2024 12:40:09 -0800 Subject: [PATCH 301/345] fix(contribs): close file in execBalancesExport (#3294) Ensures that the opened file is not leaked and closed after use. Fixes #3032 --- contribs/gnogenesis/internal/balances/balances_export.go | 1 + 1 file changed, 1 insertion(+) diff --git a/contribs/gnogenesis/internal/balances/balances_export.go b/contribs/gnogenesis/internal/balances/balances_export.go index df9d6795805..1970e348b1a 100644 --- a/contribs/gnogenesis/internal/balances/balances_export.go +++ b/contribs/gnogenesis/internal/balances/balances_export.go @@ -60,6 +60,7 @@ func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error { if err != nil { return fmt.Errorf("unable to create output file, %w", err) } + defer outputFile.Close() // Save the balances for _, balance := range state.Balances { From 53cee96236e93e647bc11fedd44729a5618344db Mon Sep 17 00:00:00 2001 From: n0izn0iz Date: Sat, 7 Dec 2024 21:44:26 +0100 Subject: [PATCH 302/345] feat(gnomod)!: forbid require and find dependencies without it (#3123) A step towards the importer package (#2932) and future of `gno.mod` (#2904) - BREAKING CHANGE: remove `require` statement support from `gno.mod` - BREAKING CHANGE: remove `-v` (verbose) and `--remote` flags in `gno mod download` - Don't require version specification in `gno.mod`'s `replace` statements - Use `.gno` files `import` statements to find dependencies - Extract and refacto imports gathering utils in `gnovm/pkg/packages` - Add `gnovm/cmd/gno/internal/pkgdownload.PackageFetcher` interface - Implement `PackageFetcher` using `vm/qfile` queries in `gnovm/cmd/gno/internal/pkgdownload/rpcpackagefetcher` - Rewrite single package download routine in `gnovm/cmd/gno/internal/pkgdownload` - Move and refacto dependencies download routine in `gnovm/cmd/gno/download_deps.go` - Add a `--remote-overrides` flag for `gno mod download` that takes `chain-domain=rpc-url` comma-separated pairs to override endpoints used to fetch packages - Add and use a testing implementation of `PackageFetcher` called `examplesPackageFetcher` that serves package from the `examples` directory for testing purposes (download tests before this PR use the portal loop public endpoint) - Make `ReadMemPackage` and it's dependencies error-out instead of panicking - Create panicking `MemPackage` utils that wrap the erroring ones and use them at existing callsites I decided to do this first to avoid having multiple ways to resolve dependencies lying around in the codebase and causing confusion in subsequent steps
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: Norman Meier Co-authored-by: Morgan Bazalgette --- contribs/gnodev/pkg/dev/packages.go | 2 +- contribs/gnogenesis/go.mod | 1 - contribs/gnogenesis/go.sum | 2 - contribs/gnomigrate/go.mod | 1 - contribs/gnomigrate/go.sum | 2 - examples/gno.land/p/demo/acl/gno.mod | 7 - examples/gno.land/p/demo/avl/pager/gno.mod | 7 - examples/gno.land/p/demo/avlhelpers/gno.mod | 2 - examples/gno.land/p/demo/blog/gno.mod | 6 - examples/gno.land/p/demo/dao/gno.mod | 2 - examples/gno.land/p/demo/dom/gno.mod | 2 - examples/gno.land/p/demo/fqname/gno.mod | 2 - .../gno.land/p/demo/gnorkle/agent/gno.mod | 5 - .../p/demo/gnorkle/feeds/static/gno.mod | 12 -- .../gno.land/p/demo/gnorkle/gnorkle/gno.mod | 8 - .../p/demo/gnorkle/ingesters/single/gno.mod | 7 - .../gno.land/p/demo/gnorkle/message/gno.mod | 2 - .../p/demo/gnorkle/storage/simple/gno.mod | 8 - examples/gno.land/p/demo/grc/grc1155/gno.mod | 6 - examples/gno.land/p/demo/grc/grc20/gno.mod | 9 - examples/gno.land/p/demo/grc/grc721/gno.mod | 7 - examples/gno.land/p/demo/grc/grc777/gno.mod | 2 - examples/gno.land/p/demo/groups/gno.mod | 2 - examples/gno.land/p/demo/int256/gno.mod | 2 - examples/gno.land/p/demo/json/gno.mod | 2 - .../gno.land/p/demo/math_eval/int32/gno.mod | 2 - examples/gno.land/p/demo/membstore/gno.mod | 8 - examples/gno.land/p/demo/memeland/gno.mod | 9 - examples/gno.land/p/demo/microblog/gno.mod | 5 - .../p/demo/ownable/exts/authorizable/gno.mod | 8 - examples/gno.land/p/demo/ownable/gno.mod | 5 - examples/gno.land/p/demo/pausable/gno.mod | 5 - examples/gno.land/p/demo/seqid/gno.mod | 2 - examples/gno.land/p/demo/simpledao/gno.mod | 11 -- .../p/demo/subscription/lifetime/gno.mod | 7 - .../p/demo/subscription/recurring/gno.mod | 7 - examples/gno.land/p/demo/svg/gno.mod | 2 - examples/gno.land/p/demo/tamagotchi/gno.mod | 2 - examples/gno.land/p/demo/tests/gno.mod | 5 - examples/gno.land/p/demo/todolist/gno.mod | 5 - examples/gno.land/p/demo/uassert/gno.mod | 2 - examples/gno.land/p/demo/urequire/gno.mod | 2 - examples/gno.land/p/demo/watchdog/gno.mod | 2 - examples/gno.land/p/gov/executor/gno.mod | 6 - examples/gno.land/p/moul/helplink/gno.mod | 5 - examples/gno.land/p/moul/mdtable/gno.mod | 2 - .../gno.land/p/moul/printfdebugging/gno.mod | 2 - examples/gno.land/p/moul/realmpath/gno.mod | 5 - examples/gno.land/p/moul/txlink/gno.mod | 2 - examples/gno.land/p/n2p5/haystack/gno.mod | 5 - examples/gno.land/p/n2p5/mgroup/gno.mod | 6 - examples/gno.land/p/nt/poa/gno.mod | 9 - .../gno.land/p/wyhaines/rand/isaac/gno.mod | 6 - .../gno.land/p/wyhaines/rand/isaac64/gno.mod | 6 - .../p/wyhaines/rand/xorshift64star/gno.mod | 5 - .../p/wyhaines/rand/xorshiftr128plus/gno.mod | 5 - examples/gno.land/r/demo/art/gnoface/gno.mod | 6 - .../gno.land/r/demo/art/millipede/gno.mod | 5 - examples/gno.land/r/demo/bar20/gno.mod | 8 - examples/gno.land/r/demo/boards/gno.mod | 6 - examples/gno.land/r/demo/daoweb/gno.mod | 6 - examples/gno.land/r/demo/disperse/gno.mod | 2 - examples/gno.land/r/demo/echo/gno.mod | 2 - examples/gno.land/r/demo/foo1155/gno.mod | 7 - examples/gno.land/r/demo/foo20/gno.mod | 11 -- examples/gno.land/r/demo/foo721/gno.mod | 7 - .../gno.land/r/demo/games/dice_roller/gno.mod | 10 -- .../gno.land/r/demo/games/shifumi/gno.mod | 6 - examples/gno.land/r/demo/grc20factory/gno.mod | 10 -- examples/gno.land/r/demo/grc20reg/gno.mod | 8 - examples/gno.land/r/demo/groups/gno.mod | 5 - examples/gno.land/r/demo/keystore/gno.mod | 7 - examples/gno.land/r/demo/math_eval/gno.mod | 5 - examples/gno.land/r/demo/memeland/gno.mod | 2 - examples/gno.land/r/demo/microblog/gno.mod | 8 - examples/gno.land/r/demo/mirror/gno.mod | 2 - examples/gno.land/r/demo/nft/gno.mod | 5 - examples/gno.land/r/demo/profile/gno.mod | 8 - .../gno.land/r/demo/releases_example/gno.mod | 2 - examples/gno.land/r/demo/tamagotchi/gno.mod | 5 - .../gno.land/r/demo/tests/crossrealm/gno.mod | 5 - .../r/demo/tests/crossrealm_b/gno.mod | 2 - examples/gno.land/r/demo/tests/gno.mod | 5 - examples/gno.land/r/demo/tests_foo/gno.mod | 2 - examples/gno.land/r/demo/todolist/gno.mod | 8 - examples/gno.land/r/demo/types/gno.mod | 2 - examples/gno.land/r/demo/ui/gno.mod | 5 - examples/gno.land/r/demo/userbook/gno.mod | 7 - examples/gno.land/r/demo/users/gno.mod | 8 - examples/gno.land/r/demo/wugnot/gno.mod | 8 - examples/gno.land/r/docs/adder/gno.mod | 2 - examples/gno.land/r/docs/avl_pager/gno.mod | 5 - examples/gno.land/r/gnoland/blog/gno.mod | 7 - examples/gno.land/r/gnoland/events/gno.mod | 8 - examples/gno.land/r/gnoland/faucet/gno.mod | 6 - examples/gno.land/r/gnoland/ghverify/gno.mod | 8 - examples/gno.land/r/gnoland/home/gno.mod | 9 - examples/gno.land/r/gnoland/monit/gno.mod | 7 - examples/gno.land/r/gnoland/pages/gno.mod | 5 - .../gno.land/r/gnoland/valopers/v2/gno.mod | 11 -- examples/gno.land/r/gov/dao/bridge/gno.mod | 10 -- examples/gno.land/r/gov/dao/v2/gno.mod | 11 -- examples/gno.land/r/leon/hof/gno.mod | 14 -- examples/gno.land/r/leon/home/gno.mod | 9 - examples/gno.land/r/morgan/guestbook/gno.mod | 6 - examples/gno.land/r/morgan/home/gno.mod | 2 - examples/gno.land/r/moul/home/gno.mod | 5 - examples/gno.land/r/moul/present/gno.mod | 5 - examples/gno.land/r/n2p5/config/gno.mod | 5 - examples/gno.land/r/n2p5/haystack/gno.mod | 7 - examples/gno.land/r/n2p5/home/gno.mod | 6 - examples/gno.land/r/stefann/home/gno.mod | 8 - examples/gno.land/r/stefann/registry/gno.mod | 2 - examples/gno.land/r/sys/params/gno.mod | 5 - examples/gno.land/r/sys/users/gno.mod | 5 - examples/gno.land/r/sys/validators/v2/gno.mod | 12 -- examples/gno.land/r/x/manfred_outfmt/gno.mod | 2 - gno.land/pkg/gnoland/genesis.go | 2 +- .../pkg/integration/testing_integration.go | 20 ++- gno.land/pkg/keyscli/addpkg.go | 2 +- gno.land/pkg/keyscli/run.go | 2 +- gno.land/pkg/sdk/vm/keeper.go | 2 +- gno.land/pkg/sdk/vm/msgs.go | 4 +- gnovm/cmd/gno/download_deps.go | 86 +++++++++ gnovm/cmd/gno/download_deps_test.go | 152 ++++++++++++++++ .../examplespkgfetcher/examplespkgfetcher.go | 52 ++++++ .../gno/internal/pkgdownload/pkgdownload.go | 30 ++++ .../gno/internal/pkgdownload/pkgfetcher.go | 7 + .../rpcpkgfetcher/rpcpkgfetcher.go | 89 ++++++++++ .../rpcpkgfetcher/rpcpkgfetcher_test.go | 53 ++++++ gnovm/cmd/gno/lint.go | 2 +- gnovm/cmd/gno/main_test.go | 11 +- gnovm/cmd/gno/mod.go | 164 +++++------------ gnovm/cmd/gno/mod_test.go | 158 +---------------- gnovm/cmd/gno/test.go | 2 +- gnovm/pkg/doc/dirs.go | 58 +++++- gnovm/pkg/doc/dirs_test.go | 13 +- gnovm/pkg/doc/testdata/dirsmod/a.gno | 9 + gnovm/pkg/doc/testdata/dirsmod/gno.mod | 6 +- gnovm/pkg/gnolang/files_test.go | 2 +- gnovm/pkg/gnolang/nodes.go | 59 +++++-- gnovm/pkg/gnomod/fetch.go | 30 ---- gnovm/pkg/gnomod/file.go | 129 +------------- gnovm/pkg/gnomod/file_test.go | 142 --------------- gnovm/pkg/gnomod/gnomod.go | 138 +-------------- gnovm/pkg/gnomod/parse.go | 22 +-- gnovm/pkg/gnomod/parse_test.go | 21 ++- gnovm/pkg/gnomod/pkg.go | 52 ++++-- gnovm/pkg/gnomod/pkg_test.go | 142 ++------------- gnovm/pkg/gnomod/preprocess.go | 37 +--- gnovm/pkg/gnomod/preprocess_test.go | 127 ------------- gnovm/pkg/gnomod/read.go | 60 ------- gnovm/pkg/gnomod/read_test.go | 167 ------------------ gnovm/pkg/packages/doc.go | 2 + gnovm/pkg/packages/imports.go | 72 ++++++++ gnovm/pkg/packages/imports_test.go | 127 +++++++++++++ gnovm/pkg/test/imports.go | 28 ++- .../integ/invalid_module_version1/gno.mod | 5 - .../integ/invalid_module_version2/gno.mod | 5 - gnovm/tests/integ/replace_with_dir/gno.mod | 4 - .../integ/replace_with_invalid_module/gno.mod | 6 +- .../replace_with_invalid_module/main.gno | 7 + gnovm/tests/integ/replace_with_module/gno.mod | 6 +- .../tests/integ/replace_with_module/main.gno | 7 + .../integ/require_invalid_module/gno.mod | 6 +- .../integ/require_invalid_module/main.gno | 7 + .../tests/integ/require_remote_module/gno.mod | 4 - gnovm/tests/integ/require_std_lib/gno.mod | 1 + gnovm/tests/integ/require_std_lib/main.gno | 7 + gnovm/tests/integ/valid2/gno.mod | 2 - misc/loop/go.mod | 1 - 171 files changed, 988 insertions(+), 1999 deletions(-) create mode 100644 gnovm/cmd/gno/download_deps.go create mode 100644 gnovm/cmd/gno/download_deps_test.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go create mode 100644 gnovm/pkg/doc/testdata/dirsmod/a.gno delete mode 100644 gnovm/pkg/gnomod/fetch.go delete mode 100644 gnovm/pkg/gnomod/file_test.go create mode 100644 gnovm/pkg/packages/doc.go create mode 100644 gnovm/pkg/packages/imports.go create mode 100644 gnovm/pkg/packages/imports_test.go delete mode 100644 gnovm/tests/integ/invalid_module_version1/gno.mod delete mode 100644 gnovm/tests/integ/invalid_module_version2/gno.mod create mode 100644 gnovm/tests/integ/replace_with_invalid_module/main.gno create mode 100644 gnovm/tests/integ/replace_with_module/main.gno create mode 100644 gnovm/tests/integ/require_invalid_module/main.gno create mode 100644 gnovm/tests/integ/require_std_lib/gno.mod create mode 100644 gnovm/tests/integ/require_std_lib/main.gno diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index cccbf316525..62c1907b8c9 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -138,7 +138,7 @@ func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetada } // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(modPkg.Dir, modPkg.Name) + memPkg := gno.MustReadMemPackage(modPkg.Dir, modPkg.Name) if err := memPkg.Validate(); err != nil { return nil, fmt.Errorf("invalid package: %w", err) } diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index 393fed0725d..b777cc6e5eb 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -53,7 +53,6 @@ require ( golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index f3161e47bad..3c6127ac216 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -195,8 +195,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index c492ae7c818..a81c2de4ba0 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -48,7 +48,6 @@ require ( golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index f3161e47bad..3c6127ac216 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -195,8 +195,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/gno.land/p/demo/acl/gno.mod b/examples/gno.land/p/demo/acl/gno.mod index 15d9f078048..04fbf9043c4 100644 --- a/examples/gno.land/p/demo/acl/gno.mod +++ b/examples/gno.land/p/demo/acl/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/acl - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/avl/pager/gno.mod b/examples/gno.land/p/demo/avl/pager/gno.mod index 59c961d73f2..020b809b208 100644 --- a/examples/gno.land/p/demo/avl/pager/gno.mod +++ b/examples/gno.land/p/demo/avl/pager/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/avl/pager - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/avlhelpers/gno.mod b/examples/gno.land/p/demo/avlhelpers/gno.mod index 559f60975cf..5adffd13a43 100644 --- a/examples/gno.land/p/demo/avlhelpers/gno.mod +++ b/examples/gno.land/p/demo/avlhelpers/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/avlhelpers - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/blog/gno.mod b/examples/gno.land/p/demo/blog/gno.mod index 65f58e7a0f6..e4e3def299b 100644 --- a/examples/gno.land/p/demo/blog/gno.mod +++ b/examples/gno.land/p/demo/blog/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/blog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/dao/gno.mod b/examples/gno.land/p/demo/dao/gno.mod index ecbab2f7692..fbb23299116 100644 --- a/examples/gno.land/p/demo/dao/gno.mod +++ b/examples/gno.land/p/demo/dao/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/dao - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/dom/gno.mod b/examples/gno.land/p/demo/dom/gno.mod index 83ca827cf66..bd8bba14d06 100644 --- a/examples/gno.land/p/demo/dom/gno.mod +++ b/examples/gno.land/p/demo/dom/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/dom - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/fqname/gno.mod b/examples/gno.land/p/demo/fqname/gno.mod index 1282e262303..afee55e0b7b 100644 --- a/examples/gno.land/p/demo/fqname/gno.mod +++ b/examples/gno.land/p/demo/fqname/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/fqname - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/gnorkle/agent/gno.mod b/examples/gno.land/p/demo/gnorkle/agent/gno.mod index 093ca9cf38e..e784354c35e 100644 --- a/examples/gno.land/p/demo/gnorkle/agent/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/agent/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/gnorkle/agent - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod b/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod index c651c62cb1b..05363a3cd06 100644 --- a/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod @@ -1,13 +1 @@ module gno.land/p/demo/gnorkle/feeds/static - -require ( - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/ingesters/single v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest - gno.land/p/demo/gnorkle/storage/simple v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod b/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod index 88fb202863f..ce2c2c3706d 100644 --- a/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/gnorkle/gnorkle - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/gnorkle/agent v0.0.0-latest - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod b/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod index 71120966a0c..8cf5a9a30d8 100644 --- a/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/gnorkle/ingesters/single - -require ( - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/storage/simple v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/message/gno.mod b/examples/gno.land/p/demo/gnorkle/message/gno.mod index 4baad40ef86..5544d0eb873 100644 --- a/examples/gno.land/p/demo/gnorkle/message/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/message/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/gnorkle/message - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod b/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod index cd673a8771c..b842e2b514c 100644 --- a/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/gnorkle/storage/simple - -require ( - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/storage v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc1155/gno.mod b/examples/gno.land/p/demo/grc/grc1155/gno.mod index d6db0700146..1c3ec6360eb 100644 --- a/examples/gno.land/p/demo/grc/grc1155/gno.mod +++ b/examples/gno.land/p/demo/grc/grc1155/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/grc/grc1155 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index 91b430d3d2f..37377b32e73 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -1,10 +1 @@ module gno.land/p/demo/grc/grc20 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/exts v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc721/gno.mod b/examples/gno.land/p/demo/grc/grc721/gno.mod index 9e1d6f56ffc..f27caee5282 100644 --- a/examples/gno.land/p/demo/grc/grc721/gno.mod +++ b/examples/gno.land/p/demo/grc/grc721/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/grc/grc721 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc777/gno.mod b/examples/gno.land/p/demo/grc/grc777/gno.mod index 9fbf2f2b7cd..da5c762b2ec 100644 --- a/examples/gno.land/p/demo/grc/grc777/gno.mod +++ b/examples/gno.land/p/demo/grc/grc777/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/grc/grc777 - -require gno.land/p/demo/grc/exts v0.0.0-latest diff --git a/examples/gno.land/p/demo/groups/gno.mod b/examples/gno.land/p/demo/groups/gno.mod index cf33d0ce74b..d33df3866fa 100644 --- a/examples/gno.land/p/demo/groups/gno.mod +++ b/examples/gno.land/p/demo/groups/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/groups - -require gno.land/p/demo/rat v0.0.0-latest diff --git a/examples/gno.land/p/demo/int256/gno.mod b/examples/gno.land/p/demo/int256/gno.mod index ef906c83c93..33fb0bc4e72 100644 --- a/examples/gno.land/p/demo/int256/gno.mod +++ b/examples/gno.land/p/demo/int256/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/int256 - -require gno.land/p/demo/uint256 v0.0.0-latest diff --git a/examples/gno.land/p/demo/json/gno.mod b/examples/gno.land/p/demo/json/gno.mod index ef794458c56..831fa56c0f9 100644 --- a/examples/gno.land/p/demo/json/gno.mod +++ b/examples/gno.land/p/demo/json/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/json - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/math_eval/int32/gno.mod b/examples/gno.land/p/demo/math_eval/int32/gno.mod index de57497a699..c4e4bc8f454 100644 --- a/examples/gno.land/p/demo/math_eval/int32/gno.mod +++ b/examples/gno.land/p/demo/math_eval/int32/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/math_eval/int32 - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/membstore/gno.mod b/examples/gno.land/p/demo/membstore/gno.mod index da22a8dcae4..007e7a5d883 100644 --- a/examples/gno.land/p/demo/membstore/gno.mod +++ b/examples/gno.land/p/demo/membstore/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/membstore - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/memeland/gno.mod b/examples/gno.land/p/demo/memeland/gno.mod index 66f22d1ccee..06cc8fbf487 100644 --- a/examples/gno.land/p/demo/memeland/gno.mod +++ b/examples/gno.land/p/demo/memeland/gno.mod @@ -1,10 +1 @@ module gno.land/p/demo/memeland - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/microblog/gno.mod b/examples/gno.land/p/demo/microblog/gno.mod index 9bbcfa19e31..a285ef5f903 100644 --- a/examples/gno.land/p/demo/microblog/gno.mod +++ b/examples/gno.land/p/demo/microblog/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/microblog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod index f36823f3f71..0e8be79f130 100644 --- a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/ownable/exts/authorizable - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/ownable/gno.mod b/examples/gno.land/p/demo/ownable/gno.mod index 00f7812f6f5..9a9abb1e661 100644 --- a/examples/gno.land/p/demo/ownable/gno.mod +++ b/examples/gno.land/p/demo/ownable/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/ownable - -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/pausable/gno.mod b/examples/gno.land/p/demo/pausable/gno.mod index 156875f7d85..a741342eb84 100644 --- a/examples/gno.land/p/demo/pausable/gno.mod +++ b/examples/gno.land/p/demo/pausable/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/pausable - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/seqid/gno.mod b/examples/gno.land/p/demo/seqid/gno.mod index d1390012c3c..63e6a1fb551 100644 --- a/examples/gno.land/p/demo/seqid/gno.mod +++ b/examples/gno.land/p/demo/seqid/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/seqid - -require gno.land/p/demo/cford32 v0.0.0-latest diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index f6f14f379ec..51de621cbec 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -1,12 +1 @@ module gno.land/p/demo/simpledao - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/membstore v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/subscription/lifetime/gno.mod b/examples/gno.land/p/demo/subscription/lifetime/gno.mod index 0084aa714c5..59b6c1cf001 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/gno.mod +++ b/examples/gno.land/p/demo/subscription/lifetime/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/subscription/lifetime - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/subscription/recurring/gno.mod b/examples/gno.land/p/demo/subscription/recurring/gno.mod index d3cf8a044f8..356402978b5 100644 --- a/examples/gno.land/p/demo/subscription/recurring/gno.mod +++ b/examples/gno.land/p/demo/subscription/recurring/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/subscription/recurring - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/svg/gno.mod b/examples/gno.land/p/demo/svg/gno.mod index 0af7ba0636d..b9dd7f47434 100644 --- a/examples/gno.land/p/demo/svg/gno.mod +++ b/examples/gno.land/p/demo/svg/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/svg - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tamagotchi/gno.mod b/examples/gno.land/p/demo/tamagotchi/gno.mod index 58441284a6b..a9c6026629e 100644 --- a/examples/gno.land/p/demo/tamagotchi/gno.mod +++ b/examples/gno.land/p/demo/tamagotchi/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/tamagotchi - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tests/gno.mod b/examples/gno.land/p/demo/tests/gno.mod index 8a19acdbb18..a342a726f61 100644 --- a/examples/gno.land/p/demo/tests/gno.mod +++ b/examples/gno.land/p/demo/tests/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/tests - -require ( - gno.land/p/demo/tests/subtests v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/todolist/gno.mod b/examples/gno.land/p/demo/todolist/gno.mod index bbccf357e3b..46d21bf0bc0 100644 --- a/examples/gno.land/p/demo/todolist/gno.mod +++ b/examples/gno.land/p/demo/todolist/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/todolist - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/uassert/gno.mod b/examples/gno.land/p/demo/uassert/gno.mod index f22276564bf..a70e7db825d 100644 --- a/examples/gno.land/p/demo/uassert/gno.mod +++ b/examples/gno.land/p/demo/uassert/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/uassert - -require gno.land/p/demo/diff v0.0.0-latest diff --git a/examples/gno.land/p/demo/urequire/gno.mod b/examples/gno.land/p/demo/urequire/gno.mod index 9689a2222ac..e5336b2c80d 100644 --- a/examples/gno.land/p/demo/urequire/gno.mod +++ b/examples/gno.land/p/demo/urequire/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/urequire - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/watchdog/gno.mod b/examples/gno.land/p/demo/watchdog/gno.mod index 29005441401..96fba14451b 100644 --- a/examples/gno.land/p/demo/watchdog/gno.mod +++ b/examples/gno.land/p/demo/watchdog/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/watchdog - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/gov/executor/gno.mod b/examples/gno.land/p/gov/executor/gno.mod index 99f2ab3610b..5dbb6f7f85e 100644 --- a/examples/gno.land/p/gov/executor/gno.mod +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -1,7 +1 @@ module gno.land/p/gov/executor - -require ( - gno.land/p/demo/context v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/moul/helplink/gno.mod b/examples/gno.land/p/moul/helplink/gno.mod index 1b106749260..cb070b79d6a 100644 --- a/examples/gno.land/p/moul/helplink/gno.mod +++ b/examples/gno.land/p/moul/helplink/gno.mod @@ -1,6 +1 @@ module gno.land/p/moul/helplink - -require ( - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest -) diff --git a/examples/gno.land/p/moul/mdtable/gno.mod b/examples/gno.land/p/moul/mdtable/gno.mod index 0cea0458895..079c935a874 100644 --- a/examples/gno.land/p/moul/mdtable/gno.mod +++ b/examples/gno.land/p/moul/mdtable/gno.mod @@ -1,3 +1 @@ module gno.land/p/moul/mdtable - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/p/moul/printfdebugging/gno.mod b/examples/gno.land/p/moul/printfdebugging/gno.mod index 2cf6aa09e61..4b8d0f3256c 100644 --- a/examples/gno.land/p/moul/printfdebugging/gno.mod +++ b/examples/gno.land/p/moul/printfdebugging/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/printfdebugging - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/moul/realmpath/gno.mod b/examples/gno.land/p/moul/realmpath/gno.mod index e391b76390f..0c012a0c3ae 100644 --- a/examples/gno.land/p/moul/realmpath/gno.mod +++ b/examples/gno.land/p/moul/realmpath/gno.mod @@ -1,6 +1 @@ module gno.land/p/moul/realmpath - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/moul/txlink/gno.mod b/examples/gno.land/p/moul/txlink/gno.mod index 6110464316f..ed16b8b74fd 100644 --- a/examples/gno.land/p/moul/txlink/gno.mod +++ b/examples/gno.land/p/moul/txlink/gno.mod @@ -1,3 +1 @@ module gno.land/p/moul/txlink - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/p/n2p5/haystack/gno.mod b/examples/gno.land/p/n2p5/haystack/gno.mod index ebd0d07a987..987d62d4565 100644 --- a/examples/gno.land/p/n2p5/haystack/gno.mod +++ b/examples/gno.land/p/n2p5/haystack/gno.mod @@ -1,6 +1 @@ module gno.land/p/n2p5/haystack - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/n2p5/haystack/needle v0.0.0-latest -) diff --git a/examples/gno.land/p/n2p5/mgroup/gno.mod b/examples/gno.land/p/n2p5/mgroup/gno.mod index 95fdbe2f195..132913d9c3d 100644 --- a/examples/gno.land/p/n2p5/mgroup/gno.mod +++ b/examples/gno.land/p/n2p5/mgroup/gno.mod @@ -1,7 +1 @@ module gno.land/p/n2p5/mgroup - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest -) diff --git a/examples/gno.land/p/nt/poa/gno.mod b/examples/gno.land/p/nt/poa/gno.mod index 5c1b75eb05a..965eeb56aed 100644 --- a/examples/gno.land/p/nt/poa/gno.mod +++ b/examples/gno.land/p/nt/poa/gno.mod @@ -1,10 +1 @@ module gno.land/p/nt/poa - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod index 0cca6aa5174..538f52e6e7e 100644 --- a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod @@ -1,7 +1 @@ module gno.land/p/wyhaines/rand/isaac - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod index dbc8713094e..79772dfe8d8 100644 --- a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod @@ -1,7 +1 @@ module gno.land/p/wyhaines/rand/isaac64 - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod index bc40b1bc71b..7918a7e7d2d 100644 --- a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod @@ -1,6 +1 @@ module gno.land/p/wyhaines/rand/xorshift64star - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod index c778fc72550..9f3be9ea8df 100644 --- a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod @@ -1,6 +1 @@ module gno.land/p/wyhaines/rand/xorshiftr128plus - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/art/gnoface/gno.mod b/examples/gno.land/r/demo/art/gnoface/gno.mod index 072c98f3bd6..9465af6216a 100644 --- a/examples/gno.land/r/demo/art/gnoface/gno.mod +++ b/examples/gno.land/r/demo/art/gnoface/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/art/gnoface - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/art/millipede/gno.mod b/examples/gno.land/r/demo/art/millipede/gno.mod index 7cd604206fa..3e5177efdcd 100644 --- a/examples/gno.land/r/demo/art/millipede/gno.mod +++ b/examples/gno.land/r/demo/art/millipede/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/art/millipede - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/bar20/gno.mod b/examples/gno.land/r/demo/bar20/gno.mod index 9fb0f083e1b..e8ede1ea44f 100644 --- a/examples/gno.land/r/demo/bar20/gno.mod +++ b/examples/gno.land/r/demo/bar20/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/bar20 - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/boards/gno.mod b/examples/gno.land/r/demo/boards/gno.mod index 24fea7ce853..dffb96740fc 100644 --- a/examples/gno.land/r/demo/boards/gno.mod +++ b/examples/gno.land/r/demo/boards/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/boards - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/daoweb/gno.mod b/examples/gno.land/r/demo/daoweb/gno.mod index bc781b311dc..74ae149cdb6 100644 --- a/examples/gno.land/r/demo/daoweb/gno.mod +++ b/examples/gno.land/r/demo/daoweb/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/daoweb - -require ( - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/json v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/disperse/gno.mod b/examples/gno.land/r/demo/disperse/gno.mod index 0ba9c88810a..06e81884dfa 100644 --- a/examples/gno.land/r/demo/disperse/gno.mod +++ b/examples/gno.land/r/demo/disperse/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/disperse - -require gno.land/r/demo/grc20factory v0.0.0-latest diff --git a/examples/gno.land/r/demo/echo/gno.mod b/examples/gno.land/r/demo/echo/gno.mod index 4ca5ccab6e0..f07d78943d1 100644 --- a/examples/gno.land/r/demo/echo/gno.mod +++ b/examples/gno.land/r/demo/echo/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/echo - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/r/demo/foo1155/gno.mod b/examples/gno.land/r/demo/foo1155/gno.mod index 0a405c5b4a2..eae12bcd1e3 100644 --- a/examples/gno.land/r/demo/foo1155/gno.mod +++ b/examples/gno.land/r/demo/foo1155/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/foo1155 - -require ( - gno.land/p/demo/grc/grc1155 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index 64b8f90a27d..79dea556e78 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -1,12 +1 @@ module gno.land/r/demo/foo20 - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/foo721/gno.mod b/examples/gno.land/r/demo/foo721/gno.mod index e013677379d..4779f2fc467 100644 --- a/examples/gno.land/r/demo/foo721/gno.mod +++ b/examples/gno.land/r/demo/foo721/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/foo721 - -require ( - gno.land/p/demo/grc/grc721 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/dice_roller/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod index 75c6473fa3e..3aae9cbe791 100644 --- a/examples/gno.land/r/demo/games/dice_roller/gno.mod +++ b/examples/gno.land/r/demo/games/dice_roller/gno.mod @@ -1,11 +1 @@ module gno.land/r/demo/games/dice_roller - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/shifumi/gno.mod b/examples/gno.land/r/demo/games/shifumi/gno.mod index 7a4fc173d3d..e6a428090a9 100644 --- a/examples/gno.land/r/demo/games/shifumi/gno.mod +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/games/shifumi - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index a2d2a55fdf0..f89ee5872a5 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -1,11 +1 @@ module gno.land/r/demo/grc20factory - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/grc20reg/gno.mod b/examples/gno.land/r/demo/grc20reg/gno.mod index f02ee09c35a..c5065c60064 100644 --- a/examples/gno.land/r/demo/grc20reg/gno.mod +++ b/examples/gno.land/r/demo/grc20reg/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/grc20reg - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/fqname v0.0.0-latest - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/groups/gno.mod b/examples/gno.land/r/demo/groups/gno.mod index fc6756e13e2..6f715471ced 100644 --- a/examples/gno.land/r/demo/groups/gno.mod +++ b/examples/gno.land/r/demo/groups/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/groups - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/keystore/gno.mod b/examples/gno.land/r/demo/keystore/gno.mod index 49b0f3494a4..cd07d24adf6 100644 --- a/examples/gno.land/r/demo/keystore/gno.mod +++ b/examples/gno.land/r/demo/keystore/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/keystore - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/math_eval/gno.mod b/examples/gno.land/r/demo/math_eval/gno.mod index 0e3fcfe6e9b..c797becfa7d 100644 --- a/examples/gno.land/r/demo/math_eval/gno.mod +++ b/examples/gno.land/r/demo/math_eval/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/math_eval - -require ( - gno.land/p/demo/math_eval/int32 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/memeland/gno.mod b/examples/gno.land/r/demo/memeland/gno.mod index 5c73379519b..0ccb353659f 100644 --- a/examples/gno.land/r/demo/memeland/gno.mod +++ b/examples/gno.land/r/demo/memeland/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/memeland - -require gno.land/p/demo/memeland v0.0.0-latest diff --git a/examples/gno.land/r/demo/microblog/gno.mod b/examples/gno.land/r/demo/microblog/gno.mod index 26349e481d4..a622200b76d 100644 --- a/examples/gno.land/r/demo/microblog/gno.mod +++ b/examples/gno.land/r/demo/microblog/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/microblog - -require ( - gno.land/p/demo/microblog v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/mirror/gno.mod b/examples/gno.land/r/demo/mirror/gno.mod index 2bf27fd6916..cb53585644a 100644 --- a/examples/gno.land/r/demo/mirror/gno.mod +++ b/examples/gno.land/r/demo/mirror/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/mirror - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/nft/gno.mod b/examples/gno.land/r/demo/nft/gno.mod index 89e0055be51..ad760d186ab 100644 --- a/examples/gno.land/r/demo/nft/gno.mod +++ b/examples/gno.land/r/demo/nft/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/nft - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc721 v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/profile/gno.mod b/examples/gno.land/r/demo/profile/gno.mod index e7feac5d680..3e875672a99 100644 --- a/examples/gno.land/r/demo/profile/gno.mod +++ b/examples/gno.land/r/demo/profile/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/profile - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/releases_example/gno.mod b/examples/gno.land/r/demo/releases_example/gno.mod index 22f640fe797..0dc5d6561dc 100644 --- a/examples/gno.land/r/demo/releases_example/gno.mod +++ b/examples/gno.land/r/demo/releases_example/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/releases_example - -require gno.land/p/demo/releases v0.0.0-latest diff --git a/examples/gno.land/r/demo/tamagotchi/gno.mod b/examples/gno.land/r/demo/tamagotchi/gno.mod index b7a2deea2c2..bccf4841666 100644 --- a/examples/gno.land/r/demo/tamagotchi/gno.mod +++ b/examples/gno.land/r/demo/tamagotchi/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tamagotchi - -require ( - gno.land/p/demo/tamagotchi v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests/crossrealm/gno.mod b/examples/gno.land/r/demo/tests/crossrealm/gno.mod index 71a89ec2ec5..2f7f217d288 100644 --- a/examples/gno.land/r/demo/tests/crossrealm/gno.mod +++ b/examples/gno.land/r/demo/tests/crossrealm/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tests/crossrealm - -require ( - gno.land/p/demo/tests/p_crossrealm v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod index 74548712caa..236010c21b3 100644 --- a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod +++ b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/tests/crossrealm_b - -require gno.land/r/demo/tests/crossrealm v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index c51571e7d04..f04aa5cf7bd 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tests - -require ( - gno.land/p/demo/nestedpkg v0.0.0-latest - gno.land/r/demo/tests/subtests v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests_foo/gno.mod b/examples/gno.land/r/demo/tests_foo/gno.mod index 226271ae4b0..e5a00113181 100644 --- a/examples/gno.land/r/demo/tests_foo/gno.mod +++ b/examples/gno.land/r/demo/tests_foo/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/tests_foo - -require gno.land/r/demo/tests v0.0.0-latest diff --git a/examples/gno.land/r/demo/todolist/gno.mod b/examples/gno.land/r/demo/todolist/gno.mod index 36909859a6f..acd336f1724 100644 --- a/examples/gno.land/r/demo/todolist/gno.mod +++ b/examples/gno.land/r/demo/todolist/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/todolist - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/todolist v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/types/gno.mod b/examples/gno.land/r/demo/types/gno.mod index 0e86e5d5676..c24f7ddbc93 100644 --- a/examples/gno.land/r/demo/types/gno.mod +++ b/examples/gno.land/r/demo/types/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/types - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/ui/gno.mod b/examples/gno.land/r/demo/ui/gno.mod index 0ef5d9dd40e..591b0b93190 100644 --- a/examples/gno.land/r/demo/ui/gno.mod +++ b/examples/gno.land/r/demo/ui/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/ui - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/userbook/gno.mod b/examples/gno.land/r/demo/userbook/gno.mod index 213586d12ee..bb709a39ed7 100644 --- a/examples/gno.land/r/demo/userbook/gno.mod +++ b/examples/gno.land/r/demo/userbook/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/userbook - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod index f2f88a0f993..4d7fd15d1cd 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/users - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avl/pager v0.0.0-latest - gno.land/p/demo/avlhelpers v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/wugnot/gno.mod b/examples/gno.land/r/demo/wugnot/gno.mod index c7081ce6963..12b6baa7ae2 100644 --- a/examples/gno.land/r/demo/wugnot/gno.mod +++ b/examples/gno.land/r/demo/wugnot/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/wugnot - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/docs/adder/gno.mod b/examples/gno.land/r/docs/adder/gno.mod index f8bbf9d6fe8..f4958c6494d 100644 --- a/examples/gno.land/r/docs/adder/gno.mod +++ b/examples/gno.land/r/docs/adder/gno.mod @@ -1,3 +1 @@ module gno.land/r/docs/adder - -require gno.land/p/moul/txlink v0.0.0-latest diff --git a/examples/gno.land/r/docs/avl_pager/gno.mod b/examples/gno.land/r/docs/avl_pager/gno.mod index 0d05b24bcd0..bc7214f7bc1 100644 --- a/examples/gno.land/r/docs/avl_pager/gno.mod +++ b/examples/gno.land/r/docs/avl_pager/gno.mod @@ -1,6 +1 @@ module gno.land/r/docs/avl_pager - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avl/pager v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 8a4c5851b4c..b510867c485 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -1,8 +1 @@ module gno.land/r/gnoland/blog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/events/gno.mod b/examples/gno.land/r/gnoland/events/gno.mod index bd3e4652b04..50aa3d8fc27 100644 --- a/examples/gno.land/r/gnoland/events/gno.mod +++ b/examples/gno.land/r/gnoland/events/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/events - -require ( - gno.land/p/demo/ownable/exts/authorizable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/faucet/gno.mod b/examples/gno.land/r/gnoland/faucet/gno.mod index 693b0e795cf..6193d111e4f 100644 --- a/examples/gno.land/r/gnoland/faucet/gno.mod +++ b/examples/gno.land/r/gnoland/faucet/gno.mod @@ -1,7 +1 @@ module gno.land/r/gnoland/faucet - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/ghverify/gno.mod b/examples/gno.land/r/gnoland/ghverify/gno.mod index 386bd9293d2..8ffdec663f7 100644 --- a/examples/gno.land/r/gnoland/ghverify/gno.mod +++ b/examples/gno.land/r/gnoland/ghverify/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/ghverify - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/gnorkle/feeds/static v0.0.0-latest - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index 52d01c6d38c..09eb0eb19e1 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -1,10 +1 @@ module gno.land/r/gnoland/home - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest - gno.land/r/gnoland/blog v0.0.0-latest - gno.land/r/gnoland/events v0.0.0-latest - gno.land/r/leon/hof v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/monit/gno.mod b/examples/gno.land/r/gnoland/monit/gno.mod index e67fdaa7d71..6086a3fa21f 100644 --- a/examples/gno.land/r/gnoland/monit/gno.mod +++ b/examples/gno.land/r/gnoland/monit/gno.mod @@ -1,8 +1 @@ module gno.land/r/gnoland/monit - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/watchdog v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/pages/gno.mod b/examples/gno.land/r/gnoland/pages/gno.mod index 31e9ad2c85b..e041fd948bc 100644 --- a/examples/gno.land/r/gnoland/pages/gno.mod +++ b/examples/gno.land/r/gnoland/pages/gno.mod @@ -1,6 +1 @@ module gno.land/r/gnoland/pages - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index 099a8406db4..064fe6d811e 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -1,12 +1 @@ module gno.land/r/gnoland/valopers/v2 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest - gno.land/r/sys/validators/v2 v0.0.0-latest -) diff --git a/examples/gno.land/r/gov/dao/bridge/gno.mod b/examples/gno.land/r/gov/dao/bridge/gno.mod index 3382557573a..9f472eaa464 100644 --- a/examples/gno.land/r/gov/dao/bridge/gno.mod +++ b/examples/gno.land/r/gov/dao/bridge/gno.mod @@ -1,11 +1 @@ module gno.land/r/gov/dao/bridge - -require ( - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/membstore v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/gov/dao/v2 v0.0.0-latest -) diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index 4da6e0a2484..4daf8c600a1 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -1,12 +1 @@ module gno.land/r/gov/dao/v2 - -require ( - gno.land/p/demo/combinederr v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/membstore v0.0.0-latest - gno.land/p/demo/simpledao v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/gov/executor v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/leon/hof/gno.mod b/examples/gno.land/r/leon/hof/gno.mod index feb31992513..f4720eb2b5a 100644 --- a/examples/gno.land/r/leon/hof/gno.mod +++ b/examples/gno.land/r/leon/hof/gno.mod @@ -1,15 +1 @@ module gno.land/r/leon/hof - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avl/pager v0.0.0-latest - gno.land/p/demo/fqname v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/pausable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest -) diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index e7ffc49a37f..56fea265e29 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -1,10 +1 @@ module gno.land/r/leon/home - -require ( - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/demo/art/gnoface v0.0.0-latest - gno.land/r/demo/art/millipede v0.0.0-latest - gno.land/r/demo/mirror v0.0.0-latest - gno.land/r/leon/config v0.0.0-latest - gno.land/r/leon/hof v0.0.0-latest -) diff --git a/examples/gno.land/r/morgan/guestbook/gno.mod b/examples/gno.land/r/morgan/guestbook/gno.mod index 2591643d33d..ac63a4cf8cd 100644 --- a/examples/gno.land/r/morgan/guestbook/gno.mod +++ b/examples/gno.land/r/morgan/guestbook/gno.mod @@ -1,7 +1 @@ module gno.land/r/morgan/guestbook - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest -) diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod index 412666e4171..573a7e139e7 100644 --- a/examples/gno.land/r/morgan/home/gno.mod +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -1,3 +1 @@ module gno.land/r/morgan/home - -require gno.land/r/leon/hof v0.0.0-latest diff --git a/examples/gno.land/r/moul/home/gno.mod b/examples/gno.land/r/moul/home/gno.mod index f42a2c2ced8..91e02df3707 100644 --- a/examples/gno.land/r/moul/home/gno.mod +++ b/examples/gno.land/r/moul/home/gno.mod @@ -1,6 +1 @@ module gno.land/r/moul/home - -require ( - gno.land/r/leon/hof v0.0.0-latest - gno.land/r/moul/config v0.0.0-latest -) diff --git a/examples/gno.land/r/moul/present/gno.mod b/examples/gno.land/r/moul/present/gno.mod index 3ae0bf2e64d..a0a7777d0ed 100644 --- a/examples/gno.land/r/moul/present/gno.mod +++ b/examples/gno.land/r/moul/present/gno.mod @@ -1,6 +1 @@ module gno.land/r/moul/present - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest -) diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod index 33f9276a409..29d5a74eb0a 100644 --- a/examples/gno.land/r/n2p5/config/gno.mod +++ b/examples/gno.land/r/n2p5/config/gno.mod @@ -1,6 +1 @@ module gno.land/r/n2p5/config - -require ( - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/n2p5/mgroup v0.0.0-latest -) diff --git a/examples/gno.land/r/n2p5/haystack/gno.mod b/examples/gno.land/r/n2p5/haystack/gno.mod index 9203eb2d3b1..17c131b8370 100644 --- a/examples/gno.land/r/n2p5/haystack/gno.mod +++ b/examples/gno.land/r/n2p5/haystack/gno.mod @@ -1,8 +1 @@ module gno.land/r/n2p5/haystack - -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/n2p5/haystack v0.0.0-latest - gno.land/p/n2p5/haystack/needle v0.0.0-latest -) diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod index 779aa914989..3b6ddbf86bb 100644 --- a/examples/gno.land/r/n2p5/home/gno.mod +++ b/examples/gno.land/r/n2p5/home/gno.mod @@ -1,7 +1 @@ module gno.land/r/n2p5/home - -require ( - gno.land/p/n2p5/chonk v0.0.0-latest - gno.land/r/leon/hof v0.0.0-latest - gno.land/r/n2p5/config v0.0.0-latest -) diff --git a/examples/gno.land/r/stefann/home/gno.mod b/examples/gno.land/r/stefann/home/gno.mod index dd556e7f817..89071aa70fb 100644 --- a/examples/gno.land/r/stefann/home/gno.mod +++ b/examples/gno.land/r/stefann/home/gno.mod @@ -1,9 +1 @@ module gno.land/r/stefann/home - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/stefann/registry v0.0.0-latest -) diff --git a/examples/gno.land/r/stefann/registry/gno.mod b/examples/gno.land/r/stefann/registry/gno.mod index 5ed3e4916e2..7ef0c32030f 100644 --- a/examples/gno.land/r/stefann/registry/gno.mod +++ b/examples/gno.land/r/stefann/registry/gno.mod @@ -1,3 +1 @@ module gno.land/r/stefann/registry - -require gno.land/p/demo/ownable v0.0.0-latest diff --git a/examples/gno.land/r/sys/params/gno.mod b/examples/gno.land/r/sys/params/gno.mod index 4b4c2bf790f..c633412ced7 100644 --- a/examples/gno.land/r/sys/params/gno.mod +++ b/examples/gno.land/r/sys/params/gno.mod @@ -1,6 +1 @@ module gno.land/r/sys/params - -require ( - gno.land/p/demo/dao v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/users/gno.mod b/examples/gno.land/r/sys/users/gno.mod index 774a364a272..e5e84a49faf 100644 --- a/examples/gno.land/r/sys/users/gno.mod +++ b/examples/gno.land/r/sys/users/gno.mod @@ -1,6 +1 @@ module gno.land/r/sys/users - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod index db94a208902..beae6e95d34 100644 --- a/examples/gno.land/r/sys/validators/v2/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -1,13 +1 @@ module gno.land/r/sys/validators/v2 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/nt/poa v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/x/manfred_outfmt/gno.mod b/examples/gno.land/r/x/manfred_outfmt/gno.mod index 7044f0f72b3..e8165d847c9 100644 --- a/examples/gno.land/r/x/manfred_outfmt/gno.mod +++ b/examples/gno.land/r/x/manfred_outfmt/gno.mod @@ -1,5 +1,3 @@ // Draft module gno.land/r/x/manfred_outfmt - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index ea692bcaf0d..778121d59ed 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -168,7 +168,7 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C var tx std.Tx // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + memPkg := gno.MustReadMemPackage(pkg.Dir, pkg.Name) err := memPkg.Validate() if err != nil { return tx, fmt.Errorf("invalid package: %w", err) diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 235b9581ae0..2a0a4cf1106 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -19,7 +19,9 @@ import ( "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" "github.com/gnolang/gno/tm2/pkg/bft/node" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" @@ -743,8 +745,20 @@ func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error { // Override package info with mod infos currentPkg.Name = gm.Module.Mod.Path currentPkg.Draft = gm.Draft - for _, req := range gm.Require { - currentPkg.Requires = append(currentPkg.Requires, req.Mod.Path) + + pkg, err := gnolang.ReadMemPackage(currentPkg.Dir, currentPkg.Name) + if err != nil { + return fmt.Errorf("unable to read package at %q: %w", currentPkg.Dir, err) + } + imports, err := packages.Imports(pkg) + if err != nil { + return fmt.Errorf("unable to load package imports in %q: %w", currentPkg.Dir, err) + } + for _, imp := range imports { + if imp == currentPkg.Name || gnolang.IsStdlib(imp) { + continue + } + currentPkg.Imports = append(currentPkg.Imports, imp) } } @@ -758,7 +772,7 @@ func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error { pl.add(currentPkg) // Add requirements to the queue - for _, pkgPath := range currentPkg.Requires { + for _, pkgPath := range currentPkg.Imports { fullPath := filepath.Join(modroot, pkgPath) queue = append(queue, gnomod.Pkg{Dir: fullPath}) } diff --git a/gno.land/pkg/keyscli/addpkg.go b/gno.land/pkg/keyscli/addpkg.go index 37463d13b5c..eb6e727fedd 100644 --- a/gno.land/pkg/keyscli/addpkg.go +++ b/gno.land/pkg/keyscli/addpkg.go @@ -96,7 +96,7 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error { } // open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(cfg.PkgDir, cfg.PkgPath) + memPkg := gno.MustReadMemPackage(cfg.PkgDir, cfg.PkgPath) if memPkg.IsEmpty() { panic(fmt.Sprintf("found an empty package %q", cfg.PkgPath)) } diff --git a/gno.land/pkg/keyscli/run.go b/gno.land/pkg/keyscli/run.go index b0e05fe5a84..00b2be585c6 100644 --- a/gno.land/pkg/keyscli/run.go +++ b/gno.land/pkg/keyscli/run.go @@ -92,7 +92,7 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error { return fmt.Errorf("could not read source path: %q, %w", sourcePath, err) } if info.IsDir() { - memPkg = gno.ReadMemPackage(sourcePath, "") + memPkg = gno.MustReadMemPackage(sourcePath, "") } else { // is file b, err := os.ReadFile(sourcePath) if err != nil { diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index e4f7a8543a7..00a0544cad6 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -186,7 +186,7 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { // does not exist. panic(fmt.Sprintf("failed loading stdlib %q: does not exist", pkgPath)) } - memPkg := gno.ReadMemPackage(stdlibPath, pkgPath) + memPkg := gno.MustReadMemPackage(stdlibPath, pkgPath) if memPkg.IsEmpty() { // no gno files are present panic(fmt.Sprintf("failed loading stdlib %q: not a valid MemPackage", pkgPath)) diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 1ce648acb19..38f35ab7110 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -29,7 +29,7 @@ func NewMsgAddPackage(creator crypto.Address, pkgPath string, files []*gnovm.Mem var pkgName string for _, file := range files { if strings.HasSuffix(file.Name, ".gno") { - pkgName = string(gno.PackageNameFromFileBody(file.Name, file.Body)) + pkgName = string(gno.MustPackageNameFromFileBody(file.Name, file.Body)) break } } @@ -156,7 +156,7 @@ var _ std.Msg = MsgRun{} func NewMsgRun(caller crypto.Address, send std.Coins, files []*gnovm.MemFile) MsgRun { for _, file := range files { if strings.HasSuffix(file.Name, ".gno") { - pkgName := string(gno.PackageNameFromFileBody(file.Name, file.Body)) + pkgName := string(gno.MustPackageNameFromFileBody(file.Name, file.Body)) if pkgName != "main" { panic("package name should be 'main'") } diff --git a/gnovm/cmd/gno/download_deps.go b/gnovm/cmd/gno/download_deps.go new file mode 100644 index 00000000000..d19de9dd338 --- /dev/null +++ b/gnovm/cmd/gno/download_deps.go @@ -0,0 +1,86 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" + "github.com/gnolang/gno/tm2/pkg/commands" + "golang.org/x/mod/module" +) + +// downloadDeps recursively fetches the imports of a local package while following a given gno.mod replace directives +func downloadDeps(io commands.IO, pkgDir string, gnoMod *gnomod.File, fetcher pkgdownload.PackageFetcher) error { + if fetcher == nil { + return errors.New("fetcher is nil") + } + + pkg, err := gnolang.ReadMemPackage(pkgDir, gnoMod.Module.Mod.Path) + if err != nil { + return fmt.Errorf("read package at %q: %w", pkgDir, err) + } + imports, err := packages.Imports(pkg) + if err != nil { + return fmt.Errorf("read imports at %q: %w", pkgDir, err) + } + + for _, pkgPath := range imports { + resolved := gnoMod.Resolve(module.Version{Path: pkgPath}) + resolvedPkgPath := resolved.Path + + if !isRemotePkgPath(resolvedPkgPath) { + continue + } + + depDir := gnomod.PackageDir("", module.Version{Path: resolvedPkgPath}) + + if err := downloadPackage(io, resolvedPkgPath, depDir, fetcher); err != nil { + return fmt.Errorf("download import %q of %q: %w", resolvedPkgPath, pkgDir, err) + } + + if err := downloadDeps(io, depDir, gnoMod, fetcher); err != nil { + return err + } + } + + return nil +} + +// downloadPackage downloads a remote gno package by pkg path and store it at dst +func downloadPackage(io commands.IO, pkgPath string, dst string, fetcher pkgdownload.PackageFetcher) error { + modFilePath := filepath.Join(dst, "gno.mod") + + if _, err := os.Stat(modFilePath); err == nil { + // modfile exists in modcache, do nothing + return nil + } else if !os.IsNotExist(err) { + return fmt.Errorf("stat downloaded module %q at %q: %w", pkgPath, dst, err) + } + + io.ErrPrintfln("gno: downloading %s", pkgPath) + + if err := pkgdownload.Download(pkgPath, dst, fetcher); err != nil { + return err + } + + // We need to write a marker file for each downloaded package. + // For example: if you first download gno.land/r/foo/bar then download gno.land/r/foo, + // we need to know that gno.land/r/foo is not downloaded yet. + // We do this by checking for the presence of gno.land/r/foo/gno.mod + if err := os.WriteFile(modFilePath, []byte("module "+pkgPath+"\n"), 0o644); err != nil { + return fmt.Errorf("write modfile at %q: %w", modFilePath, err) + } + + return nil +} + +// isRemotePkgPath determines whether s is a remote pkg path, i.e.: not a filepath nor a standard library +func isRemotePkgPath(s string) bool { + return !strings.HasPrefix(s, ".") && !filepath.IsAbs(s) && !gnolang.IsStdlib(s) +} diff --git a/gnovm/cmd/gno/download_deps_test.go b/gnovm/cmd/gno/download_deps_test.go new file mode 100644 index 00000000000..3ccfdb0055e --- /dev/null +++ b/gnovm/cmd/gno/download_deps_test.go @@ -0,0 +1,152 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" +) + +func TestDownloadDeps(t *testing.T) { + for _, tc := range []struct { + desc string + pkgPath string + modFile gnomod.File + errorShouldContain string + requirements []string + ioErrContains []string + }{ + { + desc: "not_exists", + pkgPath: "gno.land/p/demo/does_not_exists", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + }, + errorShouldContain: "query files list for pkg \"gno.land/p/demo/does_not_exists\": package \"gno.land/p/demo/does_not_exists\" is not available", + }, { + desc: "fetch_gno.land/p/demo/avl", + pkgPath: "gno.land/p/demo/avl", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + }, + requirements: []string{"avl"}, + ioErrContains: []string{ + "gno: downloading gno.land/p/demo/avl", + }, + }, { + desc: "fetch_gno.land/p/demo/blog6", + pkgPath: "gno.land/p/demo/blog", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + }, + requirements: []string{"avl", "blog", "ufmt", "mux"}, + ioErrContains: []string{ + "gno: downloading gno.land/p/demo/blog", + "gno: downloading gno.land/p/demo/avl", + "gno: downloading gno.land/p/demo/ufmt", + }, + }, { + desc: "fetch_replace_gno.land/p/demo/avl", + pkgPath: "gno.land/p/demo/replaced_avl", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + Replace: []*modfile.Replace{{ + Old: module.Version{Path: "gno.land/p/demo/replaced_avl"}, + New: module.Version{Path: "gno.land/p/demo/avl"}, + }}, + }, + requirements: []string{"avl"}, + ioErrContains: []string{ + "gno: downloading gno.land/p/demo/avl", + }, + }, { + desc: "fetch_replace_local", + pkgPath: "gno.land/p/demo/foo", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + Replace: []*modfile.Replace{{ + Old: module.Version{Path: "gno.land/p/demo/foo"}, + New: module.Version{Path: "../local_foo"}, + }}, + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + mockErr := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + dirPath := t.TempDir() + + err := os.WriteFile(filepath.Join(dirPath, "main.gno"), []byte(fmt.Sprintf("package main\n\n import %q\n", tc.pkgPath)), 0o644) + require.NoError(t, err) + + tmpGnoHome := t.TempDir() + t.Setenv("GNOHOME", tmpGnoHome) + + fetcher := examplespkgfetcher.New() + + // gno: downloading dependencies + err = downloadDeps(io, dirPath, &tc.modFile, fetcher) + if tc.errorShouldContain != "" { + require.ErrorContains(t, err, tc.errorShouldContain) + } else { + require.Nil(t, err) + + // Read dir + entries, err := os.ReadDir(filepath.Join(tmpGnoHome, "pkg", "mod", "gno.land", "p", "demo")) + if !os.IsNotExist(err) { + require.Nil(t, err) + } + + // Check dir entries + assert.Equal(t, len(tc.requirements), len(entries)) + for _, e := range entries { + assert.Contains(t, tc.requirements, e.Name()) + } + + // Check logs + for _, c := range tc.ioErrContains { + assert.Contains(t, mockErr.String(), c) + } + + mockErr.Reset() + + // Try fetching again. Should be cached + downloadDeps(io, dirPath, &tc.modFile, fetcher) + for _, c := range tc.ioErrContains { + assert.NotContains(t, mockErr.String(), c) + } + } + }) + } +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go new file mode 100644 index 00000000000..1642c62d21e --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go @@ -0,0 +1,52 @@ +// Package examplespkgfetcher provides an implementation of [pkgdownload.PackageFetcher] +// to fetch packages from the examples folder at GNOROOT +package examplespkgfetcher + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" +) + +type ExamplesPackageFetcher struct{} + +var _ pkgdownload.PackageFetcher = (*ExamplesPackageFetcher)(nil) + +func New() pkgdownload.PackageFetcher { + return &ExamplesPackageFetcher{} +} + +// FetchPackage implements [pkgdownload.PackageFetcher]. +func (e *ExamplesPackageFetcher) FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) { + pkgDir := filepath.Join(gnoenv.RootDir(), "examples", filepath.FromSlash(pkgPath)) + + entries, err := os.ReadDir(pkgDir) + if os.IsNotExist(err) { + return nil, fmt.Errorf("query files list for pkg %q: package %q is not available", pkgPath, pkgPath) + } else if err != nil { + return nil, err + } + + res := []*gnovm.MemFile{} + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + filePath := filepath.Join(pkgDir, name) + + body, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("read file at %q: %w", filePath, err) + } + + res = append(res, &gnovm.MemFile{Name: name, Body: string(body)}) + } + + return res, nil +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go b/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go new file mode 100644 index 00000000000..722cab01555 --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go @@ -0,0 +1,30 @@ +// Package pkgdownload provides interfaces and utility functions to download gno packages files. +package pkgdownload + +import ( + "fmt" + "os" + "path/filepath" +) + +// Download downloads the package identified by `pkgPath` in the directory at `dst` using the provided [PackageFetcher]. +// The directory at `dst` is created if it does not exists. +func Download(pkgPath string, dst string, fetcher PackageFetcher) error { + files, err := fetcher.FetchPackage(pkgPath) + if err != nil { + return err + } + + if err := os.MkdirAll(dst, 0o744); err != nil { + return err + } + + for _, file := range files { + fileDst := filepath.Join(dst, file.Name) + if err := os.WriteFile(fileDst, []byte(file.Body), 0o644); err != nil { + return fmt.Errorf("write file at %q: %w", fileDst, err) + } + } + + return nil +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go new file mode 100644 index 00000000000..79a7a6a54e2 --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go @@ -0,0 +1,7 @@ +package pkgdownload + +import "github.com/gnolang/gno/gnovm" + +type PackageFetcher interface { + FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go new file mode 100644 index 00000000000..a71c1d43719 --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go @@ -0,0 +1,89 @@ +// Package rpcpkgfetcher provides an implementation of [pkgdownload.PackageFetcher] +// to fetch packages from gno.land rpc endpoints +package rpcpkgfetcher + +import ( + "fmt" + "path" + "strings" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type gnoPackageFetcher struct { + remoteOverrides map[string]string +} + +var _ pkgdownload.PackageFetcher = (*gnoPackageFetcher)(nil) + +func New(remoteOverrides map[string]string) pkgdownload.PackageFetcher { + return &gnoPackageFetcher{ + remoteOverrides: remoteOverrides, + } +} + +// FetchPackage implements [pkgdownload.PackageFetcher]. +func (gpf *gnoPackageFetcher) FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) { + rpcURL, err := rpcURLFromPkgPath(pkgPath, gpf.remoteOverrides) + if err != nil { + return nil, fmt.Errorf("get rpc url for pkg path %q: %w", pkgPath, err) + } + + client, err := client.NewHTTPClient(rpcURL) + if err != nil { + return nil, fmt.Errorf("failed to instantiate tm2 client with remote %q: %w", rpcURL, err) + } + defer client.Close() + + data, err := qfile(client, pkgPath) + if err != nil { + return nil, fmt.Errorf("query files list for pkg %q: %w", pkgPath, err) + } + + files := strings.Split(string(data), "\n") + res := make([]*gnovm.MemFile, len(files)) + for i, file := range files { + filePath := path.Join(pkgPath, file) + data, err := qfile(client, filePath) + if err != nil { + return nil, fmt.Errorf("query package file %q: %w", filePath, err) + } + + res[i] = &gnovm.MemFile{Name: file, Body: string(data)} + } + return res, nil +} + +func rpcURLFromPkgPath(pkgPath string, remoteOverrides map[string]string) (string, error) { + parts := strings.Split(pkgPath, "/") + if len(parts) < 2 { + return "", fmt.Errorf("bad pkg path %q", pkgPath) + } + domain := parts[0] + + if override, ok := remoteOverrides[domain]; ok { + return override, nil + } + + // XXX: retrieve host/port from r/sys/zones. + rpcURL := fmt.Sprintf("https://rpc.%s:443", domain) + + return rpcURL, nil +} + +func qfile(c client.Client, pkgPath string) ([]byte, error) { + path := "vm/qfile" + data := []byte(pkgPath) + + qres, err := c.ABCIQuery(path, data) + if err != nil { + return nil, fmt.Errorf("query qfile: %w", err) + } + if qres.Response.Error != nil { + return nil, fmt.Errorf("qfile failed: %w\n%s", qres.Response.Error, qres.Response.Log) + } + + return qres.Response.Data, nil +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go new file mode 100644 index 00000000000..56db5b796de --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go @@ -0,0 +1,53 @@ +package rpcpkgfetcher + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRpcURLFromPkgPath(t *testing.T) { + cases := []struct { + name string + pkgPath string + overrides map[string]string + result string + errorContains string + }{ + { + name: "happy path simple", + pkgPath: "gno.land/p/demo/avl", + result: "https://rpc.gno.land:443", + }, + { + name: "happy path override", + pkgPath: "gno.land/p/demo/avl", + overrides: map[string]string{"gno.land": "https://example.com/rpc:42"}, + result: "https://example.com/rpc:42", + }, + { + name: "happy path override no effect", + pkgPath: "gno.land/p/demo/avl", + overrides: map[string]string{"some.chain": "https://example.com/rpc:42"}, + result: "https://rpc.gno.land:443", + }, + { + name: "error bad pkg path", + pkgPath: "std", + result: "", + errorContains: `bad pkg path "std"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res, err := rpcURLFromPkgPath(c.pkgPath, c.overrides) + if len(c.errorContains) == 0 { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, c.errorContains) + } + require.Equal(t, c.result, res) + }) + } +} diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index ef35cf9af83..6d5399ca932 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -102,7 +102,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { targetPath = filepath.Dir(pkgPath) } - memPkg := gno.ReadMemPackage(targetPath, targetPath) + memPkg := gno.MustReadMemPackage(targetPath, targetPath) tm := test.Machine(testStore, stdout, memPkg.Path) defer tm.Release() diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go index 76c67f6807b..2ea3e31f977 100644 --- a/gnovm/cmd/gno/main_test.go +++ b/gnovm/cmd/gno/main_test.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" - + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" ) func TestMain_Gno(t *testing.T) { @@ -60,10 +60,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { mockErr := bytes.NewBufferString("") if !test.noTmpGnohome { - tmpGnoHome, err := os.MkdirTemp(os.TempDir(), "gnotesthome_") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpGnoHome) }) - t.Setenv("GNOHOME", tmpGnoHome) + t.Setenv("GNOHOME", t.TempDir()) } checkOutputs := func(t *testing.T) { @@ -131,6 +128,8 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { io.SetOut(commands.WriteNopCloser(mockOut)) io.SetErr(commands.WriteNopCloser(mockErr)) + testPackageFetcher = examplespkgfetcher.New() + err := newGnocliCmd(io).ParseAndRun(context.Background(), test.args) if errShouldBeEmpty { diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 67af5631c71..f762b070fe4 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -4,19 +4,22 @@ import ( "context" "flag" "fmt" - "go/parser" - "go/token" "os" "path/filepath" - "sort" "strings" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/errors" "go.uber.org/multierr" ) +// testPackageFetcher allows to override the package fetcher during tests. +var testPackageFetcher pkgdownload.PackageFetcher + func newModCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ @@ -123,23 +126,17 @@ For example: } type modDownloadCfg struct { - remote string - verbose bool + remoteOverrides string } +const remoteOverridesArgName = "remote-overrides" + func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( - &c.remote, - "remote", - "gno.land:26657", - "remote for fetching gno modules", - ) - - fs.BoolVar( - &c.verbose, - "v", - false, - "verbose output when running", + &c.remoteOverrides, + remoteOverridesArgName, + "", + "chain-domain=rpc-url comma-separated list", ) } @@ -148,6 +145,17 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { return flag.ErrHelp } + fetcher := testPackageFetcher + if fetcher == nil { + remoteOverrides, err := parseRemoteOverrides(cfg.remoteOverrides) + if err != nil { + return fmt.Errorf("invalid %s flag: %w", remoteOverridesArgName, err) + } + fetcher = rpcpkgfetcher.New(remoteOverrides) + } else if len(cfg.remoteOverrides) != 0 { + return fmt.Errorf("can't use %s flag with a custom package fetcher", remoteOverridesArgName) + } + path, err := os.Getwd() if err != nil { return err @@ -176,23 +184,26 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { return fmt.Errorf("validate: %w", err) } - // fetch dependencies - if err := gnoMod.FetchDeps(gnomod.ModCachePath(), cfg.remote, cfg.verbose); err != nil { - return fmt.Errorf("fetch: %w", err) + if err := downloadDeps(io, path, gnoMod, fetcher); err != nil { + return err } - gomod, err := gnomod.GnoToGoMod(*gnoMod) - if err != nil { - return fmt.Errorf("sanitize: %w", err) - } + return nil +} - // write go.mod file - err = gomod.Write(filepath.Join(path, "go.mod")) - if err != nil { - return fmt.Errorf("write go.mod file: %w", err) +func parseRemoteOverrides(arg string) (map[string]string, error) { + pairs := strings.Split(arg, ",") + res := make(map[string]string, len(pairs)) + for _, pair := range pairs { + parts := strings.Split(pair, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("expected 2 parts in chain-domain=rpc-url pair %q", arg) + } + domain := strings.TrimSpace(parts[0]) + rpcURL := strings.TrimSpace(parts[1]) + res[domain] = rpcURL } - - return nil + return res, nil } func execModInit(args []string) error { @@ -276,26 +287,6 @@ func modTidyOnce(cfg *modTidyCfg, wd, pkgdir string, io commands.IO) error { return err } - // Drop all existing requires - for _, r := range gm.Require { - gm.DropRequire(r.Mod.Path) - } - - imports, err := getGnoPackageImports(pkgdir) - if err != nil { - return err - } - for _, im := range imports { - // skip if importpath is modulepath - if im == gm.Module.Mod.Path { - continue - } - gm.AddRequire(im, "v0.0.0-latest") - if cfg.verbose { - io.ErrPrintfln(" %s", im) - } - } - gm.Write(fname) return nil } @@ -366,79 +357,22 @@ func getImportToFilesMap(pkgPath string) (map[string][]string, error) { if strings.HasSuffix(filename, "_filetest.gno") { continue } - imports, err := getGnoFileImports(filepath.Join(pkgPath, filename)) + + data, err := os.ReadFile(filepath.Join(pkgPath, filename)) if err != nil { return nil, err } - - for _, imp := range imports { - m[imp] = append(m[imp], filename) - } - } - return m, nil -} - -// getGnoPackageImports returns the list of gno imports from a given path. -// Note: It ignores subdirs. Since right now we are still deciding on -// how to handle subdirs. -// See: -// - https://github.com/gnolang/gno/issues/1024 -// - https://github.com/gnolang/gno/issues/852 -// -// TODO: move this to better location. -func getGnoPackageImports(path string) ([]string, error) { - entries, err := os.ReadDir(path) - if err != nil { - return nil, err - } - - allImports := make([]string, 0) - seen := make(map[string]struct{}) - for _, e := range entries { - filename := e.Name() - if ext := filepath.Ext(filename); ext != ".gno" { - continue - } - if strings.HasSuffix(filename, "_filetest.gno") { - continue - } - imports, err := getGnoFileImports(filepath.Join(path, filename)) + imports, _, err := packages.FileImports(filename, string(data)) if err != nil { return nil, err } - for _, im := range imports { - if !strings.HasPrefix(im, "gno.land/") { - continue - } - if _, ok := seen[im]; ok { - continue + + for _, imp := range imports { + if imp.Error != nil { + return nil, err } - allImports = append(allImports, im) - seen[im] = struct{}{} + m[imp.PkgPath] = append(m[imp.PkgPath], filename) } } - sort.Strings(allImports) - - return allImports, nil -} - -func getGnoFileImports(fname string) ([]string, error) { - if !strings.HasSuffix(fname, ".gno") { - return nil, fmt.Errorf("not a gno file: %q", fname) - } - data, err := os.ReadFile(fname) - if err != nil { - return nil, err - } - fs := token.NewFileSet() - f, err := parser.ParseFile(fs, fname, data, parser.ImportsOnly) - if err != nil { - return nil, err - } - res := make([]string, 0) - for _, im := range f.Imports { - importPath := strings.TrimPrefix(strings.TrimSuffix(im.Path.Value, `"`), `"`) - res = append(res, importPath) - } - return res, nil + return m, nil } diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index d35ab311b6c..afce25597cd 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -1,12 +1,7 @@ package main import ( - "os" - "path/filepath" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestModApp(t *testing.T) { @@ -44,24 +39,19 @@ func TestModApp(t *testing.T) { args: []string{"mod", "download"}, testDir: "../../tests/integ/require_remote_module", simulateExternalRepo: true, + stderrShouldContain: "gno: downloading gno.land/p/demo/avl", }, { args: []string{"mod", "download"}, testDir: "../../tests/integ/require_invalid_module", simulateExternalRepo: true, - errShouldContain: "fetch: writepackage: querychain", + stderrShouldContain: "gno: downloading gno.land/p/demo/notexists", + errShouldContain: "query files list for pkg \"gno.land/p/demo/notexists\": package \"gno.land/p/demo/notexists\" is not available", }, { args: []string{"mod", "download"}, - testDir: "../../tests/integ/invalid_module_version1", + testDir: "../../tests/integ/require_std_lib", simulateExternalRepo: true, - errShouldContain: "usage: require module/path v1.2.3", - }, - { - args: []string{"mod", "download"}, - testDir: "../../tests/integ/invalid_module_version2", - simulateExternalRepo: true, - errShouldContain: "invalid: must be of the form v1.2.3", }, { args: []string{"mod", "download"}, @@ -72,12 +62,14 @@ func TestModApp(t *testing.T) { args: []string{"mod", "download"}, testDir: "../../tests/integ/replace_with_module", simulateExternalRepo: true, + stderrShouldContain: "gno: downloading gno.land/p/demo/users", }, { args: []string{"mod", "download"}, testDir: "../../tests/integ/replace_with_invalid_module", simulateExternalRepo: true, - errShouldContain: "fetch: writepackage: querychain", + stderrShouldContain: "gno: downloading gno.land/p/demo/notexists", + errShouldContain: "query files list for pkg \"gno.land/p/demo/notexists\": package \"gno.land/p/demo/notexists\" is not available", }, // test `gno mod init` with no module name @@ -158,12 +150,6 @@ func TestModApp(t *testing.T) { simulateExternalRepo: true, errShouldContain: "could not read gno.mod file", }, - { - args: []string{"mod", "tidy"}, - testDir: "../../tests/integ/invalid_module_version1", - simulateExternalRepo: true, - errShouldContain: "error parsing gno.mod file at", - }, { args: []string{"mod", "tidy"}, testDir: "../../tests/integ/minimalist_gnomod", @@ -179,12 +165,6 @@ func TestModApp(t *testing.T) { testDir: "../../tests/integ/valid2", simulateExternalRepo: true, }, - { - args: []string{"mod", "tidy"}, - testDir: "../../tests/integ/invalid_gno_file", - simulateExternalRepo: true, - errShouldContain: "expected 'package', found packag", - }, // test `gno mod why` { @@ -199,12 +179,6 @@ func TestModApp(t *testing.T) { simulateExternalRepo: true, errShouldContain: "could not read gno.mod file", }, - { - args: []string{"mod", "why", "std"}, - testDir: "../../tests/integ/invalid_module_version1", - simulateExternalRepo: true, - errShouldContain: "error parsing gno.mod file at", - }, { args: []string{"mod", "why", "std"}, testDir: "../../tests/integ/invalid_gno_file", @@ -239,122 +213,6 @@ valid.gno `, }, } - testMainCaseRun(t, tc) -} - -func TestGetGnoImports(t *testing.T) { - workingDir, err := os.Getwd() - require.NoError(t, err) - - // create external dir - tmpDir, cleanUpFn := createTmpDir(t) - defer cleanUpFn() - - // cd to tmp directory - os.Chdir(tmpDir) - defer os.Chdir(workingDir) - - files := []struct { - name, data string - }{ - { - name: "file1.gno", - data: ` - package tmp - - import ( - "std" - - "gno.land/p/demo/pkg1" - ) - `, - }, - { - name: "file2.gno", - data: ` - package tmp - - import ( - "gno.land/p/demo/pkg1" - "gno.land/p/demo/pkg2" - ) - `, - }, - { - name: "file1_test.gno", - data: ` - package tmp - - import ( - "testing" - - "gno.land/p/demo/testpkg" - ) - `, - }, - { - name: "z_0_filetest.gno", - data: ` - package main - - import ( - "gno.land/p/demo/filetestpkg" - ) - `, - }, - - // subpkg files - { - name: filepath.Join("subtmp", "file1.gno"), - data: ` - package subtmp - - import ( - "std" - - "gno.land/p/demo/subpkg1" - ) - `, - }, - { - name: filepath.Join("subtmp", "file2.gno"), - data: ` - package subtmp - - import ( - "gno.land/p/demo/subpkg1" - "gno.land/p/demo/subpkg2" - ) - `, - }, - } - - // Expected list of imports - // - ignore subdirs - // - ignore duplicate - // - ignore *_filetest.gno - // - should be sorted - expected := []string{ - "gno.land/p/demo/pkg1", - "gno.land/p/demo/pkg2", - "gno.land/p/demo/testpkg", - } - - // Create subpkg dir - err = os.Mkdir("subtmp", 0o700) - require.NoError(t, err) - - // Create files - for _, f := range files { - err = os.WriteFile(f.name, []byte(f.data), 0o644) - require.NoError(t, err) - } - imports, err := getGnoPackageImports(tmpDir) - require.NoError(t, err) - - require.Equal(t, len(expected), len(imports)) - for i := range imports { - assert.Equal(t, expected[i], imports[i]) - } + testMainCaseRun(t, tc) } diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 511a704dd7d..fec0de7c221 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -209,7 +209,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { } } - memPkg := gno.ReadMemPackage(pkg.Dir, gnoPkgPath) + memPkg := gno.MustReadMemPackage(pkg.Dir, gnoPkgPath) startedAt := time.Now() hasError := catchRuntimeError(gnoPkgPath, io.Err(), func() { diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go index 19d312f6826..eadbec7d464 100644 --- a/gnovm/pkg/doc/dirs.go +++ b/gnovm/pkg/doc/dirs.go @@ -9,10 +9,15 @@ import ( "os" "path" "path/filepath" + "slices" "sort" "strings" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" + "golang.org/x/mod/module" ) // A bfsDir describes a directory holding code by specifying @@ -60,25 +65,27 @@ func newDirs(dirs []string, modDirs []string) *bfsDirs { dir: mdir, importPath: gm.Module.Mod.Path, }) - roots = append(roots, getGnoModDirs(gm)...) + roots = append(roots, getGnoModDirs(gm, mdir)...) } go d.walk(roots) return d } -func getGnoModDirs(gm *gnomod.File) []bfsDir { +func getGnoModDirs(gm *gnomod.File, root string) []bfsDir { // cmd/go makes use of the go list command, we don't have that here. - dirs := make([]bfsDir, 0, len(gm.Require)) - for _, r := range gm.Require { - mv := gm.Resolve(r) + imports := packageImportsRecursive(root, gm.Module.Mod.Path) + + dirs := make([]bfsDir, 0, len(imports)) + for _, r := range imports { + mv := gm.Resolve(module.Version{Path: r}) path := gnomod.PackageDir("", mv) if _, err := os.Stat(path); err != nil { // only give directories which actually exist and don't give // an error when accessing if !os.IsNotExist(err) { - log.Println("open source directories from gno.mod:", err) + log.Println("open source directories from import:", err) } continue } @@ -91,6 +98,45 @@ func getGnoModDirs(gm *gnomod.File) []bfsDir { return dirs } +func packageImportsRecursive(root string, pkgPath string) []string { + pkg, err := gnolang.ReadMemPackage(root, pkgPath) + if err != nil { + // ignore invalid packages + pkg = &gnovm.MemPackage{} + } + + res, err := packages.Imports(pkg) + if err != nil { + // ignore packages with invalid imports + res = nil + } + + entries, err := os.ReadDir(root) + if err != nil { + // ignore unreadable dirs + entries = nil + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + dirName := entry.Name() + sub := packageImportsRecursive(filepath.Join(root, dirName), path.Join(pkgPath, dirName)) + + for _, imp := range sub { + if !slices.Contains(res, imp) { + res = append(res, imp) + } + } + } + + sort.Strings(res) + + return res +} + // Reset puts the scan back at the beginning. func (d *bfsDirs) Reset() { d.offset = 0 diff --git a/gnovm/pkg/doc/dirs_test.go b/gnovm/pkg/doc/dirs_test.go index 8659f3cbfcb..3139298a7ae 100644 --- a/gnovm/pkg/doc/dirs_test.go +++ b/gnovm/pkg/doc/dirs_test.go @@ -63,18 +63,9 @@ func TestNewDirs_invalidModDir(t *testing.T) { func tNewDirs(t *testing.T) (string, *bfsDirs) { t.Helper() - // modify GNO_HOME to testdata/dirsdep -- this allows us to test + // modify GNOHOME to testdata/dirsdep -- this allows us to test // dependency lookup by dirs. - old, ex := os.LookupEnv("GNO_HOME") - os.Setenv("GNO_HOME", wdJoin(t, "testdata/dirsdep")) - - t.Cleanup(func() { - if ex { - os.Setenv("GNO_HOME", old) - } else { - os.Unsetenv("GNO_HOME") - } - }) + t.Setenv("GNOHOME", wdJoin(t, "testdata/dirsdep")) return wdJoin(t, "testdata"), newDirs([]string{wdJoin(t, "testdata/dirs")}, []string{wdJoin(t, "testdata/dirsmod")}) diff --git a/gnovm/pkg/doc/testdata/dirsmod/a.gno b/gnovm/pkg/doc/testdata/dirsmod/a.gno new file mode 100644 index 00000000000..ee57c47dff5 --- /dev/null +++ b/gnovm/pkg/doc/testdata/dirsmod/a.gno @@ -0,0 +1,9 @@ +package dirsmod + +import ( + "dirs.mod/dep" +) + +func foo() { + dep.Bar() +} diff --git a/gnovm/pkg/doc/testdata/dirsmod/gno.mod b/gnovm/pkg/doc/testdata/dirsmod/gno.mod index 6c8008b958c..34d825571cc 100644 --- a/gnovm/pkg/doc/testdata/dirsmod/gno.mod +++ b/gnovm/pkg/doc/testdata/dirsmod/gno.mod @@ -1,5 +1 @@ -module dirs.mod/prefix - -require ( - dirs.mod/dep v0.0.0 -) +module dirs.mod/prefix \ No newline at end of file diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go index 09be600b198..2c82f6d8f29 100644 --- a/gnovm/pkg/gnolang/files_test.go +++ b/gnovm/pkg/gnolang/files_test.go @@ -138,7 +138,7 @@ func TestStdlibs(t *testing.T) { } fp := filepath.Join(dir, path) - memPkg := gnolang.ReadMemPackage(fp, path) + memPkg := gnolang.MustReadMemPackage(fp, path) t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) { capture, opts := sharedCapture, sharedOpts switch memPkg.Path { diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 3368c7c7bde..8d3d6d8a2cc 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -1132,14 +1132,23 @@ type FileSet struct { // PackageNameFromFileBody extracts the package name from the given Gno code body. // The 'name' parameter is used for better error traces, and 'body' contains the Gno code. -func PackageNameFromFileBody(name, body string) Name { +func PackageNameFromFileBody(name, body string) (Name, error) { fset := token.NewFileSet() astFile, err := parser.ParseFile(fset, name, body, parser.PackageClauseOnly) if err != nil { - panic(err) + return "", err } - return Name(astFile.Name.Name) + return Name(astFile.Name.Name), nil +} + +// MustPackageNameFromFileBody is a wrapper around [PackageNameFromFileBody] that panics on error. +func MustPackageNameFromFileBody(name, body string) Name { + pkgName, err := PackageNameFromFileBody(name, body) + if err != nil { + panic(err) + } + return pkgName } // ReadMemPackage initializes a new MemPackage by reading the OS directory @@ -1152,10 +1161,10 @@ func PackageNameFromFileBody(name, body string) Name { // // NOTE: panics if package name is invalid (characters must be alphanumeric or _, // lowercase, and must start with a letter). -func ReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { +func ReadMemPackage(dir string, pkgPath string) (*gnovm.MemPackage, error) { files, err := os.ReadDir(dir) if err != nil { - panic(err) + return nil, err } allowedFiles := []string{ // make case insensitive? "LICENSE", @@ -1186,24 +1195,36 @@ func ReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { return ReadMemPackageFromList(list, pkgPath) } +// MustReadMemPackage is a wrapper around [ReadMemPackage] that panics on error. +func MustReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { + pkg, err := ReadMemPackage(dir, pkgPath) + if err != nil { + panic(err) + } + return pkg +} + // ReadMemPackageFromList creates a new [gnovm.MemPackage] with the specified pkgPath, // containing the contents of all the files provided in the list slice. // No parsing or validation is done on the filenames. // -// NOTE: panics if package name is invalid (characters must be alphanumeric or _, +// NOTE: errors out if package name is invalid (characters must be alphanumeric or _, // lowercase, and must start with a letter). -func ReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { +func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, error) { memPkg := &gnovm.MemPackage{Path: pkgPath} var pkgName Name for _, fpath := range list { fname := filepath.Base(fpath) bz, err := os.ReadFile(fpath) if err != nil { - panic(err) + return nil, err } // XXX: should check that all pkg names are the same (else package is invalid) if pkgName == "" && strings.HasSuffix(fname, ".gno") { - pkgName = PackageNameFromFileBody(fname, string(bz)) + pkgName, err = PackageNameFromFileBody(fname, string(bz)) + if err != nil { + return nil, err + } if strings.HasSuffix(string(pkgName), "_test") { pkgName = pkgName[:len(pkgName)-len("_test")] } @@ -1217,11 +1238,22 @@ func ReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { // If no .gno files are present, package simply does not exist. if !memPkg.IsEmpty() { - validatePkgName(string(pkgName)) + if err := validatePkgName(string(pkgName)); err != nil { + return nil, err + } memPkg.Name = string(pkgName) } - return memPkg + return memPkg, nil +} + +// MustReadMemPackageFromList is a wrapper around [ReadMemPackageFromList] that panics on error. +func MustReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { + pkg, err := ReadMemPackageFromList(list, pkgPath) + if err != nil { + panic(err) + } + return pkg } // ParseMemPackage executes [ParseFile] on each file of the memPkg, excluding @@ -2140,10 +2172,11 @@ var rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]+$`) // TODO: consider length restrictions. // If this function is changed, ReadMemPackage's documentation should be updated accordingly. -func validatePkgName(name string) { +func validatePkgName(name string) error { if !rePkgName.MatchString(name) { - panic(fmt.Sprintf("cannot create package with invalid name %q", name)) + return fmt.Errorf("cannot create package with invalid name %q", name) } + return nil } const hiddenResultVariable = ".res_" diff --git a/gnovm/pkg/gnomod/fetch.go b/gnovm/pkg/gnomod/fetch.go deleted file mode 100644 index 24aaac2f9d4..00000000000 --- a/gnovm/pkg/gnomod/fetch.go +++ /dev/null @@ -1,30 +0,0 @@ -package gnomod - -import ( - "fmt" - - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" -) - -func queryChain(remote string, qpath string, data []byte) (res *abci.ResponseQuery, err error) { - opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } - cli, err := client.NewHTTPClient(remote) - if err != nil { - return nil, err - } - - qres, err := cli.ABCIQueryWithOptions(qpath, data, opts2) - if err != nil { - return nil, err - } - if qres.Response.Error != nil { - fmt.Printf("Log: %s\n", qres.Response.Log) - return nil, qres.Response.Error - } - - return &qres.Response, nil -} diff --git a/gnovm/pkg/gnomod/file.go b/gnovm/pkg/gnomod/file.go index b6ee95acac8..a1c77b51e45 100644 --- a/gnovm/pkg/gnomod/file.go +++ b/gnovm/pkg/gnomod/file.go @@ -12,12 +12,8 @@ package gnomod import ( "errors" "fmt" - "log" "os" - "path/filepath" - "strings" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "golang.org/x/mod/modfile" "golang.org/x/mod/module" ) @@ -27,51 +23,11 @@ type File struct { Draft bool Module *modfile.Module Go *modfile.Go - Require []*modfile.Require Replace []*modfile.Replace Syntax *modfile.FileSyntax } -// AddRequire sets the first require line for path to version vers, -// preserving any existing comments for that line and removing all -// other lines for path. -// -// If no line currently exists for path, AddRequire adds a new line -// at the end of the last require block. -func (f *File) AddRequire(path, vers string) error { - need := true - for _, r := range f.Require { - if r.Mod.Path == path { - if need { - r.Mod.Version = vers - updateLine(r.Syntax, "require", modfile.AutoQuote(path), vers) - need = false - } else { - markLineAsRemoved(r.Syntax) - *r = modfile.Require{} - } - } - } - - if need { - f.AddNewRequire(path, vers, false) - } - return nil -} - -// AddNewRequire adds a new require line for path at version vers at the end of -// the last require block, regardless of any existing require lines for path. -func (f *File) AddNewRequire(path, vers string, indirect bool) { - line := addLine(f.Syntax, nil, "require", modfile.AutoQuote(path), vers) - r := &modfile.Require{ - Mod: module.Version{Path: path, Version: vers}, - Syntax: line, - } - setIndirect(r, indirect) - f.Require = append(f.Require, r) -} - func (f *File) AddModuleStmt(path string) error { if f.Syntax == nil { f.Syntax = new(modfile.FileSyntax) @@ -107,16 +63,6 @@ func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) } -func (f *File) DropRequire(path string) error { - for _, r := range f.Require { - if r.Mod.Path == path { - markLineAsRemoved(r.Syntax) - *r = modfile.Require{} - } - } - return nil -} - func (f *File) DropReplace(oldPath, oldVers string) error { for _, r := range f.Replace { if r.Old.Path == oldPath && r.Old.Version == oldVers { @@ -136,76 +82,17 @@ func (f *File) Validate() error { return nil } -// Resolve takes a Require directive from File and returns any adequate replacement +// Resolve takes a module version and returns any adequate replacement // following the Replace directives. -func (f *File) Resolve(r *modfile.Require) module.Version { - mod, replaced := isReplaced(r.Mod, f.Replace) +func (f *File) Resolve(m module.Version) module.Version { + if f == nil { + return m + } + mod, replaced := isReplaced(m, f.Replace) if replaced { return mod } - return r.Mod -} - -// FetchDeps fetches and writes gno.mod packages -// in GOPATH/pkg/gnomod/ -func (f *File) FetchDeps(path string, remote string, verbose bool) error { - for _, r := range f.Require { - mod := f.Resolve(r) - if r.Mod.Path != mod.Path { - if modfile.IsDirectoryPath(mod.Path) { - continue - } - } - indirect := "" - if r.Indirect { - indirect = "// indirect" - } - - _, err := os.Stat(PackageDir(path, mod)) - if !os.IsNotExist(err) { - if verbose { - log.Println("cached", mod.Path, indirect) - } - continue - } - if verbose { - log.Println("fetching", mod.Path, indirect) - } - requirements, err := writePackage(remote, path, mod.Path) - if err != nil { - return fmt.Errorf("writepackage: %w", err) - } - - modFile := new(File) - modFile.AddModuleStmt(mod.Path) - for _, req := range requirements { - path := req[1 : len(req)-1] // trim leading and trailing `"` - if strings.HasSuffix(path, modFile.Module.Mod.Path) { - continue - } - - if !gno.IsStdlib(path) { - modFile.AddNewRequire(path, "v0.0.0-latest", true) - } - } - - err = modFile.FetchDeps(path, remote, verbose) - if err != nil { - return err - } - goMod, err := GnoToGoMod(*modFile) - if err != nil { - return err - } - pkgPath := PackageDir(path, mod) - goModFilePath := filepath.Join(pkgPath, "go.mod") - err = goMod.Write(goModFilePath) - if err != nil { - return err - } - } - - return nil + return m } // writes file to the given absolute file path @@ -220,5 +107,5 @@ func (f *File) Write(fname string) error { } func (f *File) Sanitize() { - removeDups(f.Syntax, &f.Require, &f.Replace) + removeDups(f.Syntax, &f.Replace) } diff --git a/gnovm/pkg/gnomod/file_test.go b/gnovm/pkg/gnomod/file_test.go deleted file mode 100644 index a64c2794a65..00000000000 --- a/gnovm/pkg/gnomod/file_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package gnomod - -import ( - "bytes" - "log" - "os" - "path/filepath" - "testing" - - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/mod/modfile" - "golang.org/x/mod/module" -) - -const testRemote string = "gno.land:26657" // XXX(race condition): test with a local node so that this test is consistent with git and not with a deploy - -func TestFetchDeps(t *testing.T) { - for _, tc := range []struct { - desc string - modFile File - errorShouldContain string - requirements []string - stdOutContains []string - cachedStdOutContains []string - }{ - { - desc: "not_exists", - modFile: File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: "testFetchDeps", - }, - }, - Require: []*modfile.Require{ - { - Mod: module.Version{ - Path: "gno.land/p/demo/does_not_exists", - Version: "v0.0.0", - }, - }, - }, - }, - errorShouldContain: "querychain (gno.land/p/demo/does_not_exists)", - }, { - desc: "fetch_gno.land/p/demo/avl", - modFile: File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: "testFetchDeps", - }, - }, - Require: []*modfile.Require{ - { - Mod: module.Version{ - Path: "gno.land/p/demo/avl", - Version: "v0.0.0", - }, - }, - }, - }, - requirements: []string{"avl"}, - stdOutContains: []string{ - "fetching gno.land/p/demo/avl", - }, - cachedStdOutContains: []string{ - "cached gno.land/p/demo/avl", - }, - }, { - desc: "fetch_gno.land/p/demo/blog6", - modFile: File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: "testFetchDeps", - }, - }, - Require: []*modfile.Require{ - { - Mod: module.Version{ - Path: "gno.land/p/demo/blog", - Version: "v0.0.0", - }, - }, - }, - }, - requirements: []string{"avl", "blog", "ufmt", "mux"}, - stdOutContains: []string{ - "fetching gno.land/p/demo/blog", - "fetching gno.land/p/demo/avl // indirect", - "fetching gno.land/p/demo/ufmt // indirect", - }, - cachedStdOutContains: []string{ - "cached gno.land/p/demo/blog", - }, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - var buf bytes.Buffer - log.SetOutput(&buf) - defer func() { - log.SetOutput(os.Stderr) - }() - - // Create test dir - dirPath, cleanUpFn := testutils.NewTestCaseDir(t) - assert.NotNil(t, dirPath) - defer cleanUpFn() - - // Fetching dependencies - err := tc.modFile.FetchDeps(dirPath, testRemote, true) - if tc.errorShouldContain != "" { - require.ErrorContains(t, err, tc.errorShouldContain) - } else { - require.Nil(t, err) - - // Read dir - entries, err := os.ReadDir(filepath.Join(dirPath, "gno.land", "p", "demo")) - require.Nil(t, err) - - // Check dir entries - assert.Equal(t, len(tc.requirements), len(entries)) - for _, e := range entries { - assert.Contains(t, tc.requirements, e.Name()) - } - - // Check logs - for _, c := range tc.stdOutContains { - assert.Contains(t, buf.String(), c) - } - - buf.Reset() - - // Try fetching again. Should be cached - tc.modFile.FetchDeps(dirPath, testRemote, true) - for _, c := range tc.cachedStdOutContains { - assert.Contains(t, buf.String(), c) - } - } - }) - } -} diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index 9384c41c293..a34caa2e48d 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -3,22 +3,16 @@ package gnomod import ( "errors" "fmt" - "go/parser" - gotoken "go/token" "os" "path/filepath" "strings" - "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/transpiler" "golang.org/x/mod/modfile" "golang.org/x/mod/module" ) -const queryPathFile = "vm/qfile" - // ModCachePath returns the path for gno modules func ModCachePath() string { return filepath.Join(gnoenv.HomeDir(), "pkg", "mod") @@ -27,127 +21,10 @@ func ModCachePath() string { // PackageDir resolves a given module.Version to the path on the filesystem. // If root is dir, it is defaulted to the value of [ModCachePath]. func PackageDir(root string, v module.Version) string { - // This is also used internally exactly like filepath.Join; but we'll keep - // the calls centralized to make sure we can change the path centrally should - // we start including the module version in the path. - if root == "" { root = ModCachePath() } - return filepath.Join(root, v.Path) -} - -func writePackage(remote, basePath, pkgPath string) (requirements []string, err error) { - res, err := queryChain(remote, queryPathFile, []byte(pkgPath)) - if err != nil { - return nil, fmt.Errorf("querychain (%s): %w", pkgPath, err) - } - - dirPath, fileName := gnovm.SplitFilepath(pkgPath) - if fileName == "" { - // Is Dir - // Create Dir if not exists - dirPath := filepath.Join(basePath, dirPath) - if _, err = os.Stat(dirPath); os.IsNotExist(err) { - if err = os.MkdirAll(dirPath, 0o755); err != nil { - return nil, fmt.Errorf("mkdir %q: %w", dirPath, err) - } - } - - files := strings.Split(string(res.Data), "\n") - for _, file := range files { - reqs, err := writePackage(remote, basePath, filepath.Join(pkgPath, file)) - if err != nil { - return nil, fmt.Errorf("writepackage: %w", err) - } - requirements = append(requirements, reqs...) - } - } else { - // Is File - // Transpile and write generated go file - file, err := parser.ParseFile(gotoken.NewFileSet(), fileName, res.Data, parser.ImportsOnly) - if err != nil { - return nil, fmt.Errorf("parse gno file: %w", err) - } - for _, i := range file.Imports { - requirements = append(requirements, i.Path.Value) - } - - // Write file - fileNameWithPath := filepath.Join(basePath, dirPath, fileName) - err = os.WriteFile(fileNameWithPath, res.Data, 0o644) - if err != nil { - return nil, fmt.Errorf("writefile %q: %w", fileNameWithPath, err) - } - } - - return removeDuplicateStr(requirements), nil -} - -// GnoToGoMod make necessary modifications in the gno.mod -// and return go.mod file. -func GnoToGoMod(f File) (*File, error) { - // TODO(morgan): good candidate to move to pkg/transpiler. - - gnoModPath := ModCachePath() - - if !gnolang.IsStdlib(f.Module.Mod.Path) { - f.AddModuleStmt(transpiler.TranspileImportPath(f.Module.Mod.Path)) - } - - for i := range f.Require { - mod, replaced := isReplaced(f.Require[i].Mod, f.Replace) - if replaced { - if modfile.IsDirectoryPath(mod.Path) { - continue - } - } - path := f.Require[i].Mod.Path - if !gnolang.IsStdlib(path) { - // Add dependency with a modified import path - f.AddRequire(transpiler.TranspileImportPath(path), f.Require[i].Mod.Version) - } - f.AddReplace(path, f.Require[i].Mod.Version, filepath.Join(gnoModPath, path), "") - // Remove the old require since the new dependency was added above - f.DropRequire(path) - } - - // Remove replacements that are not replaced by directories. - // - // Explanation: - // By this stage every replacement should be replace by dir. - // If not replaced by dir, remove it. - // - // e.g: - // - // ``` - // require ( - // gno.land/p/demo/avl v1.2.3 - // ) - // - // replace ( - // gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1 - // ) - // ``` - // - // In above case we will fetch `gno.land/p/demo/avl v3.2.1` and - // replace will look something like: - // - // ``` - // replace ( - // gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1 - // gno.land/p/demo/avl v3.2.1 => /path/to/avl/version/v3.2.1 - // ) - // ``` - // - // Remove `gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1`. - for _, r := range f.Replace { - if !modfile.IsDirectoryPath(r.New.Path) { - f.DropReplace(r.Old.Path, r.Old.Version) - } - } - - return &f, nil + return filepath.Join(root, filepath.FromSlash(v.Path)) } func CreateGnoModFile(rootDir, modPath string) error { @@ -180,7 +57,7 @@ func CreateGnoModFile(rootDir, modPath string) error { return fmt.Errorf("read file %q: %w", fpath, err) } - pn := gnolang.PackageNameFromFileBody(file.Name(), string(bz)) + pn := gnolang.MustPackageNameFromFileBody(file.Name(), string(bz)) if strings.HasSuffix(string(pkgName), "_test") { pkgName = pkgName[:len(pkgName)-len("_test")] } @@ -217,14 +94,3 @@ func isReplaced(mod module.Version, repl []*modfile.Replace) (module.Version, bo } return module.Version{}, false } - -func removeDuplicateStr(str []string) (res []string) { - m := make(map[string]struct{}, len(str)) - for _, s := range str { - if _, ok := m[s]; !ok { - m[s] = struct{}{} - res = append(res, s) - } - } - return -} diff --git a/gnovm/pkg/gnomod/parse.go b/gnovm/pkg/gnomod/parse.go index a6314d5729f..e3a3fbcaeea 100644 --- a/gnovm/pkg/gnomod/parse.go +++ b/gnovm/pkg/gnomod/parse.go @@ -105,7 +105,7 @@ func Parse(file string, data []byte) (*File, error) { Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), }) continue - case "module", "require", "replace": + case "module", "replace": for _, l := range x.Line { f.add(&errs, x, l, x.Token[0], l.Token) } @@ -180,26 +180,6 @@ func (f *File) add(errs *modfile.ErrorList, block *modfile.LineBlock, line *modf } f.Module.Mod = module.Version{Path: s} - case "require": - if len(args) != 2 { - errorf("usage: %s module/path v1.2.3", verb) - return - } - s, err := parseString(&args[0]) - if err != nil { - errorf("invalid quoted string: %v", err) - return - } - v, err := parseVersion(verb, s, &args[1]) - if err != nil { - wrapError(err) - return - } - f.Require = append(f.Require, &modfile.Require{ - Mod: module.Version{Path: s, Version: v}, - Syntax: line, - }) - case "replace": replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args) if wrappederr != nil { diff --git a/gnovm/pkg/gnomod/parse_test.go b/gnovm/pkg/gnomod/parse_test.go index 61aaa83482b..ec54c6424fc 100644 --- a/gnovm/pkg/gnomod/parse_test.go +++ b/gnovm/pkg/gnomod/parse_test.go @@ -194,16 +194,29 @@ func TestParseGnoMod(t *testing.T) { modPath: filepath.Join(pkgDir, "gno.mod"), }, { - desc: "error parsing gno.mod", + desc: "valid gno.mod file with replace", + modData: `module foo + replace bar => ../bar`, + modPath: filepath.Join(pkgDir, "gno.mod"), + }, + { + desc: "error bad module directive", modData: `module foo v0.0.0`, modPath: filepath.Join(pkgDir, "gno.mod"), errShouldContain: "error parsing gno.mod file at", }, { - desc: "error validating gno.mod", - modData: `require bar v0.0.0`, + desc: "error gno.mod without module", + modData: `replace bar => ../bar`, + modPath: filepath.Join(pkgDir, "gno.mod"), + errShouldContain: "requires module", + }, + { + desc: "error gno.mod with require", + modData: `module foo + require bar v0.0.0`, modPath: filepath.Join(pkgDir, "gno.mod"), - errShouldContain: "error validating gno.mod file at", + errShouldContain: "unknown directive: require", }, } { t.Run(tc.desc, func(t *testing.T) { diff --git a/gnovm/pkg/gnomod/pkg.go b/gnovm/pkg/gnomod/pkg.go index f6fe7f60301..35f52e3dded 100644 --- a/gnovm/pkg/gnomod/pkg.go +++ b/gnovm/pkg/gnomod/pkg.go @@ -5,14 +5,19 @@ import ( "io/fs" "os" "path/filepath" + "slices" "strings" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/packages" ) type Pkg struct { - Dir string // absolute path to package dir - Name string // package name - Requires []string // dependencies - Draft bool // whether the package is a draft + Dir string // absolute path to package dir + Name string // package name + Imports []string // direct imports of this pkg + Draft bool // whether the package is a draft } type SubPkg struct { @@ -60,10 +65,10 @@ func visitPackage(pkg Pkg, pkgs []Pkg, visited, onStack map[string]bool, sortedP onStack[pkg.Name] = true // Visit package's dependencies - for _, req := range pkg.Requires { + for _, imp := range pkg.Imports { found := false for _, p := range pkgs { - if p.Name != req { + if p.Name != imp { continue } if err := visitPackage(p, pkgs, visited, onStack, sortedPkgs); err != nil { @@ -73,7 +78,7 @@ func visitPackage(pkg Pkg, pkgs []Pkg, visited, onStack map[string]bool, sortedP break } if !found { - return fmt.Errorf("missing dependency '%s' for package '%s'", req, pkg.Name) + return fmt.Errorf("missing dependency '%s' for package '%s'", imp, pkg.Name) } } @@ -111,17 +116,28 @@ func ListPkgs(root string) (PkgList, error) { return fmt.Errorf("validate: %w", err) } + pkg, err := gnolang.ReadMemPackage(path, gnoMod.Module.Mod.Path) + if err != nil { + // ignore package files on error + pkg = &gnovm.MemPackage{} + } + + imports, err := packages.Imports(pkg) + if err != nil { + // ignore imports on error + imports = []string{} + } + + // remove self and standard libraries from imports + imports = slices.DeleteFunc(imports, func(imp string) bool { + return imp == gnoMod.Module.Mod.Path || gnolang.IsStdlib(imp) + }) + pkgs = append(pkgs, Pkg{ - Dir: path, - Name: gnoMod.Module.Mod.Path, - Draft: gnoMod.Draft, - Requires: func() []string { - var reqs []string - for _, req := range gnoMod.Require { - reqs = append(reqs, req.Mod.Path) - } - return reqs - }(), + Dir: path, + Name: gnoMod.Module.Mod.Path, + Draft: gnoMod.Draft, + Imports: imports, }) return nil }) @@ -144,7 +160,7 @@ func (sp SortedPkgList) GetNonDraftPkgs() SortedPkgList { continue } dependsOnDraft := false - for _, req := range pkg.Requires { + for _, req := range pkg.Imports { if draft[req] { dependsOnDraft = true draft[pkg.Name] = true diff --git a/gnovm/pkg/gnomod/pkg_test.go b/gnovm/pkg/gnomod/pkg_test.go index 587a0bb8f81..7c3035a4b7b 100644 --- a/gnovm/pkg/gnomod/pkg_test.go +++ b/gnovm/pkg/gnomod/pkg_test.go @@ -47,12 +47,6 @@ func TestListAndNonDraftPkgs(t *testing.T) { "foo", `module foo`, }, - { - "bar", - `module bar - - require foo v0.0.0`, - }, { "baz", `module baz`, @@ -64,121 +58,8 @@ func TestListAndNonDraftPkgs(t *testing.T) { module qux`, }, }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{"foo", "bar", "baz"}, - }, - { - desc: "package directly depends on draft package", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - require foo v0.0.0`, - }, - { - "baz", - `module baz`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz"}, - outNonDraftPkgs: []string{"baz"}, - }, - { - desc: "package indirectly depends on draft package", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - - require foo v0.0.0`, - }, - { - "baz", - `module baz - - require bar v0.0.0`, - }, - { - "qux", - `module qux`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{"qux"}, - }, - { - desc: "package indirectly depends on draft package (multiple levels - 1)", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - - require foo v0.0.0`, - }, - { - "baz", - `module baz - - require bar v0.0.0`, - }, - { - "qux", - `module qux - - require baz v0.0.0`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{}, - }, - { - desc: "package indirectly depends on draft package (multiple levels - 2)", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - - require qux v0.0.0`, - }, - { - "baz", - `module baz - - require foo v0.0.0`, - }, - { - "qux", - `module qux - - require baz v0.0.0`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{}, + outListPkgs: []string{"foo", "baz", "qux"}, + outNonDraftPkgs: []string{"foo", "baz"}, }, } { t.Run(tc.desc, func(t *testing.T) { @@ -224,6 +105,7 @@ func createGnoModPkg(t *testing.T, dirPath, pkgName, modData string) { // Create gno.mod err = os.WriteFile(filepath.Join(pkgDirPath, "gno.mod"), []byte(modData), 0o644) + require.NoError(t, err) } func TestSortPkgs(t *testing.T) { @@ -240,30 +122,30 @@ func TestSortPkgs(t *testing.T) { }, { desc: "no_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{}}, - {Name: "pkg2", Dir: "/path/to/pkg2", Requires: []string{}}, - {Name: "pkg3", Dir: "/path/to/pkg3", Requires: []string{}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{}}, + {Name: "pkg2", Dir: "/path/to/pkg2", Imports: []string{}}, + {Name: "pkg3", Dir: "/path/to/pkg3", Imports: []string{}}, }, expected: []string{"pkg1", "pkg2", "pkg3"}, }, { desc: "circular_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{"pkg2"}}, - {Name: "pkg2", Dir: "/path/to/pkg2", Requires: []string{"pkg1"}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{"pkg2"}}, + {Name: "pkg2", Dir: "/path/to/pkg2", Imports: []string{"pkg1"}}, }, shouldErr: true, }, { desc: "missing_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{"pkg2"}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{"pkg2"}}, }, shouldErr: true, }, { desc: "valid_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{"pkg2"}}, - {Name: "pkg2", Dir: "/path/to/pkg2", Requires: []string{"pkg3"}}, - {Name: "pkg3", Dir: "/path/to/pkg3", Requires: []string{}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{"pkg2"}}, + {Name: "pkg2", Dir: "/path/to/pkg2", Imports: []string{"pkg3"}}, + {Name: "pkg3", Dir: "/path/to/pkg3", Imports: []string{}}, }, expected: []string{"pkg3", "pkg2", "pkg1"}, }, diff --git a/gnovm/pkg/gnomod/preprocess.go b/gnovm/pkg/gnomod/preprocess.go index ec1faaa5c29..df6910f769b 100644 --- a/gnovm/pkg/gnomod/preprocess.go +++ b/gnovm/pkg/gnomod/preprocess.go @@ -3,50 +3,15 @@ package gnomod import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" - "golang.org/x/mod/semver" ) -func removeDups(syntax *modfile.FileSyntax, require *[]*modfile.Require, replace *[]*modfile.Replace) { - if require != nil { - purged := removeRequireDups(require) - cleanSyntaxTree(syntax, purged) - } +func removeDups(syntax *modfile.FileSyntax, replace *[]*modfile.Replace) { if replace != nil { purged := removeReplaceDups(replace) cleanSyntaxTree(syntax, purged) } } -// removeRequireDups removes duplicate requirements. -// Requirements with higher version takes priority. -func removeRequireDups(require *[]*modfile.Require) map[*modfile.Line]bool { - purge := make(map[*modfile.Line]bool) - - keepRequire := make(map[string]string) - for _, r := range *require { - if v, ok := keepRequire[r.Mod.Path]; ok { - if semver.Compare(r.Mod.Version, v) == 1 { - keepRequire[r.Mod.Path] = r.Mod.Version - } - continue - } - keepRequire[r.Mod.Path] = r.Mod.Version - } - var req []*modfile.Require - added := make(map[string]bool) - for _, r := range *require { - if v, ok := keepRequire[r.Mod.Path]; ok && !added[r.Mod.Path] && v == r.Mod.Version { - req = append(req, r) - added[r.Mod.Path] = true - continue - } - purge[r.Syntax] = true - } - *require = req - - return purge -} - // removeReplaceDups removes duplicate replacements. // Later replacements take priority over earlier ones. func removeReplaceDups(replace *[]*modfile.Replace) map[*modfile.Line]bool { diff --git a/gnovm/pkg/gnomod/preprocess_test.go b/gnovm/pkg/gnomod/preprocess_test.go index 28f42d740e3..6e0a890763c 100644 --- a/gnovm/pkg/gnomod/preprocess_test.go +++ b/gnovm/pkg/gnomod/preprocess_test.go @@ -8,133 +8,6 @@ import ( "golang.org/x/mod/module" ) -func TestRemoveRequireDups(t *testing.T) { - for _, tc := range []struct { - desc string - in []*modfile.Require - expected []*modfile.Require - }{ - { - desc: "no_duplicate", - in: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - expected: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - }, - { - desc: "one_duplicate", - in: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - expected: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - }, - { - desc: "multiple_duplicate", - in: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.2.0", - }, - }, - }, - expected: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.2.0", - }, - }, - }, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - in := tc.in - removeRequireDups(&in) - - assert.Equal(t, tc.expected, in) - }) - } -} - func TestRemoveReplaceDups(t *testing.T) { for _, tc := range []struct { desc string diff --git a/gnovm/pkg/gnomod/read.go b/gnovm/pkg/gnomod/read.go index d6d771429d3..bb03ddf6efd 100644 --- a/gnovm/pkg/gnomod/read.go +++ b/gnovm/pkg/gnomod/read.go @@ -770,12 +770,6 @@ func parseReplace(filename string, line *modfile.Line, verb string, args []strin } nv := "" if len(args) == arrow+2 { - if !modfile.IsDirectoryPath(ns) { - if strings.Contains(ns, "@") { - return nil, errorf("replacement module must match format 'path version', not 'path@version'") - } - return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)") - } if filepath.Separator == '/' && strings.Contains(ns, `\`) { return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)") } @@ -862,60 +856,6 @@ func updateLine(line *modfile.Line, tokens ...string) { line.Token = tokens } -// setIndirect sets line to have (or not have) a "// indirect" comment. -func setIndirect(r *modfile.Require, indirect bool) { - r.Indirect = indirect - line := r.Syntax - if isIndirect(line) == indirect { - return - } - if indirect { - // Adding comment. - if len(line.Suffix) == 0 { - // New comment. - line.Suffix = []modfile.Comment{{Token: "// indirect", Suffix: true}} - return - } - - com := &line.Suffix[0] - text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) - if text == "" { - // Empty comment. - com.Token = "// indirect" - return - } - - // Insert at beginning of existing comment. - com.Token = "// indirect; " + text - return - } - - // Removing comment. - f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) - if f == "indirect" { - // Remove whole comment. - line.Suffix = nil - return - } - - // Remove comment prefix. - com := &line.Suffix[0] - i := strings.Index(com.Token, "indirect;") - com.Token = "//" + com.Token[i+len("indirect;"):] -} - -// isIndirect reports whether line has a "// indirect" comment, -// meaning it is in go.mod only for its effect on indirect dependencies, -// so that it can be dropped entirely once the effective version of the -// indirect dependency reaches the given minimum version. -func isIndirect(line *modfile.Line) bool { - if len(line.Suffix) == 0 { - return false - } - f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) - return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") -} - // addLine adds a line containing the given tokens to the file. // // If the first token of the hint matches the first token of the diff --git a/gnovm/pkg/gnomod/read_test.go b/gnovm/pkg/gnomod/read_test.go index cf3b6f59076..d9c35205a51 100644 --- a/gnovm/pkg/gnomod/read_test.go +++ b/gnovm/pkg/gnomod/read_test.go @@ -210,85 +210,6 @@ comments before "// e" } } -var addRequireTests = []struct { - desc string - in string - path string - vers string - out string -}{ - { - `existing`, - ` - module m - require x.y/z v1.2.3 - `, - "x.y/z", "v1.5.6", - ` - module m - require x.y/z v1.5.6 - `, - }, - { - `existing2`, - ` - module m - require ( - x.y/z v1.2.3 // first - x.z/a v0.1.0 // first-a - ) - require x.y/z v1.4.5 // second - require ( - x.y/z v1.6.7 // third - x.z/a v0.2.0 // third-a - ) - `, - "x.y/z", "v1.8.9", - ` - module m - - require ( - x.y/z v1.8.9 // first - x.z/a v0.1.0 // first-a - ) - - require x.z/a v0.2.0 // third-a - `, - }, - { - `new`, - ` - module m - require x.y/z v1.2.3 - `, - "x.y/w", "v1.5.6", - ` - module m - require ( - x.y/z v1.2.3 - x.y/w v1.5.6 - ) - `, - }, - { - `new2`, - ` - module m - require x.y/z v1.2.3 - require x.y/q/v2 v2.3.4 - `, - "x.y/w", "v1.5.6", - ` - module m - require x.y/z v1.2.3 - require ( - x.y/q/v2 v2.3.4 - x.y/w v1.5.6 - ) - `, - }, -} - var addModuleStmtTests = []struct { desc string in string @@ -299,12 +220,10 @@ var addModuleStmtTests = []struct { `existing`, ` module m - require x.y/z v1.2.3 `, "n", ` module n - require x.y/z v1.2.3 `, }, { @@ -330,7 +249,6 @@ var addReplaceTests = []struct { `replace_with_module`, ` module m - require x.y/z v1.2.3 `, "x.y/z", "v1.5.6", @@ -338,7 +256,6 @@ var addReplaceTests = []struct { "v1.5.6", ` module m - require x.y/z v1.2.3 replace x.y/z v1.5.6 => a.b/c v1.5.6 `, }, @@ -346,7 +263,6 @@ var addReplaceTests = []struct { `replace_with_dir`, ` module m - require x.y/z v1.2.3 `, "x.y/z", "v1.5.6", @@ -354,66 +270,11 @@ var addReplaceTests = []struct { "", ` module m - require x.y/z v1.2.3 replace x.y/z v1.5.6 => /path/to/dir `, }, } -var dropRequireTests = []struct { - desc string - in string - path string - out string -}{ - { - `existing`, - ` - module m - require x.y/z v1.2.3 - `, - "x.y/z", - ` - module m - `, - }, - { - `existing2`, - ` - module m - require ( - x.y/z v1.2.3 // first - x.z/a v0.1.0 // first-a - ) - require x.y/z v1.4.5 // second - require ( - x.y/z v1.6.7 // third - x.z/a v0.2.0 // third-a - ) - `, - "x.y/z", - ` - module m - - require x.z/a v0.1.0 // first-a - - require x.z/a v0.2.0 // third-a - `, - }, - { - `not_exists`, - ` - module m - require x.y/z v1.2.3 - `, - "a.b/c", - ` - module m - require x.y/z v1.2.3 - `, - }, -} - var dropReplaceTests = []struct { desc string in string @@ -425,7 +286,6 @@ var dropReplaceTests = []struct { `existing`, ` module m - require x.y/z v1.2.3 replace x.y/z v1.2.3 => a.b/c v1.5.6 `, @@ -433,14 +293,12 @@ var dropReplaceTests = []struct { "v1.2.3", ` module m - require x.y/z v1.2.3 `, }, { `not_exists`, ` module m - require x.y/z v1.2.3 replace x.y/z v1.2.3 => a.b/c v1.5.6 `, @@ -448,25 +306,12 @@ var dropReplaceTests = []struct { "v3.2.1", ` module m - require x.y/z v1.2.3 replace x.y/z v1.2.3 => a.b/c v1.5.6 `, }, } -func TestAddRequire(t *testing.T) { - for _, tt := range addRequireTests { - t.Run(tt.desc, func(t *testing.T) { - testEdit(t, tt.in, tt.out, func(f *File) error { - err := f.AddRequire(tt.path, tt.vers) - f.Syntax.Cleanup() - return err - }) - }) - } -} - func TestAddModuleStmt(t *testing.T) { for _, tt := range addModuleStmtTests { t.Run(tt.desc, func(t *testing.T) { @@ -491,18 +336,6 @@ func TestAddReplace(t *testing.T) { } } -func TestDropRequire(t *testing.T) { - for _, tt := range dropRequireTests { - t.Run(tt.desc, func(t *testing.T) { - testEdit(t, tt.in, tt.out, func(f *File) error { - err := f.DropRequire(tt.path) - f.Syntax.Cleanup() - return err - }) - }) - } -} - func TestDropReplace(t *testing.T) { for _, tt := range dropReplaceTests { t.Run(tt.desc, func(t *testing.T) { diff --git a/gnovm/pkg/packages/doc.go b/gnovm/pkg/packages/doc.go new file mode 100644 index 00000000000..fb63ae3838e --- /dev/null +++ b/gnovm/pkg/packages/doc.go @@ -0,0 +1,2 @@ +// Package packages provides utility functions to statically analyze Gno MemPackages +package packages diff --git a/gnovm/pkg/packages/imports.go b/gnovm/pkg/packages/imports.go new file mode 100644 index 00000000000..e72f37276db --- /dev/null +++ b/gnovm/pkg/packages/imports.go @@ -0,0 +1,72 @@ +package packages + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "sort" + "strconv" + "strings" + + "github.com/gnolang/gno/gnovm" +) + +// Imports returns the list of gno imports from a [gnovm.MemPackage]. +func Imports(pkg *gnovm.MemPackage) ([]string, error) { + allImports := make([]string, 0) + seen := make(map[string]struct{}) + for _, file := range pkg.Files { + if !strings.HasSuffix(file.Name, ".gno") { + continue + } + if strings.HasSuffix(file.Name, "_filetest.gno") { + continue + } + imports, _, err := FileImports(file.Name, file.Body) + if err != nil { + return nil, err + } + for _, im := range imports { + if im.Error != nil { + return nil, err + } + if _, ok := seen[im.PkgPath]; ok { + continue + } + allImports = append(allImports, im.PkgPath) + seen[im.PkgPath] = struct{}{} + } + } + sort.Strings(allImports) + + return allImports, nil +} + +type FileImport struct { + PkgPath string + Spec *ast.ImportSpec + Error error +} + +// FileImports returns the list of gno imports in the given file src. +// The given filename is only used when recording position information. +func FileImports(filename string, src string) ([]*FileImport, *token.FileSet, error) { + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, filename, src, parser.ImportsOnly) + if err != nil { + return nil, nil, err + } + res := make([]*FileImport, len(f.Imports)) + for i, im := range f.Imports { + fi := FileImport{Spec: im} + importPath, err := strconv.Unquote(im.Path.Value) + if err != nil { + fi.Error = fmt.Errorf("%v: unexpected invalid import path: %v", fs.Position(im.Pos()).String(), im.Path.Value) + } else { + fi.PkgPath = importPath + } + res[i] = &fi + } + return res, fs, nil +} diff --git a/gnovm/pkg/packages/imports_test.go b/gnovm/pkg/packages/imports_test.go new file mode 100644 index 00000000000..14808dcbd6f --- /dev/null +++ b/gnovm/pkg/packages/imports_test.go @@ -0,0 +1,127 @@ +package packages + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/stretchr/testify/require" +) + +func TestImports(t *testing.T) { + workingDir, err := os.Getwd() + require.NoError(t, err) + + // create external dir + tmpDir := t.TempDir() + + // cd to tmp directory + os.Chdir(tmpDir) + defer os.Chdir(workingDir) + + files := []struct { + name, data string + }{ + { + name: "file1.gno", + data: ` + package tmp + + import ( + "std" + + "gno.land/p/demo/pkg1" + ) + `, + }, + { + name: "file2.gno", + data: ` + package tmp + + import ( + "gno.land/p/demo/pkg1" + "gno.land/p/demo/pkg2" + ) + `, + }, + { + name: "file1_test.gno", + data: ` + package tmp + + import ( + "testing" + + "gno.land/p/demo/testpkg" + ) + `, + }, + { + name: "z_0_filetest.gno", + data: ` + package main + + import ( + "gno.land/p/demo/filetestpkg" + ) + `, + }, + + // subpkg files + { + name: filepath.Join("subtmp", "file1.gno"), + data: ` + package subtmp + + import ( + "std" + + "gno.land/p/demo/subpkg1" + ) + `, + }, + { + name: filepath.Join("subtmp", "file2.gno"), + data: ` + package subtmp + + import ( + "gno.land/p/demo/subpkg1" + "gno.land/p/demo/subpkg2" + ) + `, + }, + } + + // Expected list of imports + // - ignore subdirs + // - ignore duplicate + // - ignore *_filetest.gno + // - should be sorted + expected := []string{ + "gno.land/p/demo/pkg1", + "gno.land/p/demo/pkg2", + "gno.land/p/demo/testpkg", + "std", + "testing", + } + + // Create subpkg dir + err = os.Mkdir("subtmp", 0o700) + require.NoError(t, err) + + // Create files + for _, f := range files { + err = os.WriteFile(f.name, []byte(f.data), 0o644) + require.NoError(t, err) + } + + pkg, err := gnolang.ReadMemPackage(tmpDir, "test") + require.NoError(t, err) + imports, err := Imports(pkg) + require.NoError(t, err) + + require.Equal(t, expected, imports) +} diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index b57fc6388b1..731bf9756dd 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -4,18 +4,16 @@ import ( "encoding/json" "errors" "fmt" - "go/parser" - "go/token" "io" "math/big" "os" "path/filepath" "runtime/debug" - "strconv" "strings" "time" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/packages" teststdlibs "github.com/gnolang/gno/gnovm/tests/stdlibs" teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" "github.com/gnolang/gno/tm2/pkg/db/memdb" @@ -45,7 +43,7 @@ func Store( const testPath = "github.com/gnolang/gno/_test/" if strings.HasPrefix(pkgPath, testPath) { baseDir := filepath.Join(rootDir, "gnovm", "tests", "files", "extern", pkgPath[len(testPath):]) - memPkg := gno.ReadMemPackage(baseDir, pkgPath) + memPkg := gno.MustReadMemPackage(baseDir, pkgPath) send := std.Coins{} ctx := Context(pkgPath, send) m2 := gno.NewMachineWithOptions(gno.MachineOptions{ @@ -137,7 +135,7 @@ func Store( // if examples package... examplePath := filepath.Join(rootDir, "examples", pkgPath) if osm.DirExists(examplePath) { - memPkg := gno.ReadMemPackage(examplePath, pkgPath) + memPkg := gno.MustReadMemPackage(examplePath, pkgPath) if memPkg.IsEmpty() { panic(fmt.Sprintf("found an empty package %q", pkgPath)) } @@ -193,7 +191,7 @@ func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gn return nil, nil } - memPkg := gno.ReadMemPackageFromList(files, pkgPath) + memPkg := gno.MustReadMemPackageFromList(files, pkgPath) m2 := gno.NewMachineWithOptions(gno.MachineOptions{ // NOTE: see also pkgs/sdk/vm/builtins.go // Needs PkgPath != its name because TestStore.getPackage is the package @@ -241,24 +239,22 @@ func LoadImports(store gno.Store, filename string, content []byte) (err error) { } }() - fset := token.NewFileSet() - fl, err := parser.ParseFile(fset, filename, content, parser.ImportsOnly) + imports, fset, err := packages.FileImports(filename, string(content)) if err != nil { - return fmt.Errorf("parse failure: %w", err) + return err } - for _, imp := range fl.Imports { - impPath, err := strconv.Unquote(imp.Path.Value) - if err != nil { - return fmt.Errorf("%v: unexpected invalid import path: %v", fset.Position(imp.Pos()).String(), imp.Path.Value) + for _, imp := range imports { + if imp.Error != nil { + return imp.Error } - if gno.IsRealmPath(impPath) { + if gno.IsRealmPath(imp.PkgPath) { // Don't eagerly load realms. // Realms persist state and can change the state of other realms in initialization. continue } - pkg := store.GetPackage(impPath, true) + pkg := store.GetPackage(imp.PkgPath, true) if pkg == nil { - return fmt.Errorf("%v: unknown import path %v", fset.Position(imp.Pos()).String(), impPath) + return fmt.Errorf("%v: unknown import path %v", fset.Position(imp.Spec.Pos()).String(), imp.PkgPath) } } return nil diff --git a/gnovm/tests/integ/invalid_module_version1/gno.mod b/gnovm/tests/integ/invalid_module_version1/gno.mod deleted file mode 100644 index e4c64e3106f..00000000000 --- a/gnovm/tests/integ/invalid_module_version1/gno.mod +++ /dev/null @@ -1,5 +0,0 @@ -module tmp - -require ( - "gno.land/p/demo/avl" //missing version -) diff --git a/gnovm/tests/integ/invalid_module_version2/gno.mod b/gnovm/tests/integ/invalid_module_version2/gno.mod deleted file mode 100644 index 0a3088b454a..00000000000 --- a/gnovm/tests/integ/invalid_module_version2/gno.mod +++ /dev/null @@ -1,5 +0,0 @@ -module tmp - -require ( - "gno.land/p/demo/avl" version-2 //invalid versioning -) diff --git a/gnovm/tests/integ/replace_with_dir/gno.mod b/gnovm/tests/integ/replace_with_dir/gno.mod index 6a7b1b664c8..69ae753a58a 100644 --- a/gnovm/tests/integ/replace_with_dir/gno.mod +++ b/gnovm/tests/integ/replace_with_dir/gno.mod @@ -1,9 +1,5 @@ module gno.land/tests/replaceavl -require ( - "gno.land/p/demo/notexists" v0.0.0 -) - replace ( "gno.land/p/demo/notexists" => /path/to/dir ) diff --git a/gnovm/tests/integ/replace_with_invalid_module/gno.mod b/gnovm/tests/integ/replace_with_invalid_module/gno.mod index ee90787ff0e..2a9527da7d6 100644 --- a/gnovm/tests/integ/replace_with_invalid_module/gno.mod +++ b/gnovm/tests/integ/replace_with_invalid_module/gno.mod @@ -1,9 +1,5 @@ module gno.land/tests/replaceavl -require ( - "gno.land/p/demo/avl" v0.0.0 -) - replace ( - "gno.land/p/demo/avl" => "gno.land/p/demo/avlll" v0.0.0 + "gno.land/p/demo/avl" => "gno.land/p/demo/notexists" ) diff --git a/gnovm/tests/integ/replace_with_invalid_module/main.gno b/gnovm/tests/integ/replace_with_invalid_module/main.gno new file mode 100644 index 00000000000..7f78497fa02 --- /dev/null +++ b/gnovm/tests/integ/replace_with_invalid_module/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "gno.land/p/demo/avl" +) + +var foo = avl.Bar diff --git a/gnovm/tests/integ/replace_with_module/gno.mod b/gnovm/tests/integ/replace_with_module/gno.mod index 09c77df7a95..de730c90a53 100644 --- a/gnovm/tests/integ/replace_with_module/gno.mod +++ b/gnovm/tests/integ/replace_with_module/gno.mod @@ -1,9 +1,5 @@ module gno.land/tests/replaceavl -require ( - "gno.land/p/demo/avl" v0.0.2 -) - replace ( - "gno.land/p/demo/avl" v0.0.2 => "gno.land/p/demo/avl" v1.0.0 + "gno.land/p/demo/avl" => "gno.land/p/demo/users" ) diff --git a/gnovm/tests/integ/replace_with_module/main.gno b/gnovm/tests/integ/replace_with_module/main.gno new file mode 100644 index 00000000000..7f78497fa02 --- /dev/null +++ b/gnovm/tests/integ/replace_with_module/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "gno.land/p/demo/avl" +) + +var foo = avl.Bar diff --git a/gnovm/tests/integ/require_invalid_module/gno.mod b/gnovm/tests/integ/require_invalid_module/gno.mod index f0b455f128b..f10dff8c8d5 100644 --- a/gnovm/tests/integ/require_invalid_module/gno.mod +++ b/gnovm/tests/integ/require_invalid_module/gno.mod @@ -1,5 +1 @@ -module gno.land/tests/reqinvalidmodule - -require ( - "gno.land/p/demo/notexists" v1.2.3 -) +module gno.land/tests/reqinvalidmodule \ No newline at end of file diff --git a/gnovm/tests/integ/require_invalid_module/main.gno b/gnovm/tests/integ/require_invalid_module/main.gno new file mode 100644 index 00000000000..703ec65ee5a --- /dev/null +++ b/gnovm/tests/integ/require_invalid_module/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "gno.land/p/demo/notexists" +) + +var foo = notexists.Bar diff --git a/gnovm/tests/integ/require_remote_module/gno.mod b/gnovm/tests/integ/require_remote_module/gno.mod index 4823c72585d..946f41398ba 100644 --- a/gnovm/tests/integ/require_remote_module/gno.mod +++ b/gnovm/tests/integ/require_remote_module/gno.mod @@ -1,5 +1 @@ module gno.land/tests/importavl - -require ( - "gno.land/p/demo/avl" v0.0.0 -) diff --git a/gnovm/tests/integ/require_std_lib/gno.mod b/gnovm/tests/integ/require_std_lib/gno.mod new file mode 100644 index 00000000000..f10dff8c8d5 --- /dev/null +++ b/gnovm/tests/integ/require_std_lib/gno.mod @@ -0,0 +1 @@ +module gno.land/tests/reqinvalidmodule \ No newline at end of file diff --git a/gnovm/tests/integ/require_std_lib/main.gno b/gnovm/tests/integ/require_std_lib/main.gno new file mode 100644 index 00000000000..920d238cccc --- /dev/null +++ b/gnovm/tests/integ/require_std_lib/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "std" +) + +var foo std.Address diff --git a/gnovm/tests/integ/valid2/gno.mod b/gnovm/tests/integ/valid2/gno.mod index 98a5a0dacc1..3eaaa374994 100644 --- a/gnovm/tests/integ/valid2/gno.mod +++ b/gnovm/tests/integ/valid2/gno.mod @@ -1,3 +1 @@ module gno.land/p/integ/valid - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/misc/loop/go.mod b/misc/loop/go.mod index f1c09cd9f82..a6bbdad3c82 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -73,7 +73,6 @@ require ( golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect From 66c2eb6041f6970d616822e58dff00ac70d0fdce Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Sun, 8 Dec 2024 05:01:51 +0800 Subject: [PATCH 303/345] fix(gnovm): make `Stacktrace` correctly handle panics with `len(Stmts) == 0` (#3273) While finalizing the realm, all statements in the machine have been popped out. A necessary check must be performed during stack trace handling.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
Co-authored-by: Petar Dambovaliev --- gnovm/pkg/gnolang/machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 4f4c7c188f3..b48c0742e6f 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -401,7 +401,7 @@ func destar(x Expr) Expr { // Stacktrace returns the stack trace of the machine. // It collects the executions and frames from the machine's frames and statements. func (m *Machine) Stacktrace() (stacktrace Stacktrace) { - if len(m.Frames) == 0 { + if len(m.Frames) == 0 || len(m.Stmts) == 0 { return } From ca1df8689774cbfcb7b714ed6ac407d0163129d2 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:23:10 +0100 Subject: [PATCH 304/345] ci: use go.mod to determine go version (#3279) Related to https://github.com/gnolang/gno/pull/3229#discussion_r1862370753 This PR replaces most fixed versions of Go in the CI workflows with retrieving the version from the relevant go.mod file. For workflows that do not have an associated go.mod file, the go.mod file at the root of the repository is used. All `*_template.yml` workflows seem designed to use a fixed version of go and do not allow passing a go.mod file to the `setup-go` action. Achieving this would require a more significant refactor.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
Co-authored-by: Morgan --- .github/workflows/benchmark-master-push.yml | 2 +- .github/workflows/dependabot-tidy.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/genesis-verify.yml | 2 +- .github/workflows/gh-pages.yml | 2 +- .github/workflows/releaser-master.yml | 2 +- .github/workflows/releaser-nightly.yml | 2 +- .github/workflows/releaser.yml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/benchmark-master-push.yml b/.github/workflows/benchmark-master-push.yml index bde6e623a88..622baefc0de 100644 --- a/.github/workflows/benchmark-master-push.yml +++ b/.github/workflows/benchmark-master-push.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod - name: Run benchmark # add more benchmarks by adding additional lines for different packages; diff --git a/.github/workflows/dependabot-tidy.yml b/.github/workflows/dependabot-tidy.yml index 59e9e1c8146..39fed8b0172 100644 --- a/.github/workflows/dependabot-tidy.yml +++ b/.github/workflows/dependabot-tidy.yml @@ -20,7 +20,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: go.mod - name: Tidy all Go mods env: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 262b341276c..c9d9af0fb6f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version-file: go.mod - name: Install dependencies run: go mod download diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml index f870cd0658c..1288d588100 100644 --- a/.github/workflows/genesis-verify.yml +++ b/.github/workflows/genesis-verify.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.22" + go-version-file: contribs/gnogenesis/go.mod - name: Build gnogenesis run: make -C contribs/gnogenesis diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a8407f57291..1b955b52cd0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod - run: "cd misc/gendocs && make install gen" - uses: actions/configure-pages@v5 id: pages diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 36a709a242a..3d194e2cb4c 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - uses: sigstore/cosign-installer@v3.7.0 diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index e9a5c15a22d..4308f1c4a7d 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - uses: sigstore/cosign-installer@v3.7.0 diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index d33432bd16d..309664bdcce 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - uses: sigstore/cosign-installer@v3.7.0 From 3de2475bc1d2b12a86e68b150d6d0224909a47da Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Sat, 7 Dec 2024 16:31:18 -0500 Subject: [PATCH 305/345] feat(examples/mux): support query string in path (#3281) This PR adds support for request URLs with query strings for `p/demo/mux` package. Previously, `mux.Router` would fail to find a correct handler if request URL contains query string. ```go r := mux.NewRouter() r.HandleFunc("hello", func (rw *mux.ResponseWriter, req *mux.Request) { ... }) reqUrl := "hello?foo=bar" r.Render(reqUrl) // Fails ``` This PR fixes this behavior and introduces a new `mux.Request.RawPath` field which contains a raw request path including query string. The `RawPath` field is designed to be used for packages like `p/demo/avl/pager` to extract query params from a string.\ The `Path` field, as before, contains just path segment of request, without query strings.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
CC @moul @thehowl @jeronimoalbi --------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Morgan --- examples/gno.land/p/demo/mux/request.gno | 10 ++- examples/gno.land/p/demo/mux/router.gno | 15 +++- examples/gno.land/p/demo/mux/router_test.gno | 89 +++++++++++++++----- gnovm/cmd/gno/download_deps_test.go | 2 +- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index f7996fe40fe..7b5b74da91b 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -4,7 +4,15 @@ import "strings" // Request represents an incoming request. type Request struct { - Path string + // Path is request path name. + // + // Note: use RawPath to obtain a raw path with query string. + Path string + + // RawPath contains a whole request path, including query string. + RawPath string + + // HandlerPath is handler rule that matches a request. HandlerPath string } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index a2efb3a4ebf..fe6bf70abdf 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -18,7 +18,8 @@ func NewRouter() *Router { // Render renders the output for the given path using the registered route handler. func (r *Router) Render(reqPath string) string { - reqParts := strings.Split(reqPath, "/") + clearPath := stripQueryString(reqPath) + reqParts := strings.Split(clearPath, "/") for _, route := range r.routes { patParts := strings.Split(route.Pattern, "/") @@ -45,7 +46,8 @@ func (r *Router) Render(reqPath string) string { } if match { req := &Request{ - Path: reqPath, + Path: clearPath, + RawPath: reqPath, HandlerPath: route.Pattern, } res := &ResponseWriter{} @@ -66,3 +68,12 @@ func (r *Router) HandleFunc(pattern string, fn HandlerFunc) { route := Handler{Pattern: pattern, Fn: fn} r.routes = append(r.routes, route) } + +func stripQueryString(reqPath string) string { + i := strings.Index(reqPath, "?") + if i == -1 { + return reqPath + } + + return reqPath[:i] +} diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index 13fd5b97955..cc6aad62146 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -1,34 +1,85 @@ package mux -import "testing" +import ( + "testing" -func TestRouter_Render(t *testing.T) { - // Define handlers and route configuration - router := NewRouter() - router.HandleFunc("hello/{name}", func(res *ResponseWriter, req *Request) { - name := req.GetVar("name") - if name != "" { - res.Write("Hello, " + name + "!") - } else { - res.Write("Hello, world!") - } - }) - router.HandleFunc("hi", func(res *ResponseWriter, req *Request) { - res.Write("Hi, earth!") - }) + "gno.land/p/demo/uassert" +) +func TestRouter_Render(t *testing.T) { cases := []struct { + label string path string expectedOutput string + setupHandler func(t *testing.T, r *Router) }{ - {"hello/Alice", "Hello, Alice!"}, - {"hi", "Hi, earth!"}, - {"hello/Bob", "Hello, Bob!"}, + { + label: "route with named parameter", + path: "hello/Alice", + expectedOutput: "Hello, Alice!", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/{name}", func(rw *ResponseWriter, req *Request) { + name := req.GetVar("name") + uassert.Equal(t, "Alice", name) + rw.Write("Hello, " + name + "!") + }) + }, + }, + { + label: "static route", + path: "hi", + expectedOutput: "Hi, earth!", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hi", func(rw *ResponseWriter, req *Request) { + uassert.Equal(t, req.Path, "hi") + rw.Write("Hi, earth!") + }) + }, + }, + { + label: "route with named parameter and query string", + path: "hello/foo/bar?foo=bar&baz", + expectedOutput: "foo bar", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/{key}/{val}", func(rw *ResponseWriter, req *Request) { + key := req.GetVar("key") + val := req.GetVar("val") + uassert.Equal(t, "foo", key) + uassert.Equal(t, "bar", val) + uassert.Equal(t, "hello/foo/bar?foo=bar&baz", req.RawPath) + uassert.Equal(t, "hello/foo/bar", req.Path) + rw.Write(key + " " + val) + }) + }, + }, + { + // TODO: finalize how router should behave with double slash in path. + label: "double slash in nested route", + path: "a/foo//", + expectedOutput: "test foo", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("a/{key}", func(rw *ResponseWriter, req *Request) { + // Assert not called + uassert.False(t, true, "unexpected handler called") + }) + + r.HandleFunc("a/{key}/{val}/", func(rw *ResponseWriter, req *Request) { + key := req.GetVar("key") + val := req.GetVar("val") + uassert.Equal(t, key, "foo") + uassert.Empty(t, val) + rw.Write("test " + key) + }) + }, + }, + // TODO: {"hello", "Hello, world!"}, // TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc } for _, tt := range cases { - t.Run(tt.path, func(t *testing.T) { + t.Run(tt.label, func(t *testing.T) { + router := NewRouter() + tt.setupHandler(t, router) output := router.Render(tt.path) if output != tt.expectedOutput { t.Errorf("Expected output %q, but got %q", tt.expectedOutput, output) diff --git a/gnovm/cmd/gno/download_deps_test.go b/gnovm/cmd/gno/download_deps_test.go index 3ccfdb0055e..0828e9b2245 100644 --- a/gnovm/cmd/gno/download_deps_test.go +++ b/gnovm/cmd/gno/download_deps_test.go @@ -60,7 +60,7 @@ func TestDownloadDeps(t *testing.T) { }, }, }, - requirements: []string{"avl", "blog", "ufmt", "mux"}, + requirements: []string{"avl", "blog", "diff", "uassert", "ufmt", "mux"}, ioErrContains: []string{ "gno: downloading gno.land/p/demo/blog", "gno: downloading gno.land/p/demo/avl", From 727868728204f1a4b46e94a86e7ea524be8a3741 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:57:14 +0100 Subject: [PATCH 306/345] docs: hyperlink buttons demo (#3245) ## Description Adds a `r/docs` that showcases how "buttons" can be added to realm renders.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan --- examples/gno.land/r/docs/buttons/buttons.gno | 44 +++++++++++++++++++ .../gno.land/r/docs/buttons/buttons_test.gno | 14 ++++++ examples/gno.land/r/docs/buttons/gno.mod | 1 + examples/gno.land/r/docs/docs.gno | 1 + 4 files changed, 60 insertions(+) create mode 100644 examples/gno.land/r/docs/buttons/buttons.gno create mode 100644 examples/gno.land/r/docs/buttons/buttons_test.gno create mode 100644 examples/gno.land/r/docs/buttons/gno.mod diff --git a/examples/gno.land/r/docs/buttons/buttons.gno b/examples/gno.land/r/docs/buttons/buttons.gno new file mode 100644 index 00000000000..cb050b1bc38 --- /dev/null +++ b/examples/gno.land/r/docs/buttons/buttons.gno @@ -0,0 +1,44 @@ +package buttons + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +var ( + motd = "The Initial Message\n\n" + lastCaller std.Address +) + +func UpdateMOTD(newmotd string) { + motd = newmotd + lastCaller = std.PrevRealm().Addr() +} + +func Render(path string) string { + if path == "motd" { + out := "# Message of the Day:\n\n" + out += "---\n\n" + out += "# " + motd + "\n\n" + out += "---\n\n" + link := txlink.Call("UpdateMOTD", "newmotd", "Message!") // "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + out += ufmt.Sprintf("Click **[here](%s)** to update the Message of The Day!\n\n", link) + out += "[Go back to home page](/r/docs/buttons)\n\n" + out += "Last updated by " + lastCaller.String() + + return out + } + + out := `# Buttons + +Users can create simple hyperlink buttons to view specific realm pages and +do specific realm actions, such as calling a specific function with some arguments. + +The foundation for this functionality are markdown links; for example, you can +click... +` + "\n## [here](/r/docs/buttons:motd)\n" + `...to view this realm's message of the day.` + + return out +} diff --git a/examples/gno.land/r/docs/buttons/buttons_test.gno b/examples/gno.land/r/docs/buttons/buttons_test.gno new file mode 100644 index 00000000000..2903fa1a858 --- /dev/null +++ b/examples/gno.land/r/docs/buttons/buttons_test.gno @@ -0,0 +1,14 @@ +package buttons + +import ( + "strings" + "testing" +) + +func TestRenderMotdLink(t *testing.T) { + res := Render("motd") + const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + if !strings.Contains(res, wantLink) { + t.Fatalf("%s\ndoes not contain correct help page link: %s", res, wantLink) + } +} diff --git a/examples/gno.land/r/docs/buttons/gno.mod b/examples/gno.land/r/docs/buttons/gno.mod new file mode 100644 index 00000000000..43cc2d773da --- /dev/null +++ b/examples/gno.land/r/docs/buttons/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/buttons diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index 57d020cd737..28bac4171b5 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -11,6 +11,7 @@ Explore various examples to learn more about Gno functionality and usage. - [Hello World](/r/docs/hello) - A simple introductory example. - [Adder](/r/docs/adder) - An interactive example to update a number with transactions. - [Source](/r/docs/source) - View realm source code. +- [Buttons](/r/docs/buttons) - Add buttons to your realm's render. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - ... From 918c9ab88d320a1a01404d91f7131063a4bf54f4 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:21:32 +0100 Subject: [PATCH 307/345] chore(examples): update userbook example to use avl_pager (#3251) ## Description Updates the `r/demo/userbook` example to use the `avl_pager` package.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan --- examples/gno.land/r/demo/userbook/render.gno | 51 ++++++ .../gno.land/r/demo/userbook/userbook.gno | 148 +++--------------- .../r/demo/userbook/userbook_test.gno | 79 ---------- 3 files changed, 69 insertions(+), 209 deletions(-) create mode 100644 examples/gno.land/r/demo/userbook/render.gno delete mode 100644 examples/gno.land/r/demo/userbook/userbook_test.gno diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno new file mode 100644 index 00000000000..22d7f97eabd --- /dev/null +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -0,0 +1,51 @@ +// Package userbook demonstrates a small userbook system working with gnoweb +package userbook + +import ( + "sort" + "strconv" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +func Render(path string) string { + p := pager.NewPager(signupsTree, 2) + page := p.MustGetPageByPath(path) + + out := "# Welcome to UserBook!\n\n" + + out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) + out += "---\n\n" + + var sorted sortedSignups + for _, item := range page.Items { + sorted = append(sorted, item.Value.(*Signup)) + } + + sort.Sort(sorted) + + for _, item := range sorted { + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05")) + } + + out += "---\n\n" + out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" + out += page.Selector() // Repeat selector for ease of navigation + return out +} + +type sortedSignups []*Signup + +func (s sortedSignups) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s sortedSignups) Len() int { + return len(s) +} + +func (s sortedSignups) Less(i, j int) bool { + return s[i].timestamp.Before(s[j].timestamp) +} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c49bd90fa42..c958dc9e5b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -1,158 +1,46 @@ -// This realm demonstrates a small userbook system working with gnoweb +// Package userbook demonstrates a small userbook system working with gnoweb package userbook import ( "std" - "strconv" + "time" "gno.land/p/demo/avl" - "gno.land/p/demo/mux" "gno.land/p/demo/ufmt" ) type Signup struct { - account string - height int64 + address std.Address + ordinal int + timestamp time.Time } -// signups - keep a slice of signed up addresses efficient pagination -var signups []Signup +var signupsTree = avl.NewTree() -// tracker - keep track of who signed up -var ( - tracker *avl.Tree - router *mux.Router -) - -const ( - defaultPageSize = 20 - pathArgument = "number" - subPath = "page/{" + pathArgument + "}" - signUpEvent = "SignUp" -) +const signUpEvent = "SignUp" func init() { - // Set up tracker tree - tracker = avl.NewTree() - - // Set up route handling - router = mux.NewRouter() - router.HandleFunc("", renderHelper) - router.HandleFunc(subPath, renderHelper) - - // Sign up the deployer - SignUp() + SignUp() // Sign up the deployer } func SignUp() string { // Get transaction caller - caller := std.PrevRealm().Addr().String() - height := std.GetHeight() + caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := tracker.Get(caller); exists { + if _, exists := signupsTree.Get(caller.String()); exists { panic(caller + " is already signed up!") } + now := time.Now() // Sign up the user - tracker.Set(caller, struct{}{}) - signup := Signup{ - caller, - height, - } - - signups = append(signups, signup) - std.Emit(signUpEvent, "SignedUpAccount", signup.account) + signupsTree.Set(caller.String(), &Signup{ + std.PrevRealm().Addr(), + signupsTree.Size(), + now, + }) - return ufmt.Sprintf("%s added to userbook up at block #%d!", signup.account, signup.height) -} - -func GetSignupsInRange(page, pageSize int) ([]Signup, int) { - if page < 1 { - panic("page number cannot be less than 1") - } - - if pageSize < 1 || pageSize > 50 { - panic("page size must be from 1 to 50") - } - - // Pagination - // Calculate indexes - startIndex := (page - 1) * pageSize - endIndex := startIndex + pageSize - - // If page does not contain any users - if startIndex >= len(signups) { - return nil, -1 - } - - // If page contains fewer users than the page size - if endIndex > len(signups) { - endIndex = len(signups) - } - - return signups[startIndex:endIndex], endIndex -} - -func renderHelper(res *mux.ResponseWriter, req *mux.Request) { - totalSignups := len(signups) - res.Write("# Welcome to UserBook!\n\n") - - // Get URL parameter - page, err := strconv.Atoi(req.GetVar("number")) - if err != nil { - page = 1 // render first page on bad input - } - - // Fetch paginated signups - fetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize) - // Handle empty page case - if len(fetchedSignups) == 0 { - res.Write("No users on this page!\n\n") - res.Write("---\n\n") - res.Write("[Back to Page #1](/r/demo/userbook:page/1)\n\n") - return - } - - // Write page title - res.Write(ufmt.Sprintf("## UserBook - Page #%d:\n\n", page)) - - // Write signups - pageStartIndex := defaultPageSize * (page - 1) - for i, signup := range fetchedSignups { - out := ufmt.Sprintf("#### User #%d - %s - signed up at Block #%d\n", pageStartIndex+i, signup.account, signup.height) - res.Write(out) - } - - res.Write("---\n\n") - - // Write UserBook info - latestSignupIndex := totalSignups - 1 - res.Write(ufmt.Sprintf("#### Total users: %d\n", totalSignups)) - res.Write(ufmt.Sprintf("#### Latest signup: User #%d at Block #%d\n", latestSignupIndex, signups[latestSignupIndex].height)) - - res.Write("---\n\n") - - // Write page number - res.Write(ufmt.Sprintf("You're viewing page #%d", page)) - - // Write navigation buttons - var prevPage string - var nextPage string - // If we are on any page that is not the first page - if page > 1 { - prevPage = ufmt.Sprintf(" - [Previous page](/r/demo/userbook:page/%d)", page-1) - } - - // If there are more pages after the current one - if endIndex < totalSignups { - nextPage = ufmt.Sprintf(" - [Next page](/r/demo/userbook:page/%d)\n\n", page+1) - } - - res.Write(prevPage) - res.Write(nextPage) -} + std.Emit(signUpEvent, "SignedUpAccount", caller.String()) -func Render(path string) string { - return router.Render(path) + return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/userbook/userbook_test.gno b/examples/gno.land/r/demo/userbook/userbook_test.gno deleted file mode 100644 index 8d10d381e08..00000000000 --- a/examples/gno.land/r/demo/userbook/userbook_test.gno +++ /dev/null @@ -1,79 +0,0 @@ -package userbook - -import ( - "std" - "strings" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" -) - -func TestRender(t *testing.T) { - // Sign up 20 users + deployer - for i := 0; i < 20; i++ { - addrName := ufmt.Sprintf("test%d", i) - caller := testutils.TestAddress(addrName) - std.TestSetOrigCaller(caller) - SignUp() - } - - testCases := []struct { - name string - nextPage bool - prevPage bool - path string - expectedNumberOfUsers int - }{ - { - name: "1st page render", - nextPage: true, - prevPage: false, - path: "page/1", - expectedNumberOfUsers: 20, - }, - { - name: "2nd page render", - nextPage: false, - prevPage: true, - path: "page/2", - expectedNumberOfUsers: 1, - }, - { - name: "Invalid path render", - nextPage: true, - prevPage: false, - path: "page/invalidtext", - expectedNumberOfUsers: 20, - }, - { - name: "Empty Page", - nextPage: false, - prevPage: false, - path: "page/1000", - expectedNumberOfUsers: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := Render(tc.path) - numUsers := countUsers(got) - - if tc.prevPage && !strings.Contains(got, "Previous page") { - t.Fatalf("expected to find Previous page, didn't find it") - } - if tc.nextPage && !strings.Contains(got, "Next page") { - t.Fatalf("expected to find Next page, didn't find it") - } - - if tc.expectedNumberOfUsers != numUsers { - t.Fatalf("expected %d, got %d users", tc.expectedNumberOfUsers, numUsers) - } - }) - } -} - -func countUsers(input string) int { - return strings.Count(input, "#### User #") -} From 61a5c020ccf28ce1877aa783b264a6bb6f482fd2 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:33:10 +0100 Subject: [PATCH 308/345] feat: add `p/moul/{md,debug,web25}` + update `r/moul/home` (#2819) Related with https://hackmd.io/a8k09_TeQUu6WawvkpDDpw?both - [x] `p/moul/web25` - displays a link suggesting to view the realm from an external webui, but avoid recursive printing when seen from the external webui - [x] `p/moul/md` - minimal `markdown` helpers library - [x] `p/moul/debug` - displays useful informations when adding `?debug=1` - [x] update `r/moul/home` - [x] markdown table with links example - [x] svg example - [x] use of the new `p/moul/...` packages - [x] Release a static web25 interface somewhere (https://github.com/moul/gno-moul-home-web25) --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 12 +- contribs/gnodev/cmd/gnodev/setup_web.go | 1 + examples/gno.land/p/moul/debug/debug.gno | 92 +++++++ examples/gno.land/p/moul/debug/gno.mod | 1 + .../gno.land/p/moul/debug/z1_filetest.gno | 31 +++ .../gno.land/p/moul/debug/z2_filetest.gno | 37 +++ examples/gno.land/p/moul/md/gno.mod | 1 + examples/gno.land/p/moul/md/md.gno | 242 ++++++++++++++++++ examples/gno.land/p/moul/md/md_test.gno | 88 +++++++ examples/gno.land/p/moul/md/z1_filetest.gno | 87 +++++++ examples/gno.land/p/moul/web25/gno.mod | 1 + examples/gno.land/p/moul/web25/web25.gno | 51 ++++ examples/gno.land/p/moul/web25/web25_test.gno | 1 + .../gno.land/r/moul/config/config_test.gno | 1 + examples/gno.land/r/moul/home/home.gno | 83 ++++-- examples/gno.land/r/moul/home/z1_filetest.gno | 24 +- examples/gno.land/r/moul/home/z2_filetest.gno | 63 ++++- 17 files changed, 778 insertions(+), 38 deletions(-) create mode 100644 examples/gno.land/p/moul/debug/debug.gno create mode 100644 examples/gno.land/p/moul/debug/gno.mod create mode 100644 examples/gno.land/p/moul/debug/z1_filetest.gno create mode 100644 examples/gno.land/p/moul/debug/z2_filetest.gno create mode 100644 examples/gno.land/p/moul/md/gno.mod create mode 100644 examples/gno.land/p/moul/md/md.gno create mode 100644 examples/gno.land/p/moul/md/md_test.gno create mode 100644 examples/gno.land/p/moul/md/z1_filetest.gno create mode 100644 examples/gno.land/p/moul/web25/gno.mod create mode 100644 examples/gno.land/p/moul/web25/web25.gno create mode 100644 examples/gno.land/p/moul/web25/web25_test.gno create mode 100644 examples/gno.land/r/moul/config/config_test.gno diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index c9d6487d753..082d0cb8270 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -59,6 +59,7 @@ type devCfg struct { // Web Configuration webListenerAddr string webRemoteHelperAddr string + webWithHTML bool // Node Configuration minimal bool @@ -126,14 +127,21 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { &c.webListenerAddr, "web-listener", defaultDevOptions.webListenerAddr, - "web server listener address", + "gnoweb: web server listener address", ) fs.StringVar( &c.webRemoteHelperAddr, "web-help-remote", defaultDevOptions.webRemoteHelperAddr, - "web server help page's remote addr (default to )", + "gnoweb: web server help page's remote addr (default to )", + ) + + fs.BoolVar( + &c.webWithHTML, + "web-with-html", + defaultDevOptions.webWithHTML, + "gnoweb: enable HTML parsing in markdown rendering", ) fs.StringVar( diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index 635c27af19d..d55814142a6 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -15,6 +15,7 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) htt webConfig.HelpChainID = cfg.chainId webConfig.RemoteAddr = dnode.GetRemoteAddress() webConfig.HelpRemote = cfg.webRemoteHelperAddr + webConfig.WithHTML = cfg.webWithHTML // If `HelpRemote` is empty default it to `RemoteAddr` if webConfig.HelpRemote == "" { diff --git a/examples/gno.land/p/moul/debug/debug.gno b/examples/gno.land/p/moul/debug/debug.gno new file mode 100644 index 00000000000..9ba3dd36a98 --- /dev/null +++ b/examples/gno.land/p/moul/debug/debug.gno @@ -0,0 +1,92 @@ +// Package debug provides utilities for logging and displaying debug information +// within Gno realms. It supports conditional rendering of logs and metadata, +// toggleable via query parameters. +// +// Key Features: +// - Log collection and display using Markdown formatting. +// - Metadata display for realm path, address, and height. +// - Collapsible debug section for cleaner presentation. +// - Query-based debug toggle using `?debug=1`. +package debug + +import ( + "std" + "time" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/realmpath" +) + +// Debug encapsulates debug information, including logs and metadata. +type Debug struct { + Logs []string + HideMetadata bool +} + +// Log appends a new line of debug information to the Logs slice. +func (d *Debug) Log(line string) { + d.Logs = append(d.Logs, line) +} + +// Render generates the debug content as a collapsible Markdown section. +// It conditionally renders logs and metadata if enabled via the `?debug=1` query parameter. +func (d Debug) Render(path string) string { + if realmpath.Parse(path).Query.Get("debug") != "1" { + return "" + } + + var content string + + if d.Logs != nil { + content += md.H3("Logs") + content += md.BulletList(d.Logs) + } + + if !d.HideMetadata { + content += md.H3("Metadata") + table := mdtable.Table{ + Headers: []string{"Key", "Value"}, + } + table.Append([]string{"`std.CurrentRealm().PkgPath()`", string(std.CurrentRealm().PkgPath())}) + table.Append([]string{"`std.CurrentRealm().Addr()`", string(std.CurrentRealm().Addr())}) + table.Append([]string{"`std.PrevRealm().PkgPath()`", string(std.PrevRealm().PkgPath())}) + table.Append([]string{"`std.PrevRealm().Addr()`", string(std.PrevRealm().Addr())}) + table.Append([]string{"`std.GetHeight()`", ufmt.Sprintf("%d", std.GetHeight())}) + table.Append([]string{"`time.Now().Format(time.RFC3339)`", time.Now().Format(time.RFC3339)}) + content += table.String() + } + + if content == "" { + return "" + } + + return md.CollapsibleSection("debug", content) +} + +// Render displays metadata about the current realm but does not display logs. +// This function uses a default Debug struct with metadata enabled and no logs. +func Render(path string) string { + return Debug{}.Render(path) +} + +// IsEnabled checks if the `?debug=1` query parameter is set in the given path. +// Returns true if debugging is enabled, otherwise false. +func IsEnabled(path string) bool { + req := realmpath.Parse(path) + return req.Query.Get("debug") == "1" +} + +// ToggleURL modifies the given path's query string to toggle the `?debug=1` parameter. +// If debugging is currently enabled, it removes the parameter. +// If debugging is disabled, it adds the parameter. +func ToggleURL(path string) string { + req := realmpath.Parse(path) + if IsEnabled(path) { + req.Query.Del("debug") + } else { + req.Query.Add("debug", "1") + } + return req.String() +} diff --git a/examples/gno.land/p/moul/debug/gno.mod b/examples/gno.land/p/moul/debug/gno.mod new file mode 100644 index 00000000000..eb48ed292ca --- /dev/null +++ b/examples/gno.land/p/moul/debug/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/debug diff --git a/examples/gno.land/p/moul/debug/z1_filetest.gno b/examples/gno.land/p/moul/debug/z1_filetest.gno new file mode 100644 index 00000000000..8203749d3c7 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z1_filetest.gno @@ -0,0 +1,31 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + println("---") + println(debug.Render("")) + println("---") + println(debug.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/debug/z2_filetest.gno b/examples/gno.land/p/moul/debug/z2_filetest.gno new file mode 100644 index 00000000000..32c2fe49951 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z2_filetest.gno @@ -0,0 +1,37 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + var d debug.Debug + d.Log("hello world!") + d.Log("foobar") + println("---") + println(d.Render("")) + println("---") + println(d.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Logs +// - hello world! +// - foobar +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/md/gno.mod b/examples/gno.land/p/moul/md/gno.mod new file mode 100644 index 00000000000..55d124d9e6b --- /dev/null +++ b/examples/gno.land/p/moul/md/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/md diff --git a/examples/gno.land/p/moul/md/md.gno b/examples/gno.land/p/moul/md/md.gno new file mode 100644 index 00000000000..61d6948b997 --- /dev/null +++ b/examples/gno.land/p/moul/md/md.gno @@ -0,0 +1,242 @@ +// Package md provides helper functions for generating Markdown content programmatically. +// +// It includes utilities for text formatting, creating lists, blockquotes, code blocks, +// links, images, and more. +// +// Highlights: +// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists. +// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists). +// - Includes advanced helpers like inline images with links and nested list prefixes. +package md + +import ( + "strconv" + "strings" +) + +// Bold returns bold text for markdown. +// Example: Bold("foo") => "**foo**" +func Bold(text string) string { + return "**" + text + "**" +} + +// Italic returns italicized text for markdown. +// Example: Italic("foo") => "*foo*" +func Italic(text string) string { + return "*" + text + "*" +} + +// Strikethrough returns strikethrough text for markdown. +// Example: Strikethrough("foo") => "~~foo~~" +func Strikethrough(text string) string { + return "~~" + text + "~~" +} + +// H1 returns a level 1 header for markdown. +// Example: H1("foo") => "# foo\n" +func H1(text string) string { + return "# " + text + "\n" +} + +// H2 returns a level 2 header for markdown. +// Example: H2("foo") => "## foo\n" +func H2(text string) string { + return "## " + text + "\n" +} + +// H3 returns a level 3 header for markdown. +// Example: H3("foo") => "### foo\n" +func H3(text string) string { + return "### " + text + "\n" +} + +// H4 returns a level 4 header for markdown. +// Example: H4("foo") => "#### foo\n" +func H4(text string) string { + return "#### " + text + "\n" +} + +// H5 returns a level 5 header for markdown. +// Example: H5("foo") => "##### foo\n" +func H5(text string) string { + return "##### " + text + "\n" +} + +// H6 returns a level 6 header for markdown. +// Example: H6("foo") => "###### foo\n" +func H6(text string) string { + return "###### " + text + "\n" +} + +// BulletList returns a bullet list for markdown. +// Example: BulletList([]string{"foo", "bar"}) => "- foo\n- bar\n" +func BulletList(items []string) string { + var sb strings.Builder + for _, item := range items { + sb.WriteString(BulletItem(item)) + } + return sb.String() +} + +// BulletItem returns a bullet item for markdown. +// Example: BulletItem("foo") => "- foo\n" +func BulletItem(item string) string { + var sb strings.Builder + lines := strings.Split(item, "\n") + sb.WriteString("- " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// OrderedList returns an ordered list for markdown. +// Example: OrderedList([]string{"foo", "bar"}) => "1. foo\n2. bar\n" +func OrderedList(items []string) string { + var sb strings.Builder + for i, item := range items { + lines := strings.Split(item, "\n") + sb.WriteString(strconv.Itoa(i+1) + ". " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + } + return sb.String() +} + +// TodoList returns a list of todo items with checkboxes for markdown. +// Example: TodoList([]string{"foo", "bar\nmore bar"}, []bool{true, false}) => "- [x] foo\n- [ ] bar\n more bar\n" +func TodoList(items []string, done []bool) string { + var sb strings.Builder + for i, item := range items { + sb.WriteString(TodoItem(item, done[i])) + } + return sb.String() +} + +// TodoItem returns a todo item with checkbox for markdown. +// Example: TodoItem("foo", true) => "- [x] foo\n" +func TodoItem(item string, done bool) string { + var sb strings.Builder + checkbox := " " + if done { + checkbox = "x" + } + lines := strings.Split(item, "\n") + sb.WriteString("- [" + checkbox + "] " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// Nested prefixes each line with a given prefix, enabling nested lists. +// Example: Nested("- foo\n- bar", " ") => " - foo\n - bar\n" +func Nested(content, prefix string) string { + lines := strings.Split(content, "\n") + for i := range lines { + if strings.TrimSpace(lines[i]) != "" { + lines[i] = prefix + lines[i] + } + } + return strings.Join(lines, "\n") +} + +// Blockquote returns a blockquote for markdown. +// Example: Blockquote("foo\nbar") => "> foo\n> bar\n" +func Blockquote(text string) string { + lines := strings.Split(text, "\n") + var sb strings.Builder + for _, line := range lines { + sb.WriteString("> " + line + "\n") + } + return sb.String() +} + +// InlineCode returns inline code for markdown. +// Example: InlineCode("foo") => "`foo`" +func InlineCode(code string) string { + return "`" + strings.ReplaceAll(code, "`", "\\`") + "`" +} + +// CodeBlock creates a markdown code block. +// Example: CodeBlock("foo") => "```\nfoo\n```" +func CodeBlock(content string) string { + return "```\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting. +// Example: LanguageCodeBlock("go", "foo") => "```go\nfoo\n```" +func LanguageCodeBlock(language, content string) string { + return "```" + language + "\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// HorizontalRule returns a horizontal rule for markdown. +// Example: HorizontalRule() => "---\n" +func HorizontalRule() string { + return "---\n" +} + +// Link returns a hyperlink for markdown. +// Example: Link("foo", "http://example.com") => "[foo](http://example.com)" +func Link(text, url string) string { + return "[" + EscapeText(text) + "](" + url + ")" +} + +// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown. +// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[![alt text](image-url)](link-url)" +func InlineImageWithLink(altText, imageUrl, linkUrl string) string { + return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")" +} + +// Image returns an image for markdown. +// Example: Image("foo", "http://example.com") => "![foo](http://example.com)" +func Image(altText, url string) string { + return "![" + EscapeText(altText) + "](" + url + ")" +} + +// Footnote returns a footnote for markdown. +// Example: Footnote("foo", "bar") => "[foo]: bar" +func Footnote(reference, text string) string { + return "[" + EscapeText(reference) + "]: " + text +} + +// Paragraph wraps the given text in a Markdown paragraph. +// Example: Paragraph("foo") => "foo\n" +func Paragraph(content string) string { + return content + "\n\n" +} + +// CollapsibleSection creates a collapsible section for markdown using +// HTML
and tags. +// Example: +// CollapsibleSection("Click to expand", "Hidden content") +// => +//
Click to expand +// +// Hidden content +//
+func CollapsibleSection(title, content string) string { + return "
" + EscapeText(title) + "\n\n" + content + "\n
\n" +} + +// EscapeText escapes special Markdown characters in regular text where needed. +func EscapeText(text string) string { + replacer := strings.NewReplacer( + `*`, `\*`, + `_`, `\_`, + `[`, `\[`, + `]`, `\]`, + `(`, `\(`, + `)`, `\)`, + `~`, `\~`, + `>`, `\>`, + `|`, `\|`, + `-`, `\-`, + `+`, `\+`, + ".", `\.`, + "!", `\!`, + "`", "\\`", + ) + return replacer.Replace(text) +} diff --git a/examples/gno.land/p/moul/md/md_test.gno b/examples/gno.land/p/moul/md/md_test.gno new file mode 100644 index 00000000000..144ae58d918 --- /dev/null +++ b/examples/gno.land/p/moul/md/md_test.gno @@ -0,0 +1,88 @@ +package md + +import ( + "testing" + + "gno.land/p/moul/md" +) + +func TestHelpers(t *testing.T) { + tests := []struct { + name string + function func() string + expected string + }{ + {"Bold", func() string { return md.Bold("foo") }, "**foo**"}, + {"Italic", func() string { return md.Italic("foo") }, "*foo*"}, + {"Strikethrough", func() string { return md.Strikethrough("foo") }, "~~foo~~"}, + {"H1", func() string { return md.H1("foo") }, "# foo\n"}, + {"HorizontalRule", md.HorizontalRule, "---\n"}, + {"InlineCode", func() string { return md.InlineCode("foo") }, "`foo`"}, + {"CodeBlock", func() string { return md.CodeBlock("foo") }, "```\nfoo\n```"}, + {"LanguageCodeBlock", func() string { return md.LanguageCodeBlock("go", "foo") }, "```go\nfoo\n```"}, + {"Link", func() string { return md.Link("foo", "http://example.com") }, "[foo](http://example.com)"}, + {"Image", func() string { return md.Image("foo", "http://example.com") }, "![foo](http://example.com)"}, + {"InlineImageWithLink", func() string { return md.InlineImageWithLink("alt", "image-url", "link-url") }, "[![alt](image-url)](link-url)"}, + {"Footnote", func() string { return md.Footnote("foo", "bar") }, "[foo]: bar"}, + {"Paragraph", func() string { return md.Paragraph("foo") }, "foo\n\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.function() + if result != tt.expected { + t.Errorf("%s() = %q, want %q", tt.name, result, tt.expected) + } + }) + } +} + +func TestLists(t *testing.T) { + t.Run("BulletList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "- foo\n- bar\n" + result := md.BulletList(items) + if result != expected { + t.Errorf("BulletList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("OrderedList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "1. foo\n2. bar\n" + result := md.OrderedList(items) + if result != expected { + t.Errorf("OrderedList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("TodoList", func(t *testing.T) { + items := []string{"foo", "bar\nmore bar"} + done := []bool{true, false} + expected := "- [x] foo\n- [ ] bar\n more bar\n" + result := md.TodoList(items, done) + if result != expected { + t.Errorf("TodoList(%q, %q) = %q, want %q", items, done, result, expected) + } + }) +} + +func TestNested(t *testing.T) { + t.Run("Nested Single Level", func(t *testing.T) { + content := "- foo\n- bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) + + t.Run("Nested Double Level", func(t *testing.T) { + content := " - foo\n - bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) +} diff --git a/examples/gno.land/p/moul/md/z1_filetest.gno b/examples/gno.land/p/moul/md/z1_filetest.gno new file mode 100644 index 00000000000..077e1732bcb --- /dev/null +++ b/examples/gno.land/p/moul/md/z1_filetest.gno @@ -0,0 +1,87 @@ +package main + +import "gno.land/p/moul/md" + +func main() { + println(md.H1("Header 1")) + println(md.H2("Header 2")) + println(md.H3("Header 3")) + println(md.H4("Header 4")) + println(md.H5("Header 5")) + println(md.H6("Header 6")) + println(md.Bold("bold")) + println(md.Italic("italic")) + println(md.Strikethrough("strikethrough")) + println(md.BulletList([]string{ + "Item 1", + "Item 2\nMore details for item 2", + })) + println(md.OrderedList([]string{"Step 1", "Step 2"})) + println(md.TodoList([]string{"Task 1", "Task 2\nSubtask 2"}, []bool{true, false})) + println(md.Nested(md.BulletList([]string{"Parent Item", md.OrderedList([]string{"Child 1", "Child 2"})}), " ")) + println(md.Blockquote("This is a blockquote\nSpanning multiple lines")) + println(md.InlineCode("inline `code`")) + println(md.CodeBlock("line1\nline2")) + println(md.LanguageCodeBlock("go", "func main() {\nprintln(\"Hello, world!\")\n}")) + println(md.HorizontalRule()) + println(md.Link("Gno", "http://gno.land")) + println(md.Image("Alt Text", "http://example.com/image.png")) + println(md.InlineImageWithLink("Alt Text", "http://example.com/image.png", "http://example.com")) + println(md.Footnote("ref", "This is a footnote")) + println(md.Paragraph("This is a paragraph.")) +} + +// Output: +// # Header 1 +// +// ## Header 2 +// +// ### Header 3 +// +// #### Header 4 +// +// ##### Header 5 +// +// ###### Header 6 +// +// **bold** +// *italic* +// ~~strikethrough~~ +// - Item 1 +// - Item 2 +// More details for item 2 +// +// 1. Step 1 +// 2. Step 2 +// +// - [x] Task 1 +// - [ ] Task 2 +// Subtask 2 +// +// - Parent Item +// - 1. Child 1 +// 2. Child 2 +// +// +// > This is a blockquote +// > Spanning multiple lines +// +// `inline \`code\`` +// ``` +// line1 +// line2 +// ``` +// ```go +// func main() { +// println("Hello, world!") +// } +// ``` +// --- +// +// [Gno](http://gno.land) +// ![Alt Text](http://example.com/image.png) +// [![Alt Text](http://example.com/image.png)](http://example.com) +// [ref]: This is a footnote +// This is a paragraph. +// +// diff --git a/examples/gno.land/p/moul/web25/gno.mod b/examples/gno.land/p/moul/web25/gno.mod new file mode 100644 index 00000000000..f27bc793bf7 --- /dev/null +++ b/examples/gno.land/p/moul/web25/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/web25 diff --git a/examples/gno.land/p/moul/web25/web25.gno b/examples/gno.land/p/moul/web25/web25.gno new file mode 100644 index 00000000000..46d564b70ad --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25.gno @@ -0,0 +1,51 @@ +// Pacakge web25 provides an opinionated way to register an external web2 +// frontend to provide a "better" web2.5 experience. +package web25 + +import ( + "strings" + + "gno.land/p/moul/realmpath" +) + +type Config struct { + CID string + URL string + Text string +} + +func (c *Config) SetRemoteFrontendByURL(url string) { + c.CID = "" + c.URL = url +} + +func (c *Config) SetRemoteFrontendByCID(cid string) { + c.CID = cid + c.URL = "" +} + +func (c Config) GetLink() string { + if c.CID != "" { + return "https://ipfs.io/ipfs/" + c.CID + } + return c.URL +} + +const DefaultText = "Click [here]({link}) to visit the full rendering experience.\n" + +// Render displays a frontend link at the top of your realm's Render function in +// a concistent way to help gno visitors to have a consistent experience. +// +// if query is not nil, then it will check if it's not disable by ?no-web25, so +// that you can call the render function from an external point of view. +func (c Config) Render(path string) string { + if realmpath.Parse(path).Query.Get("no-web25") == "1" { + return "" + } + text := c.Text + if text == "" { + text = DefaultText + } + text = strings.ReplaceAll(text, "{link}", c.GetLink()) + return text +} diff --git a/examples/gno.land/p/moul/web25/web25_test.gno b/examples/gno.land/p/moul/web25/web25_test.gno new file mode 100644 index 00000000000..6d58a586595 --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25_test.gno @@ -0,0 +1 @@ +package web25 diff --git a/examples/gno.land/r/moul/config/config_test.gno b/examples/gno.land/r/moul/config/config_test.gno new file mode 100644 index 00000000000..d912156bec0 --- /dev/null +++ b/examples/gno.land/r/moul/config/config_test.gno @@ -0,0 +1 @@ +package config diff --git a/examples/gno.land/r/moul/home/home.gno b/examples/gno.land/r/moul/home/home.gno index 140e7b5e0c8..1094ce29cc5 100644 --- a/examples/gno.land/r/moul/home/home.gno +++ b/examples/gno.land/r/moul/home/home.gno @@ -1,14 +1,23 @@ package home import ( + "strconv" + + "gno.land/p/demo/svg" + "gno.land/p/moul/debug" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/txlink" + "gno.land/p/moul/web25" "gno.land/r/leon/hof" "gno.land/r/moul/config" ) var ( - todos []string - status string - memeImgURL string + todos []string + status string + memeImgURL string + web25config = web25.Config{URL: "https://moul.github.io/gno-moul-home-web25/"} ) func init() { @@ -19,36 +28,74 @@ func init() { } func Render(path string) string { - content := "# Manfred's (gn)home Dashboard\n\n" + content := web25config.Render(path) + var d debug.Debug + + content += md.H1("Manfred's (gn)home Dashboard") - content += "## Meme\n" - content += "![](" + memeImgURL + ")\n\n" + content += md.H2("Meme") + content += md.Paragraph( + md.Image("meme", memeImgURL), + ) - content += "## Status\n" - content += status + "\n\n" + content += md.H2("Status") + content += md.Paragraph(status) + content += md.Paragraph(md.Link("update", txlink.Call("UpdateStatus"))) - content += "## Personal ToDo List\n" - for _, todo := range todos { - content += "- [ ] " + todo + "\n" + d.Log("hello world!") + + content += md.H2("Personal TODO List (bullet list)") + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("x", txlink.Call("DeleteTodo", "idx", idstr)) + content += md.BulletItem(todo + " " + deleteLink) } - content += "\n" + content += md.BulletItem(md.Link("[new]", txlink.Call("AddTodo"))) + + content += md.H2("Personal TODO List (table)") + table := mdtable.Table{ + Headers: []string{"ID", "Item", "Links"}, + } + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("[del]", txlink.Call("DeleteTodo", "idx", idstr)) + table.Append([]string{"#" + idstr, todo, deleteLink}) + } + content += table.String() + + content += md.H2("SVG Example") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph(svg.Canvas{ + Width: 500, Height: 500, + Elems: []svg.Elem{ + svg.Rectangle{50, 50, 100, 100, "red"}, + svg.Circle{50, 50, 100, "red"}, + svg.Text{100, 100, "hello world!", "magenta"}, + }, + }.String()) - // TODO: Implement a feature to list replies on r/boards on my posts - // TODO: Maybe integrate a calendar feature for upcoming events? + content += md.H2("Debug") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph( + md.Link("toggle debug", debug.ToggleURL(path)), + ) + // TODO: my r/boards posts + // TODO: my r/events events + content += d.Render(path) return content } -func AddNewTodo(todo string) { +func AddTodo(todo string) { config.AssertIsAdmin() todos = append(todos, todo) } -func DeleteTodo(todoIndex int) { +func DeleteTodo(idx int) { config.AssertIsAdmin() - if todoIndex >= 0 && todoIndex < len(todos) { + if idx >= 0 && idx < len(todos) { // Remove the todo from the list by merging slices from before and after the todo - todos = append(todos[:todoIndex], todos[todoIndex+1:]...) + todos = append(todos[:idx], todos[idx+1:]...) } else { panic("Invalid todo index") } diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno index b26c919dd3a..b9d7d91a702 100644 --- a/examples/gno.land/r/moul/home/z1_filetest.gno +++ b/examples/gno.land/r/moul/home/z1_filetest.gno @@ -7,15 +7,31 @@ func main() { } // Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. // # Manfred's (gn)home Dashboard -// // ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) +// ![meme](https://i.imgflip.com/7ze8dc.jpg) // // ## Status // Online // -// ## Personal ToDo List -// - [ ] fill this todo list... +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:?debug=1) // // diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno index 489dc2aeecd..f471280d8ef 100644 --- a/examples/gno.land/r/moul/home/z2_filetest.gno +++ b/examples/gno.land/r/moul/home/z2_filetest.gno @@ -8,30 +8,65 @@ import ( func main() { std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - home.AddNewTodo("aaa") - home.AddNewTodo("bbb") - home.AddNewTodo("ccc") - home.AddNewTodo("ddd") - home.AddNewTodo("eee") + home.AddTodo("aaa") + home.AddTodo("bbb") + home.AddTodo("ccc") + home.AddTodo("ddd") + home.AddTodo("eee") home.UpdateStatus("Lorem Ipsum") home.DeleteTodo(3) - println(home.Render("")) + println(home.Render("?debug=1")) } // Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. // # Manfred's (gn)home Dashboard -// // ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) +// ![meme](https://i.imgflip.com/7ze8dc.jpg) // // ## Status // Lorem Ipsum // -// ## Personal ToDo List -// - [ ] fill this todo list... -// - [ ] aaa -// - [ ] bbb -// - [ ] ddd -// - [ ] eee +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - aaa [x](/r/moul/home$help&func=DeleteTodo&idx=1) +// - bbb [x](/r/moul/home$help&func=DeleteTodo&idx=2) +// - ddd [x](/r/moul/home$help&func=DeleteTodo&idx=3) +// - eee [x](/r/moul/home$help&func=DeleteTodo&idx=4) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// | #1 | aaa | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=1) | +// | #2 | bbb | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=2) | +// | #3 | ddd | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=3) | +// | #4 | eee | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=4) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:) +// +//
debug +// +// ### Logs +// - hello world! +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | gno.land/r/moul/home | +// | `std.CurrentRealm().Addr()` | g1h8h57ntxadcze3f703skymfzdwa6t3ugf0nq3z | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1manfred47kzduec920z88wfr64ylksmdcedlf5 | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | // +//
// From 052a2a1cfdeddbb8a3c6ac291917226aaf46491e Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:06:50 +0100 Subject: [PATCH 309/345] feat(examples): add avl_pager reverse option, update userbook (#3297) ## Description This PR adds the reverse option in the avl_pager package, and updates the rendering in the `r/demo/userbook` realm.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/p/demo/avl/pager/pager.gno | 24 +++-- .../gno.land/p/demo/avl/pager/pager_test.gno | 93 +++++++++++++------ .../gno.land/p/demo/avl/pager/z_filetest.gno | 4 +- examples/gno.land/r/demo/userbook/render.gno | 35 +++---- .../gno.land/r/demo/userbook/userbook.gno | 20 ++-- examples/gno.land/r/demo/users/users.gno | 4 +- .../gno.land/r/docs/avl_pager/avl_pager.gno | 6 +- examples/gno.land/r/leon/hof/render.gno | 4 +- 8 files changed, 115 insertions(+), 75 deletions(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index 60bb44d97b6..cccdc0df645 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -15,6 +15,7 @@ type Pager struct { PageQueryParam string SizeQueryParam string DefaultPageSize int + Reversed bool } // Page represents a single page of results. @@ -36,12 +37,13 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree *avl.Tree, defaultPageSize int) *Pager { +func NewPager(tree *avl.Tree, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", SizeQueryParam: "size", DefaultPageSize: defaultPageSize, + Reversed: reversed, } } @@ -86,10 +88,18 @@ func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { } items := []Item{} - p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { - items = append(items, Item{Key: key, Value: value}) - return false - }) + + if p.Reversed { + p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } else { + p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } page.Items = items page.PageNumber = pageNumber @@ -115,8 +125,8 @@ func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { return p.GetPageWithSize(pageNumber, pageSize), nil } -// UI generates the Markdown UI for the page selector. -func (p *Page) Selector() string { +// Picker generates the Markdown UI for the page Picker +func (p *Page) Picker() string { pageNumber := p.PageNumber pageNumber = max(pageNumber, 1) diff --git a/examples/gno.land/p/demo/avl/pager/pager_test.gno b/examples/gno.land/p/demo/avl/pager/pager_test.gno index da4680db8c7..9869924e5b5 100644 --- a/examples/gno.land/p/demo/avl/pager/pager_test.gno +++ b/examples/gno.land/p/demo/avl/pager/pager_test.gno @@ -18,34 +18,67 @@ func TestPager_GetPage(t *testing.T) { tree.Set("d", 4) tree.Set("e", 5) - // Create a new pager. - pager := NewPager(tree, 10) + t.Run("normal ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, + {3, 2, []Item{{Key: "e", Value: 5}}}, + {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {2, 5, []Item{}}, + } - // Define test cases. - tests := []struct { - pageNumber int - pageSize int - expected []Item - }{ - {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, - {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, - {3, 2, []Item{{Key: "e", Value: 5}}}, - {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, - {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {2, 5, []Item{}}, - } + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - for _, tt := range tests { - page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + uassert.Equal(t, len(tt.expected), len(page.Items)) - uassert.Equal(t, len(tt.expected), len(page.Items)) + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } + } + }) + + t.Run("reversed ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, true) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "b", Value: 2}}}, + {3, 2, []Item{{Key: "a", Value: 1}}}, + {1, 3, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {1, 5, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}, {Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {2, 5, []Item{}}, + } - for i, item := range page.Items { - uassert.Equal(t, tt.expected[i].Key, item.Key) - uassert.Equal(t, tt.expected[i].Value, item.Value) + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + uassert.Equal(t, len(tt.expected), len(page.Items)) + + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } } - } + }) } func TestPager_GetPageByPath(t *testing.T) { @@ -56,7 +89,7 @@ func TestPager_GetPageByPath(t *testing.T) { } // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { @@ -80,7 +113,7 @@ func TestPager_GetPageByPath(t *testing.T) { } } -func TestPage_Selector(t *testing.T) { +func TestPage_Picker(t *testing.T) { // Create a new AVL tree and populate it with some key-value pairs. tree := avl.NewTree() tree.Set("a", 1) @@ -90,7 +123,7 @@ func TestPage_Selector(t *testing.T) { tree.Set("e", 5) // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { @@ -106,7 +139,7 @@ func TestPage_Selector(t *testing.T) { for _, tt := range tests { page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - ui := page.Selector() + ui := page.Picker() uassert.Equal(t, tt.expected, ui) } } @@ -119,7 +152,7 @@ func TestPager_UI_WithManyPages(t *testing.T) { } // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases for a large number of pages. tests := []struct { @@ -145,7 +178,7 @@ func TestPager_UI_WithManyPages(t *testing.T) { for _, tt := range tests { page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - ui := page.Selector() + ui := page.Picker() uassert.Equal(t, tt.expected, ui) } } @@ -160,7 +193,7 @@ func TestPager_ParseQuery(t *testing.T) { tree.Set("e", 5) // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { diff --git a/examples/gno.land/p/demo/avl/pager/z_filetest.gno b/examples/gno.land/p/demo/avl/pager/z_filetest.gno index 17029f57861..6342888d6b4 100644 --- a/examples/gno.land/p/demo/avl/pager/z_filetest.gno +++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno @@ -16,7 +16,7 @@ func main() { } // Create a new pager. - pager := pager.NewPager(tree, 7) + pager := pager.NewPager(tree, 7, false) for pn := -1; pn < 8; pn++ { page := pager.GetPage(pn) @@ -25,7 +25,7 @@ func main() { for idx, item := range page.Items { println(ufmt.Sprintf("- idx=%d key=%s value=%d", idx, item.Key, item.Value)) } - println(page.Selector()) + println(page.Picker()) println() } } diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno index 22d7f97eabd..94f7567cbf4 100644 --- a/examples/gno.land/r/demo/userbook/render.gno +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -2,16 +2,19 @@ package userbook import ( - "sort" "strconv" + "gno.land/r/demo/users" + "gno.land/p/demo/avl/pager" "gno.land/p/demo/ufmt" "gno.land/p/moul/txlink" ) +const usersLink = "/r/demo/users" + func Render(path string) string { - p := pager.NewPager(signupsTree, 2) + p := pager.NewPager(signupsTree, 20, true) page := p.MustGetPageByPath(path) out := "# Welcome to UserBook!\n\n" @@ -19,33 +22,19 @@ func Render(path string) string { out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) out += "---\n\n" - var sorted sortedSignups for _, item := range page.Items { - sorted = append(sorted, item.Value.(*Signup)) - } + signup := item.Value.(*Signup) + user := signup.address.String() - sort.Sort(sorted) + if data := users.GetUserByAddress(signup.address); data != nil { + user = ufmt.Sprintf("[%s](%s:%s)", data.Name, usersLink, data.Name) + } - for _, item := range sorted { - out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05")) + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", signup.ordinal, user, signup.timestamp.Format("January 2 2006, 03:04:04 PM")) } out += "---\n\n" out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" - out += page.Selector() // Repeat selector for ease of navigation + out += page.Picker() return out } - -type sortedSignups []*Signup - -func (s sortedSignups) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s sortedSignups) Len() int { - return len(s) -} - -func (s sortedSignups) Less(i, j int) bool { - return s[i].timestamp.Before(s[j].timestamp) -} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c958dc9e5b0..03027f064b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -6,6 +6,7 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) @@ -15,7 +16,11 @@ type Signup struct { timestamp time.Time } -var signupsTree = avl.NewTree() +var ( + signupsTree = avl.NewTree() + tracker = avl.NewTree() + idCounter seqid.ID +) const signUpEvent = "SignUp" @@ -28,19 +33,22 @@ func SignUp() string { caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := signupsTree.Get(caller.String()); exists { - panic(caller + " is already signed up!") + if _, exists := tracker.Get(caller.String()); exists { + panic(caller.String() + " is already signed up!") } now := time.Now() + // Sign up the user - signupsTree.Set(caller.String(), &Signup{ - std.PrevRealm().Addr(), + signupsTree.Set(idCounter.Next().String(), &Signup{ + caller, signupsTree.Size(), now, }) - std.Emit(signUpEvent, "SignedUpAccount", caller.String()) + tracker.Set(caller.String(), struct{}{}) + + std.Emit(signUpEvent, "account", caller.String()) return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 1f08c9ae08c..8547a6e60e0 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -328,14 +328,14 @@ func Render(fullPath string) string { func renderHome(path string) string { doc := "" - page := pager.NewPager(&name2User, 50).MustGetPageByPath(path) + page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) for _, item := range page.Items { user := item.Value.(*users.User) doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" } doc += "\n" - doc += page.Selector() + doc += page.Picker() return doc } diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager.gno b/examples/gno.land/r/docs/avl_pager/avl_pager.gno index 75807b71981..af8a6a10b48 100644 --- a/examples/gno.land/r/docs/avl_pager/avl_pager.gno +++ b/examples/gno.land/r/docs/avl_pager/avl_pager.gno @@ -22,19 +22,19 @@ func init() { // Render paginated content based on the given URL path. // URL format: `...?page=&size=` (default is page 1 and size 10). func Render(path string) string { - p := pager.NewPager(tree, 10) // Default page size is 10 + p := pager.NewPager(tree, 10, false) // Default page size is 10 page := p.MustGetPageByPath(path) // Header and pagination info result := "# Paginated Items\n" result += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" - result += page.Selector() + "\n\n" + result += page.Picker() + "\n\n" // Display items on the current page for _, item := range page.Items { result += "- " + item.Key + ": " + item.Value.(string) + "\n" } - result += "\n" + page.Selector() // Repeat selector for ease of navigation + result += "\n" + page.Picker() // Repeat page picker for ease of navigation return result } diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index b4d51d03362..0721c7d6e72 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -38,7 +38,7 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - page := pager.NewPager(e.itemsSorted, pageSize).MustGetPageByPath(path) + page := pager.NewPager(e.itemsSorted, pageSize, false).MustGetPageByPath(path) for i := len(page.Items) - 1; i >= 0; i-- { item := page.Items[i] @@ -52,7 +52,7 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - out += page.Selector() + out += page.Picker() return out } From e35fc9a1c6f5e561660a29988bfc3eefa9280c4f Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:32:23 +0100 Subject: [PATCH 310/345] docs: add `std.ChainDomain()` reference (#3301) ## Description Adds the reference docs on `std.ChainDomain()`. Co-authored-by: Antoine Eddi <5222525+aeddi@users.noreply.github.com> --- docs/reference/stdlibs/std/chain.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 0e5ead338c5..b1791e65608 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -28,6 +28,18 @@ std.AssertOriginCall() ``` --- +## ChainDomain +```go +func ChainDomain() string +``` +Returns the chain domain. Currently only `gno.land` is supported. + +#### Usage +```go +domain := std.ChainDomain() // gno.land +``` +--- + ## Emit ```go func Emit(typ string, attrs ...string) From e46f457e70ac2881a8d2c0e3fe625ca9ecdda183 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:51:01 +0100 Subject: [PATCH 311/345] chore: remove PR template (#3300) See https://github.com/gnolang/gno/issues/3238#issuecomment-2526138055 Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/pull_request_template.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 12e07a9cde6..00000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ - - -
Contributors' checklist... - -- [ ] Added new tests, or not needed, or not feasible -- [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory -- [ ] Updated the official documentation or not needed -- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description -- [ ] Added references to related issues and PRs -- [ ] Provided any useful hints for running manual tests -
From 666c54af3d4a7cf354546028de89ec99fe1ce984 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Sun, 8 Dec 2024 19:20:00 +0100 Subject: [PATCH 312/345] chore: import gnolang/overflow into gno/tm2/pkg/overflow (#3302) We remove the dependency on gnolang/overflow, a clone of https://github.com/JohnCGriffin/overflow which is now unmaintained, and import it into gno/tm2/pkg/overflow. We can now have full control on it, fix it and improve it. This PR just changes the import path, no content change is done yet. --------- Co-authored-by: Morgan Bazalgette --- contribs/gnodev/go.mod | 1 - contribs/gnodev/go.sum | 2 - contribs/gnofaucet/go.mod | 1 - contribs/gnofaucet/go.sum | 2 - contribs/gnogenesis/go.mod | 1 - contribs/gnogenesis/go.sum | 2 - contribs/gnohealth/go.mod | 1 - contribs/gnohealth/go.sum | 2 - contribs/gnokeykc/go.mod | 1 - contribs/gnokeykc/go.sum | 2 - contribs/gnomigrate/go.mod | 1 - contribs/gnomigrate/go.sum | 2 - gnovm/pkg/gnolang/machine.go | 3 +- go.mod | 1 - go.sum | 2 - misc/autocounterd/go.mod | 1 - misc/autocounterd/go.sum | 2 - misc/loop/go.mod | 1 - misc/loop/go.sum | 4 - tm2/pkg/overflow/README.md | 66 +++++ tm2/pkg/overflow/overflow.go | 131 ++++++++++ tm2/pkg/overflow/overflow_impl.go | 360 ++++++++++++++++++++++++++ tm2/pkg/overflow/overflow_template.sh | 112 ++++++++ tm2/pkg/overflow/overflow_test.go | 115 ++++++++ tm2/pkg/std/coin.go | 2 +- tm2/pkg/store/gas/store.go | 2 +- tm2/pkg/store/types/gas.go | 2 +- tm2/pkg/store/types/gas_test.go | 2 +- 28 files changed, 789 insertions(+), 35 deletions(-) create mode 100644 tm2/pkg/overflow/README.md create mode 100644 tm2/pkg/overflow/overflow.go create mode 100644 tm2/pkg/overflow/overflow_impl.go create mode 100755 tm2/pkg/overflow/overflow_template.sh create mode 100644 tm2/pkg/overflow/overflow_test.go diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index a315d88591c..2053a61db6c 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -50,7 +50,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e38c3621483..f9250d34462 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -101,8 +101,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index c5bb1ad0d81..eab9fc90c50 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -20,7 +20,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index f4bdc65d7ec..aabe858e893 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -49,8 +49,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gnolang/faucet v0.3.2 h1:3QBrdmnQszRaAZbxgO5xDDm3czNa0L/RFmhnCkbxy5I= github.com/gnolang/faucet v0.3.2/go.mod h1:/wbw9h4ooMzzyNBuM0X+ol7CiPH2OFjAFF3bYAXqA7U= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index b777cc6e5eb..f1b316c2bee 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -18,7 +18,6 @@ require ( github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index 3c6127ac216..7ba3aede534 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index e6d9f119c7b..4f5862a0d2e 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -12,7 +12,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index 116cfbff021..dd287d9ca84 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -45,8 +45,6 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 0c794afd54c..479daed22f6 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -21,7 +21,6 @@ require ( github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 50eb5add218..cacf6788d45 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -54,8 +54,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index a81c2de4ba0..cd31adc4f6f 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -17,7 +17,6 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index 3c6127ac216..7ba3aede534 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index b48c0742e6f..a497648dbc8 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -11,10 +11,9 @@ import ( "strings" "sync" - "github.com/gnolang/overflow" - "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/gnolang/gno/tm2/pkg/store" ) diff --git a/go.mod b/go.mod index f73ba1926e6..f389e60b988 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fortytw2/leaktest v1.3.0 - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/google/gofuzz v1.2.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum index 78d60eeea90..b987535607e 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/misc/autocounterd/go.mod b/misc/autocounterd/go.mod index 5de1d3c2974..30a6f23b458 100644 --- a/misc/autocounterd/go.mod +++ b/misc/autocounterd/go.mod @@ -14,7 +14,6 @@ require ( github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index b34cbde0c00..5d624ca18cb 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/misc/loop/go.mod b/misc/loop/go.mod index a6bbdad3c82..70e9d21734b 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -29,7 +29,6 @@ require ( github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 740cc629a21..8e0feb11e4a 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -68,8 +68,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/gnolang/tx-archive v0.4.0 h1:+1Rgo0U0HjLQLq/xqeGdJwtAzo9xWj09t1oZLvrL3bU= github.com/gnolang/tx-archive v0.4.0/go.mod h1:seKHGnvxUnDgH/mSsCEdwG0dHY/FrpbUm6Hd0+KMd9w= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -258,8 +256,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tm2/pkg/overflow/README.md b/tm2/pkg/overflow/README.md new file mode 100644 index 00000000000..55a9ba4c327 --- /dev/null +++ b/tm2/pkg/overflow/README.md @@ -0,0 +1,66 @@ +# overflow + +Check for int/int8/int16/int64/int32 integer overflow in Golang arithmetic. + +Forked from https://github.com/JohnCGriffin/overflow + +### Install +``` +go get github.com/johncgriffin/overflow +``` +Note that because Go has no template types, the majority of repetitive code is +generated by overflow_template.sh. If you have to change an +algorithm, change it there and regenerate the Go code via: +``` +go generate +``` +### Synopsis + +``` +package main + +import "fmt" +import "math" +import "github.com/JohnCGriffin/overflow" + +func main() { + + addend := math.MaxInt64 - 5 + + for i := 0; i < 10; i++ { + sum, ok := overflow.Add(addend, i) + fmt.Printf("%v+%v -> (%v,%v)\n", + addend, i, sum, ok) + } + +} +``` +yields the output +``` +9223372036854775802+0 -> (9223372036854775802,true) +9223372036854775802+1 -> (9223372036854775803,true) +9223372036854775802+2 -> (9223372036854775804,true) +9223372036854775802+3 -> (9223372036854775805,true) +9223372036854775802+4 -> (9223372036854775806,true) +9223372036854775802+5 -> (9223372036854775807,true) +9223372036854775802+6 -> (0,false) +9223372036854775802+7 -> (0,false) +9223372036854775802+8 -> (0,false) +9223372036854775802+9 -> (0,false) +``` + +For int, int64, and int32 types, provide Add, Add32, Add64, Sub, Sub32, Sub64, etc. +Unsigned types not covered at the moment, but such additions are welcome. + +### Stay calm and panic + +There's a good case to be made that a panic is an unidiomatic but proper response. Iff you +believe that there's no valid way to continue your program after math goes wayward, you can +use the easier Addp, Mulp, Subp, and Divp versions which return the normal result or panic. + + + + + + + diff --git a/tm2/pkg/overflow/overflow.go b/tm2/pkg/overflow/overflow.go new file mode 100644 index 00000000000..b476ea5776e --- /dev/null +++ b/tm2/pkg/overflow/overflow.go @@ -0,0 +1,131 @@ +/* +Package overflow offers overflow-checked integer arithmetic operations +for int, int32, and int64. Each of the operations returns a +result,bool combination. This was prompted by the need to know when +to flow into higher precision types from the math.big library. + +For instance, assuing a 64 bit machine: + +10 + 20 -> 30 +int(math.MaxInt64) + 1 -> -9223372036854775808 + +whereas + +overflow.Add(10,20) -> (30, true) +overflow.Add(math.MaxInt64,1) -> (0, false) + +Add, Sub, Mul, Div are for int. Add64, Add32, etc. are specifically sized. + +If anybody wishes an unsigned version, submit a pull request for code +and new tests. +*/ +package overflow + +//go:generate ./overflow_template.sh + +import "math" + +func _is64Bit() bool { + maxU32 := uint(math.MaxUint32) + return ((maxU32 << 1) >> 1) == maxU32 +} + +/********** PARTIAL TEST COVERAGE FROM HERE DOWN ************* + +The only way that I could see to do this is a combination of +my normal 64 bit system and a GopherJS running on Node. My +understanding is that its ints are 32 bit. + +So, FEEL FREE to carefully review the code visually. + +*************************************************************/ + +// Unspecified size, i.e. normal signed int + +// Add sums two ints, returning the result and a boolean status. +func Add(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Add64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Add32(int32(a), int32(b)) + return int(r32), ok +} + +// Sub returns the difference of two ints and a boolean status. +func Sub(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Sub64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Sub32(int32(a), int32(b)) + return int(r32), ok +} + +// Mul returns the product of two ints and a boolean status. +func Mul(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Mul64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Mul32(int32(a), int32(b)) + return int(r32), ok +} + +// Div returns the quotient of two ints and a boolean status +func Div(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Div64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Div32(int32(a), int32(b)) + return int(r32), ok +} + +// Quotient returns the quotient, remainder and status of two ints +func Quotient(a, b int) (int, int, bool) { + if _is64Bit() { + q64, r64, ok := Quotient64(int64(a), int64(b)) + return int(q64), int(r64), ok + } + q32, r32, ok := Quotient32(int32(a), int32(b)) + return int(q32), int(r32), ok +} + +/************* Panic versions for int ****************/ + +// Addp returns the sum of two ints, panicking on overflow +func Addp(a, b int) int { + r, ok := Add(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Subp returns the difference of two ints, panicking on overflow. +func Subp(a, b int) int { + r, ok := Sub(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mulp returns the product of two ints, panicking on overflow. +func Mulp(a, b int) int { + r, ok := Mul(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Divp returns the quotient of two ints, panicking on overflow. +func Divp(a, b int) int { + r, ok := Div(a, b) + if !ok { + panic("division failure") + } + return r +} diff --git a/tm2/pkg/overflow/overflow_impl.go b/tm2/pkg/overflow/overflow_impl.go new file mode 100644 index 00000000000..a9a90c43835 --- /dev/null +++ b/tm2/pkg/overflow/overflow_impl.go @@ -0,0 +1,360 @@ +package overflow + +// This is generated code, created by overflow_template.sh executed +// by "go generate" + +// Add8 performs + operation on two int8 operands +// returning a result and status +func Add8(a, b int8) (int8, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add8p is the unchecked panicing version of Add8 +func Add8p(a, b int8) int8 { + r, ok := Add8(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub8 performs - operation on two int8 operands +// returning a result and status +func Sub8(a, b int8) (int8, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub8p is the unchecked panicing version of Sub8 +func Sub8p(a, b int8) int8 { + r, ok := Sub8(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul8 performs * operation on two int8 operands +// returning a result and status +func Mul8(a, b int8) (int8, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul8p is the unchecked panicing version of Mul8 +func Mul8p(a, b int8) int8 { + r, ok := Mul8(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div8 performs / operation on two int8 operands +// returning a result and status +func Div8(a, b int8) (int8, bool) { + q, _, ok := Quotient8(a, b) + return q, ok +} + +// Div8p is the unchecked panicing version of Div8 +func Div8p(a, b int8) int8 { + r, ok := Div8(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient8 performs + operation on two int8 operands +// returning a quotient, a remainder and status +func Quotient8(a, b int8) (int8, int8, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add16 performs + operation on two int16 operands +// returning a result and status +func Add16(a, b int16) (int16, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add16p is the unchecked panicing version of Add16 +func Add16p(a, b int16) int16 { + r, ok := Add16(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub16 performs - operation on two int16 operands +// returning a result and status +func Sub16(a, b int16) (int16, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub16p is the unchecked panicing version of Sub16 +func Sub16p(a, b int16) int16 { + r, ok := Sub16(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul16 performs * operation on two int16 operands +// returning a result and status +func Mul16(a, b int16) (int16, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul16p is the unchecked panicing version of Mul16 +func Mul16p(a, b int16) int16 { + r, ok := Mul16(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div16 performs / operation on two int16 operands +// returning a result and status +func Div16(a, b int16) (int16, bool) { + q, _, ok := Quotient16(a, b) + return q, ok +} + +// Div16p is the unchecked panicing version of Div16 +func Div16p(a, b int16) int16 { + r, ok := Div16(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient16 performs + operation on two int16 operands +// returning a quotient, a remainder and status +func Quotient16(a, b int16) (int16, int16, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add32 performs + operation on two int32 operands +// returning a result and status +func Add32(a, b int32) (int32, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add32p is the unchecked panicing version of Add32 +func Add32p(a, b int32) int32 { + r, ok := Add32(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub32 performs - operation on two int32 operands +// returning a result and status +func Sub32(a, b int32) (int32, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub32p is the unchecked panicing version of Sub32 +func Sub32p(a, b int32) int32 { + r, ok := Sub32(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul32 performs * operation on two int32 operands +// returning a result and status +func Mul32(a, b int32) (int32, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul32p is the unchecked panicing version of Mul32 +func Mul32p(a, b int32) int32 { + r, ok := Mul32(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div32 performs / operation on two int32 operands +// returning a result and status +func Div32(a, b int32) (int32, bool) { + q, _, ok := Quotient32(a, b) + return q, ok +} + +// Div32p is the unchecked panicing version of Div32 +func Div32p(a, b int32) int32 { + r, ok := Div32(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient32 performs + operation on two int32 operands +// returning a quotient, a remainder and status +func Quotient32(a, b int32) (int32, int32, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add64 performs + operation on two int64 operands +// returning a result and status +func Add64(a, b int64) (int64, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add64p is the unchecked panicing version of Add64 +func Add64p(a, b int64) int64 { + r, ok := Add64(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub64 performs - operation on two int64 operands +// returning a result and status +func Sub64(a, b int64) (int64, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub64p is the unchecked panicing version of Sub64 +func Sub64p(a, b int64) int64 { + r, ok := Sub64(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul64 performs * operation on two int64 operands +// returning a result and status +func Mul64(a, b int64) (int64, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul64p is the unchecked panicing version of Mul64 +func Mul64p(a, b int64) int64 { + r, ok := Mul64(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div64 performs / operation on two int64 operands +// returning a result and status +func Div64(a, b int64) (int64, bool) { + q, _, ok := Quotient64(a, b) + return q, ok +} + +// Div64p is the unchecked panicing version of Div64 +func Div64p(a, b int64) int64 { + r, ok := Div64(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient64 performs + operation on two int64 operands +// returning a quotient, a remainder and status +func Quotient64(a, b int64) (int64, int64, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} diff --git a/tm2/pkg/overflow/overflow_template.sh b/tm2/pkg/overflow/overflow_template.sh new file mode 100755 index 00000000000..a2a85f2c581 --- /dev/null +++ b/tm2/pkg/overflow/overflow_template.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +exec > overflow_impl.go + +echo "package overflow + +// This is generated code, created by overflow_template.sh executed +// by \"go generate\" + +" + + +for SIZE in 8 16 32 64 +do +echo " + +// Add${SIZE} performs + operation on two int${SIZE} operands +// returning a result and status +func Add${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add${SIZE}p is the unchecked panicing version of Add${SIZE} +func Add${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Add${SIZE}(a, b) + if !ok { + panic(\"addition overflow\") + } + return r +} + + +// Sub${SIZE} performs - operation on two int${SIZE} operands +// returning a result and status +func Sub${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub${SIZE}p is the unchecked panicing version of Sub${SIZE} +func Sub${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Sub${SIZE}(a, b) + if !ok { + panic(\"subtraction overflow\") + } + return r +} + + +// Mul${SIZE} performs * operation on two int${SIZE} operands +// returning a result and status +func Mul${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul${SIZE}p is the unchecked panicing version of Mul${SIZE} +func Mul${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Mul${SIZE}(a, b) + if !ok { + panic(\"multiplication overflow\") + } + return r +} + + + +// Div${SIZE} performs / operation on two int${SIZE} operands +// returning a result and status +func Div${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + q, _, ok := Quotient${SIZE}(a, b) + return q, ok +} + +// Div${SIZE}p is the unchecked panicing version of Div${SIZE} +func Div${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Div${SIZE}(a, b) + if !ok { + panic(\"division failure\") + } + return r +} + +// Quotient${SIZE} performs + operation on two int${SIZE} operands +// returning a quotient, a remainder and status +func Quotient${SIZE}(a, b int${SIZE}) (int${SIZE}, int${SIZE}, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} +" +done + +go run -modfile ../../../misc/devdeps/go.mod mvdan.cc/gofumpt -w overflow_impl.go diff --git a/tm2/pkg/overflow/overflow_test.go b/tm2/pkg/overflow/overflow_test.go new file mode 100644 index 00000000000..2b2d345b55d --- /dev/null +++ b/tm2/pkg/overflow/overflow_test.go @@ -0,0 +1,115 @@ +package overflow + +import ( + "fmt" + "math" + "testing" +) + +// sample all possibilities of 8 bit numbers +// by checking against 64 bit numbers + +func TestAlgorithms(t *testing.T) { + errors := 0 + + for a64 := int64(math.MinInt8); a64 <= int64(math.MaxInt8); a64++ { + for b64 := int64(math.MinInt8); b64 <= int64(math.MaxInt8) && errors < 10; b64++ { + a8 := int8(a64) + b8 := int8(b64) + + if int64(a8) != a64 || int64(b8) != b64 { + t.Fatal("LOGIC FAILURE IN TEST") + } + + // ADDITION + { + r64 := a64 + b64 + + // now the verification + result, ok := Add8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v + %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // SUBTRACTION + { + r64 := a64 - b64 + + // now the verification + result, ok := Sub8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v - %v = %v instead of %v\n", + a8, b8, result, r64) + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // MULTIPLICATION + { + r64 := a64 * b64 + + // now the verification + result, ok := Mul8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v * %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // DIVISION + if b8 != 0 { + r64 := a64 / b64 + + // now the verification + result, _, ok := Quotient8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v / %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && result != 0 && int64(result) == r64 { + t.Fail() + errors++ + } + } + } + } +} + +func TestQuotient(t *testing.T) { + q, r, ok := Quotient(100, 3) + if r != 1 || q != 33 || !ok { + t.Errorf("expected 100/3 => 33, r=1") + } + if _, _, ok = Quotient(1, 0); ok { + t.Error("unexpected lack of failure") + } +} + +//func TestAdditionInt(t *testing.T) { +// fmt.Printf("\nminint8 = %v\n", math.MinInt8) +// fmt.Printf("maxint8 = %v\n\n", math.MaxInt8) +// fmt.Printf("maxint32 = %v\n", math.MaxInt32) +// fmt.Printf("minint32 = %v\n\n", math.MinInt32) +// fmt.Printf("maxint64 = %v\n", math.MaxInt64) +// fmt.Printf("minint64 = %v\n\n", math.MinInt64) +//} + +func Test64(t *testing.T) { + fmt.Println("64bit:", _is64Bit()) +} diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index 6457b193a6b..fba20a5ba78 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" ) // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/store/gas/store.go b/tm2/pkg/store/gas/store.go index db5ea7a79b0..81e898a90d8 100644 --- a/tm2/pkg/store/gas/store.go +++ b/tm2/pkg/store/gas/store.go @@ -1,9 +1,9 @@ package gas import ( + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/gnolang/gno/tm2/pkg/store/types" "github.com/gnolang/gno/tm2/pkg/store/utils" - "github.com/gnolang/overflow" ) var _ types.Store = &Store{} diff --git a/tm2/pkg/store/types/gas.go b/tm2/pkg/store/types/gas.go index fd631dd3259..9d1f3d70c28 100644 --- a/tm2/pkg/store/types/gas.go +++ b/tm2/pkg/store/types/gas.go @@ -3,7 +3,7 @@ package types import ( "math" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" ) // Gas consumption descriptors. diff --git a/tm2/pkg/store/types/gas_test.go b/tm2/pkg/store/types/gas_test.go index 410ba0b7e92..115d347bd5e 100644 --- a/tm2/pkg/store/types/gas_test.go +++ b/tm2/pkg/store/types/gas_test.go @@ -4,7 +4,7 @@ import ( "math" "testing" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/stretchr/testify/require" ) From 5f16b8c703867c3311c9023b60105b010a9e97be Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Dec 2024 10:14:40 +0100 Subject: [PATCH 313/345] fix(gnovm): use strconv.UnquoteChar to parse rune literals (#3296) This fixes a bug, as shown in rune3.gno, whereby rune literals which would not be parsed correctly by `strconv.Unquote` are now parsed correctly. Previously, the test would print out 65533, for the unicode invalid code point. --- gnovm/pkg/gnolang/nodes.go | 1 + gnovm/pkg/gnolang/op_eval.go | 10 ++++------ gnovm/tests/files/rune3.gno | 10 ++++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 gnovm/tests/files/rune3.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 8d3d6d8a2cc..0496d37ed72 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -2153,6 +2153,7 @@ type ValuePather interface { // Utility func (x *BasicLitExpr) GetString() string { + // Matches string literal parsing in go/constant.MakeFromLiteral. str, err := strconv.Unquote(x.Value) if err != nil { panic("error in parsing string literal: " + err.Error()) diff --git a/gnovm/pkg/gnolang/op_eval.go b/gnovm/pkg/gnolang/op_eval.go index 1beba1d6e3f..2aa13b21753 100644 --- a/gnovm/pkg/gnolang/op_eval.go +++ b/gnovm/pkg/gnolang/op_eval.go @@ -204,16 +204,14 @@ func (m *Machine) doOpEval() { // and github.com/golang/go/issues/19921 panic("imaginaries are not supported") case CHAR: - cstr, err := strconv.Unquote(x.Value) + // Matching character literal parsing in go/constant.MakeFromLiteral. + val := x.Value + rne, _, _, err := strconv.UnquoteChar(val[1:len(val)-1], '\'') if err != nil { panic("error in parsing character literal: " + err.Error()) } - runes := []rune(cstr) - if len(runes) != 1 { - panic(fmt.Sprintf("error in parsing character literal: 1 rune expected, but got %v (%s)", len(runes), cstr)) - } tv := TypedValue{T: UntypedRuneType} - tv.SetInt32(runes[0]) + tv.SetInt32(rne) m.PushValue(tv) case STRING: m.PushValue(TypedValue{ diff --git a/gnovm/tests/files/rune3.gno b/gnovm/tests/files/rune3.gno new file mode 100644 index 00000000000..e848565e3a4 --- /dev/null +++ b/gnovm/tests/files/rune3.gno @@ -0,0 +1,10 @@ +package main + +const overflow = '\xff' + +func main() { + println(overflow) +} + +// Output: +// 255 From 1fba5cfa840d3499d9ce22507a7d2ada19abbdd4 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Dec 2024 11:13:38 +0100 Subject: [PATCH 314/345] feat(cmd/gno): perform type checking when calling linter (#1730) Depends on (in order): 1. #1700 2. #1702 This PR uses the type checker added in #1702 to perform Gno type checking when calling `gno lint`. Additionally, it adds validation of gno.mod indirectly (the parsed gno mod is used to determine if a package is a draft, and if so skip type checking). Because `gno lint` uses the TestStore, the resulting `MemPackage`s may contain redefinitions, for overwriting standard libraries like `AssertOriginCall`. I changed the type checker to filter out the redefinitions before they reach the Go type checker. Further improvements, which can be done after this: - Add shims for gonative special libraries (`fmt`, `os`...) - This will allow us to fully type check also tests and filetests - Make the type checking on-chain (#1702) also typecheck tests - as a consequence of the above. --- gnovm/cmd/gno/lint.go | 205 ++++++++++++------ gnovm/cmd/gno/lint_test.go | 39 ++-- gnovm/cmd/gno/run_test.go | 19 +- .../gno/testdata/{gno_fmt => fmt}/empty.txtar | 0 .../{gno_fmt => fmt}/import_cleaning.txtar | 0 .../testdata/{gno_fmt => fmt}/include.txtar | 0 .../{gno_fmt => fmt}/multi_import.txtar | 0 .../{gno_fmt => fmt}/noimport_format.txtar | 0 .../{gno_fmt => fmt}/parse_error.txtar | 0 .../{gno_fmt => fmt}/shadow_import.txtar | 0 .../gno/testdata/gno_lint/file_error_txtar | 20 -- .../{gno_lint => lint}/bad_import.txtar | 8 +- .../{gno_lint => lint}/file_error.txtar | 5 +- .../{gno_lint => lint}/no_error.txtar | 9 +- .../{gno_lint => lint}/no_gnomod.txtar | 6 +- .../{gno_lint => lint}/not_declared.txtar | 12 +- .../{gno_test => test}/dir_not_exist.txtar | 0 .../{gno_test => test}/empty_dir.txtar | 0 .../{gno_test => test}/empty_gno1.txtar | 0 .../{gno_test => test}/empty_gno2.txtar | 0 .../{gno_test => test}/empty_gno3.txtar | 0 .../{gno_test => test}/error_correct.txtar | 0 .../{gno_test => test}/error_incorrect.txtar | 0 .../{gno_test => test}/error_sync.txtar | 0 .../{gno_test => test}/failing_filetest.txtar | 0 .../{gno_test => test}/failing_test.txtar | 0 .../{gno_test => test}/filetest_events.txtar | 0 .../flag_print-runtime-metrics.txtar | 0 .../{gno_test => test}/flag_run.txtar | 0 .../{gno_test => test}/flag_timeout.txtar | 0 .../{gno_test => test}/fmt_write_import.txtar | 0 .../testdata/{gno_test => test}/minim1.txtar | 0 .../testdata/{gno_test => test}/minim2.txtar | 0 .../testdata/{gno_test => test}/minim3.txtar | 0 .../{gno_test => test}/multitest_events.txtar | 0 .../testdata/{gno_test => test}/no_args.txtar | 0 .../{gno_test => test}/output_correct.txtar | 0 .../{gno_test => test}/output_incorrect.txtar | 0 .../{gno_test => test}/output_sync.txtar | 0 .../testdata/{gno_test => test}/panic.txtar | 0 .../pkg_underscore_test.txtar | 0 .../realm_boundmethod.txtar | 0 .../{gno_test => test}/realm_correct.txtar | 0 .../{gno_test => test}/realm_incorrect.txtar | 0 .../{gno_test => test}/realm_sync.txtar | 0 .../testdata/{gno_test => test}/recover.txtar | 0 .../testdata/{gno_test => test}/skip.txtar | 0 .../{gno_test => test}/unknown_package.txtar | 0 .../{gno_test => test}/valid_filetest.txtar | 0 .../{gno_test => test}/valid_test.txtar | 0 .../gobuild_flag_build_error.txtar | 0 .../gobuild_flag_parse_error.txtar | 0 .../invalid_import.txtar | 0 .../no_args.txtar | 0 .../parse_error.txtar | 0 .../valid_empty_dir.txtar | 0 .../valid_gobuild_file.txtar | 0 .../valid_gobuild_flag.txtar | 0 .../valid_output_flag.txtar | 0 .../valid_output_gobuild.txtar | 0 .../valid_transpile_file.txtar | 0 .../valid_transpile_package.txtar | 0 .../valid_transpile_tree.txtar | 0 gnovm/cmd/gno/testdata_test.go | 1 - gnovm/cmd/gno/transpile_test.go | 20 -- gnovm/pkg/gnolang/go2gno.go | 59 ++++- .../integ/typecheck_missing_return/gno.mod | 1 + .../integ/typecheck_missing_return/main.gno | 5 + 68 files changed, 263 insertions(+), 146 deletions(-) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/empty.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/import_cleaning.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/include.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/multi_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/noimport_format.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/shadow_import.txtar (100%) delete mode 100644 gnovm/cmd/gno/testdata/gno_lint/file_error_txtar rename gnovm/cmd/gno/testdata/{gno_lint => lint}/bad_import.txtar (54%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/file_error.txtar (88%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/no_error.txtar (68%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/no_gnomod.txtar (60%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/not_declared.txtar (55%) rename gnovm/cmd/gno/testdata/{gno_test => test}/dir_not_exist.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_dir.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno1.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno2.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno3.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/failing_filetest.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/failing_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/filetest_events.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_print-runtime-metrics.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_run.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_timeout.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/fmt_write_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim1.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim2.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim3.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/multitest_events.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/no_args.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/panic.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/pkg_underscore_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_boundmethod.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/recover.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/skip.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/unknown_package.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/valid_filetest.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/valid_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/gobuild_flag_build_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/gobuild_flag_parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/invalid_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/no_args.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_empty_dir.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_gobuild_file.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_gobuild_flag.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_output_flag.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_output_gobuild.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_file.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_package.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_tree.txtar (100%) create mode 100644 gnovm/tests/integ/typecheck_missing_return/gno.mod create mode 100644 gnovm/tests/integ/typecheck_missing_return/main.gno diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 6d5399ca932..a3e7f5310e1 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -6,17 +6,19 @@ import ( "flag" "fmt" "go/scanner" + "go/types" "io" "os" "path/filepath" "regexp" "strings" + "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/gnovm/pkg/test" "github.com/gnolang/gno/tm2/pkg/commands" - osm "github.com/gnolang/gno/tm2/pkg/os" "go.uber.org/multierr" ) @@ -50,6 +52,31 @@ func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar(&c.rootDir, "root-dir", rootdir, "clone location of github.com/gnolang/gno (gno tries to guess it)") } +type lintCode int + +const ( + lintUnknown lintCode = iota + lintGnoMod + lintGnoError + lintParserError + lintTypeCheckError + + // TODO: add new linter codes here. +) + +type lintIssue struct { + Code lintCode + Msg string + Confidence float64 // 1 is 100% + Location string // file:line, or equivalent + // TODO: consider writing fix suggestions +} + +func (i lintIssue) String() string { + // TODO: consider crafting a doc URL based on Code. + return fmt.Sprintf("%s: %s (code=%d)", i.Location, i.Msg, i.Code) +} + func execLint(cfg *lintCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp @@ -72,37 +99,55 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { for _, pkgPath := range pkgPaths { if verbose { - fmt.Fprintf(io.Err(), "Linting %q...\n", pkgPath) + io.ErrPrintln(pkgPath) + } + + info, err := os.Stat(pkgPath) + if err == nil && !info.IsDir() { + pkgPath = filepath.Dir(pkgPath) } // Check if 'gno.mod' exists - gnoModPath := filepath.Join(pkgPath, "gno.mod") - if !osm.FileExists(gnoModPath) { - hasError = true + gmFile, err := gnomod.ParseAt(pkgPath) + if err != nil { issue := lintIssue{ - Code: lintNoGnoMod, + Code: lintGnoMod, Confidence: 1, Location: pkgPath, - Msg: "missing 'gno.mod' file", + Msg: err.Error(), } - fmt.Fprint(io.Err(), issue.String()+"\n") + io.ErrPrintln(issue) + hasError = true } - // Handle runtime errors - hasError = catchRuntimeError(pkgPath, io.Err(), func() { - stdout, stdin, stderr := io.Out(), io.In(), io.Err() - _, testStore := test.Store( - rootDir, false, - stdin, stdout, stderr, - ) - - targetPath := pkgPath - info, err := os.Stat(pkgPath) - if err == nil && !info.IsDir() { - targetPath = filepath.Dir(pkgPath) + stdout, stdin, stderr := io.Out(), io.In(), io.Err() + _, testStore := test.Store( + rootDir, false, + stdin, stdout, stderr, + ) + + memPkg, err := gno.ReadMemPackage(pkgPath, pkgPath) + if err != nil { + io.ErrPrintln(issueFromError(pkgPath, err).String()) + hasError = true + continue + } + + // Run type checking + if gmFile == nil || !gmFile.Draft { + foundErr, err := lintTypeCheck(io, memPkg, testStore) + if err != nil { + io.ErrPrintln(err) + hasError = true + } else if foundErr { + hasError = true } + } else if verbose { + io.ErrPrintfln("%s: module is draft, skipping type check", pkgPath) + } - memPkg := gno.MustReadMemPackage(targetPath, targetPath) + // Handle runtime errors + hasRuntimeErr := catchRuntimeError(pkgPath, io.Err(), func() { tm := test.Machine(testStore, stdout, memPkg.Path) defer tm.Release() @@ -110,28 +155,13 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { tm.RunMemPackage(memPkg, true) // Check test files - testfiles := &gno.FileSet{} - for _, mfile := range memPkg.Files { - if !strings.HasSuffix(mfile.Name, ".gno") { - continue // Skip non-GNO files - } + testFiles := lintTestFiles(memPkg) - n, _ := gno.ParseFile(mfile.Name, mfile.Body) - if n == nil { - continue // Skip empty files - } - - // XXX: package ending with `_test` is not supported yet - if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { - // Keep only test files - testfiles.AddFiles(n) - } - } - - tm.RunFiles(testfiles.Files...) - }) || hasError - - // TODO: Add more checkers + tm.RunFiles(testFiles.Files...) + }) + if hasRuntimeErr { + hasError = true + } } if hasError { @@ -141,6 +171,66 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { return nil } +func lintTypeCheck(io commands.IO, memPkg *gnovm.MemPackage, testStore gno.Store) (errorsFound bool, err error) { + tcErr := gno.TypeCheckMemPackageTest(memPkg, testStore) + if tcErr == nil { + return false, nil + } + + errs := multierr.Errors(tcErr) + for _, err := range errs { + switch err := err.(type) { + case types.Error: + io.ErrPrintln(lintIssue{ + Code: lintTypeCheckError, + Msg: err.Msg, + Confidence: 1, + Location: err.Fset.Position(err.Pos).String(), + }) + case scanner.ErrorList: + for _, scErr := range err { + io.ErrPrintln(lintIssue{ + Code: lintParserError, + Msg: scErr.Msg, + Confidence: 1, + Location: scErr.Pos.String(), + }) + } + case scanner.Error: + io.ErrPrintln(lintIssue{ + Code: lintParserError, + Msg: err.Msg, + Confidence: 1, + Location: err.Pos.String(), + }) + default: + return false, fmt.Errorf("unexpected error type: %T", err) + } + } + return true, nil +} + +func lintTestFiles(memPkg *gnovm.MemPackage) *gno.FileSet { + testfiles := &gno.FileSet{} + for _, mfile := range memPkg.Files { + if !strings.HasSuffix(mfile.Name, ".gno") { + continue // Skip non-GNO files + } + + n, _ := gno.ParseFile(mfile.Name, mfile.Body) + if n == nil { + continue // Skip empty files + } + + // XXX: package ending with `_test` is not supported yet + if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { + // Keep only test files + testfiles.AddFiles(n) + } + } + return testfiles +} + func guessSourcePath(pkg, source string) string { if info, err := os.Stat(pkg); !os.IsNotExist(err) && !info.IsDir() { pkg = filepath.Dir(pkg) @@ -174,21 +264,21 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha switch verr := r.(type) { case *gno.PreprocessError: err := verr.Unwrap() - fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, err).String()) case error: errors := multierr.Errors(verr) for _, err := range errors { errList, ok := err.(scanner.ErrorList) if ok { for _, errorInList := range errList { - fmt.Fprint(stderr, issueFromError(pkgPath, errorInList).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, errorInList).String()) } } else { - fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, err).String()) } } case string: - fmt.Fprint(stderr, issueFromError(pkgPath, errors.New(verr)).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, errors.New(verr)).String()) default: panic(r) } @@ -198,29 +288,6 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha return } -type lintCode int - -const ( - lintUnknown lintCode = 0 - lintNoGnoMod lintCode = iota - lintGnoError - - // TODO: add new linter codes here. -) - -type lintIssue struct { - Code lintCode - Msg string - Confidence float64 // 1 is 100% - Location string // file:line, or equivalent - // TODO: consider writing fix suggestions -} - -func (i lintIssue) String() string { - // TODO: consider crafting a doc URL based on Code. - return fmt.Sprintf("%s: %s (code=%d).", i.Location, i.Msg, i.Code) -} - func issueFromError(pkgPath string, err error) lintIssue { var issue lintIssue issue.Confidence = 1 diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 031c252bc79..4589fc55f92 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestLintApp(t *testing.T) { tc := []testMainCase{ @@ -9,7 +12,7 @@ func TestLintApp(t *testing.T) { errShouldBe: "flag: help requested", }, { args: []string{"lint", "../../tests/integ/run_main/"}, - stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", + stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, @@ -17,33 +20,43 @@ func TestLintApp(t *testing.T) { errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"}, - stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2).", + stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6", + stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", - errShouldBe: "exit code: 1", - }, { - args: []string{"lint", "../../tests/integ/run_main/"}, - stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", - errShouldBe: "exit code: 1", + args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, + stderrShouldContain: func() string { + lines := []string{ + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + } + return strings.Join(lines, "\n") + "\n" + }(), + errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files }, { args: []string{"lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid + }, { + args: []string{"lint", "../../tests/integ/invalid_gno_file/"}, + stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)", + errShouldBe: "exit code: 1", + }, { + args: []string{"lint", "../../tests/integ/typecheck_missing_return/"}, + stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)", + errShouldBe: "exit code: 1", }, // TODO: 'gno mod' is valid? - // TODO: is gno source valid? // TODO: are dependencies valid? // TODO: is gno source using unsafe/discouraged features? - // TODO: consider making `gno transpile; go lint *gen.go` // TODO: check for imports of native libs from non _test.gno files } testMainCaseRun(t, tc) diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index 74f99f7490c..aa7780c149e 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestRunApp(t *testing.T) { tc := []testMainCase{ @@ -84,9 +87,17 @@ func TestRunApp(t *testing.T) { stdoutShouldContain: "Context worked", }, { - args: []string{"run", "../../tests/integ/several-files-multiple-errors/"}, - stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", - errShouldBe: "exit code: 1", + args: []string{"run", "../../tests/integ/several-files-multiple-errors/"}, + stderrShouldContain: func() string { + lines := []string{ + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + } + return strings.Join(lines, "\n") + "\n" + }(), + errShouldBe: "exit code: 1", }, // TODO: a test file // TODO: args diff --git a/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar b/gnovm/cmd/gno/testdata/fmt/empty.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/empty.txtar rename to gnovm/cmd/gno/testdata/fmt/empty.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar b/gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar rename to gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/include.txtar b/gnovm/cmd/gno/testdata/fmt/include.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/include.txtar rename to gnovm/cmd/gno/testdata/fmt/include.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar b/gnovm/cmd/gno/testdata/fmt/multi_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar rename to gnovm/cmd/gno/testdata/fmt/multi_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar b/gnovm/cmd/gno/testdata/fmt/noimport_format.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar rename to gnovm/cmd/gno/testdata/fmt/noimport_format.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar b/gnovm/cmd/gno/testdata/fmt/parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar rename to gnovm/cmd/gno/testdata/fmt/parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar b/gnovm/cmd/gno/testdata/fmt/shadow_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar rename to gnovm/cmd/gno/testdata/fmt/shadow_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar deleted file mode 100644 index 9482eeb1f4f..00000000000 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar +++ /dev/null @@ -1,20 +0,0 @@ -# gno lint: test file error - -! gno lint ./i_have_error_test.gno - -cmp stdout stdout.golden -cmp stderr stderr.golden - --- i_have_error_test.gno -- -package main - -import "fmt" - -func TestIHaveSomeError() { - i := undefined_variable - fmt.Println("Hello", 42) -} - --- stdout.golden -- --- stderr.golden -- -i_have_error_test.gno:6: name undefined_variable not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar b/gnovm/cmd/gno/testdata/lint/bad_import.txtar similarity index 54% rename from gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar rename to gnovm/cmd/gno/testdata/lint/bad_import.txtar index 52141dff09b..b5edbdd0223 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar +++ b/gnovm/cmd/gno/testdata/lint/bad_import.txtar @@ -11,9 +11,13 @@ package main import "python" func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/test + -- stdout.golden -- -- stderr.golden -- -bad_file.gno:3:8: unknown import path python (code=2). +bad_file.gno:3:8: could not import python (import not found: python) (code=4) +bad_file.gno:3:8: unknown import path python (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar b/gnovm/cmd/gno/testdata/lint/file_error.txtar similarity index 88% rename from gnovm/cmd/gno/testdata/gno_lint/file_error.txtar rename to gnovm/cmd/gno/testdata/lint/file_error.txtar index 5aa3a3282d5..4fa50c6da81 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/file_error.txtar @@ -15,6 +15,9 @@ func TestIHaveSomeError() { fmt.Println("Hello", 42) } +-- gno.mod -- +module gno.land/p/test + -- stdout.golden -- -- stderr.golden -- -i_have_error_test.gno:6:7: name undefined_variable not declared (code=2). +i_have_error_test.gno:6:7: name undefined_variable not declared (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar b/gnovm/cmd/gno/testdata/lint/no_error.txtar similarity index 68% rename from gnovm/cmd/gno/testdata/gno_lint/no_error.txtar rename to gnovm/cmd/gno/testdata/lint/no_error.txtar index 95356b1ba2b..5dd3b164952 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_error.txtar @@ -1,6 +1,6 @@ # testing simple gno lint command with any error -gno lint ./good_file.gno +gno lint ./good_file.gno cmp stdout stdout.golden cmp stdout stderr.golden @@ -8,11 +8,12 @@ cmp stdout stderr.golden -- good_file.gno -- package main -import "fmt" - func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/demo/test + -- stdout.golden -- -- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar similarity index 60% rename from gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar rename to gnovm/cmd/gno/testdata/lint/no_gnomod.txtar index 52daa6f0e9b..b5a046a7095 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar @@ -8,12 +8,10 @@ cmp stderr stderr.golden -- good_file.gno -- package main -import "fmt" - func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } -- stdout.golden -- -- stderr.golden -- -./.: missing 'gno.mod' file (code=1). +./.: parsing gno.mod at ./.: gno.mod file not found in current or any parent directory (code=1) diff --git a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar b/gnovm/cmd/gno/testdata/lint/not_declared.txtar similarity index 55% rename from gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar rename to gnovm/cmd/gno/testdata/lint/not_declared.txtar index b63c5c447e1..ac56b27e0df 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar +++ b/gnovm/cmd/gno/testdata/lint/not_declared.txtar @@ -8,13 +8,15 @@ cmp stderr stderr.golden -- bad_file.gno -- package main -import "fmt" - func main() { - hello.Foo() - fmt.Println("Hello", 42) + hello.Foo() + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/demo/hello + -- stdout.golden -- -- stderr.golden -- -bad_file.gno:6:3: name hello not declared (code=2). +bad_file.gno:4:2: undefined: hello (code=4) +bad_file.gno:4:2: name hello not declared (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar b/gnovm/cmd/gno/testdata/test/dir_not_exist.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar rename to gnovm/cmd/gno/testdata/test/dir_not_exist.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar b/gnovm/cmd/gno/testdata/test/empty_dir.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar rename to gnovm/cmd/gno/testdata/test/empty_dir.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar b/gnovm/cmd/gno/testdata/test/empty_gno1.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno1.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar b/gnovm/cmd/gno/testdata/test/empty_gno2.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno2.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar b/gnovm/cmd/gno/testdata/test/empty_gno3.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno3.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/test/error_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_correct.txtar rename to gnovm/cmd/gno/testdata/test/error_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/test/error_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/error_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar b/gnovm/cmd/gno/testdata/test/error_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_sync.txtar rename to gnovm/cmd/gno/testdata/test/error_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/test/failing_filetest.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar rename to gnovm/cmd/gno/testdata/test/failing_filetest.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_test.txtar b/gnovm/cmd/gno/testdata/test/failing_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/failing_test.txtar rename to gnovm/cmd/gno/testdata/test/failing_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar b/gnovm/cmd/gno/testdata/test/filetest_events.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar rename to gnovm/cmd/gno/testdata/test/filetest_events.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar b/gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar rename to gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_run.txtar b/gnovm/cmd/gno/testdata/test/flag_run.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_run.txtar rename to gnovm/cmd/gno/testdata/test/flag_run.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar b/gnovm/cmd/gno/testdata/test/flag_timeout.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar rename to gnovm/cmd/gno/testdata/test/flag_timeout.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar b/gnovm/cmd/gno/testdata/test/fmt_write_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar rename to gnovm/cmd/gno/testdata/test/fmt_write_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar b/gnovm/cmd/gno/testdata/test/minim1.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim1.txtar rename to gnovm/cmd/gno/testdata/test/minim1.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar b/gnovm/cmd/gno/testdata/test/minim2.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim2.txtar rename to gnovm/cmd/gno/testdata/test/minim2.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar b/gnovm/cmd/gno/testdata/test/minim3.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim3.txtar rename to gnovm/cmd/gno/testdata/test/minim3.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar b/gnovm/cmd/gno/testdata/test/multitest_events.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar rename to gnovm/cmd/gno/testdata/test/multitest_events.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/no_args.txtar b/gnovm/cmd/gno/testdata/test/no_args.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/no_args.txtar rename to gnovm/cmd/gno/testdata/test/no_args.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/test/output_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_correct.txtar rename to gnovm/cmd/gno/testdata/test/output_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/test/output_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/output_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/test/output_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_sync.txtar rename to gnovm/cmd/gno/testdata/test/output_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/panic.txtar b/gnovm/cmd/gno/testdata/test/panic.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/panic.txtar rename to gnovm/cmd/gno/testdata/test/panic.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar b/gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar rename to gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar b/gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar rename to gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/test/realm_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar rename to gnovm/cmd/gno/testdata/test/realm_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/test/realm_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/realm_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/test/realm_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar rename to gnovm/cmd/gno/testdata/test/realm_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/recover.txtar b/gnovm/cmd/gno/testdata/test/recover.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/recover.txtar rename to gnovm/cmd/gno/testdata/test/recover.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/skip.txtar b/gnovm/cmd/gno/testdata/test/skip.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/skip.txtar rename to gnovm/cmd/gno/testdata/test/skip.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar b/gnovm/cmd/gno/testdata/test/unknown_package.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar rename to gnovm/cmd/gno/testdata/test/unknown_package.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar rename to gnovm/cmd/gno/testdata/test/valid_filetest.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar b/gnovm/cmd/gno/testdata/test/valid_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/valid_test.txtar rename to gnovm/cmd/gno/testdata/test/valid_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar b/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar rename to gnovm/cmd/gno/testdata/transpile/invalid_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar b/gnovm/cmd/gno/testdata/transpile/no_args.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar rename to gnovm/cmd/gno/testdata/transpile/no_args.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar rename to gnovm/cmd/gno/testdata/transpile/parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar b/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar diff --git a/gnovm/cmd/gno/testdata_test.go b/gnovm/cmd/gno/testdata_test.go index 15bc8d96e26..6b1bbd1d459 100644 --- a/gnovm/cmd/gno/testdata_test.go +++ b/gnovm/cmd/gno/testdata_test.go @@ -24,7 +24,6 @@ func Test_Scripts(t *testing.T) { } name := dir.Name() - t.Logf("testing: %s", name) t.Run(name, func(t *testing.T) { updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) p := testscript.Params{ diff --git a/gnovm/cmd/gno/transpile_test.go b/gnovm/cmd/gno/transpile_test.go index 827c09e23f1..5a03ddc7657 100644 --- a/gnovm/cmd/gno/transpile_test.go +++ b/gnovm/cmd/gno/transpile_test.go @@ -6,29 +6,9 @@ import ( "strconv" "testing" - "github.com/rogpeppe/go-internal/testscript" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/gnovm/pkg/integration" ) -func Test_ScriptsTranspile(t *testing.T) { - p := testscript.Params{ - Dir: "testdata/gno_transpile", - } - - if coverdir, ok := integration.ResolveCoverageDir(); ok { - err := integration.SetupTestscriptsCoverage(&p, coverdir) - require.NoError(t, err) - } - - err := integration.SetupGno(&p, t.TempDir()) - require.NoError(t, err) - - testscript.Run(t, p) -} - func Test_parseGoBuildErrors(t *testing.T) { t.Parallel() diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 338efa20fcc..82d5c69b08b 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -39,7 +39,9 @@ import ( "go/token" "go/types" "os" + "path" "reflect" + "slices" "strconv" "strings" @@ -499,6 +501,18 @@ type MemPackageGetter interface { // If format is true, the code will be automatically updated with the // formatted source code. func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, format bool) error { + return typeCheckMemPackage(mempkg, getter, false, format) +} + +// TypeCheckMemPackageTest performs the same type checks as [TypeCheckMemPackage], +// but allows re-declarations. +// +// Note: like TypeCheckMemPackage, this function ignores tests and filetests. +func TypeCheckMemPackageTest(mempkg *gnovm.MemPackage, getter MemPackageGetter) error { + return typeCheckMemPackage(mempkg, getter, true, false) +} + +func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, testing, format bool) error { var errs error imp := &gnoImporter{ getter: getter, @@ -508,6 +522,7 @@ func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, form errs = multierr.Append(errs, err) }, }, + allowRedefinitions: testing, } imp.cfg.Importer = imp @@ -529,6 +544,9 @@ type gnoImporter struct { getter MemPackageGetter cache map[string]gnoImporterResult cfg *types.Config + + // allow symbol redefinitions? (test standard libraries) + allowRedefinitions bool } // Unused, but satisfies the Importer interface. @@ -559,22 +577,39 @@ func (g *gnoImporter) ImportFrom(path, _ string, _ types.ImportMode) (*types.Pac } func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*types.Package, error) { + // This map is used to allow for function re-definitions, which are allowed + // in Gno (testing context) but not in Go. + // This map links each function identifier with a closure to remove its + // associated declaration. + var delFunc map[string]func() + if g.allowRedefinitions { + delFunc = make(map[string]func()) + } + fset := token.NewFileSet() files := make([]*ast.File, 0, len(mpkg.Files)) var errs error for _, file := range mpkg.Files { + // Ignore non-gno files. + // TODO: support filetest type checking. (should probably handle as each its + // own separate pkg, which should also be typechecked) if !strings.HasSuffix(file.Name, ".gno") || - endsWith(file.Name, []string{"_test.gno", "_filetest.gno"}) { - continue // skip spurious file. + strings.HasSuffix(file.Name, "_test.gno") || + strings.HasSuffix(file.Name, "_filetest.gno") { + continue } const parseOpts = parser.ParseComments | parser.DeclarationErrors | parser.SkipObjectResolution - f, err := parser.ParseFile(fset, file.Name, file.Body, parseOpts) + f, err := parser.ParseFile(fset, path.Join(mpkg.Path, file.Name), file.Body, parseOpts) if err != nil { errs = multierr.Append(errs, err) continue } + if delFunc != nil { + deleteOldIdents(delFunc, f) + } + // enforce formatting if fmt { var buf bytes.Buffer @@ -595,6 +630,24 @@ func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*t return g.cfg.Check(mpkg.Path, fset, files, nil) } +func deleteOldIdents(idents map[string]func(), f *ast.File) { + for _, decl := range f.Decls { + fd, ok := decl.(*ast.FuncDecl) + if !ok || fd.Recv != nil { // ignore methods + continue + } + if del := idents[fd.Name.Name]; del != nil { + del() + } + decl := decl + idents[fd.Name.Name] = func() { + // NOTE: cannot use the index as a file may contain multiple decls to be removed, + // so removing one would make all "later" indexes wrong. + f.Decls = slices.DeleteFunc(f.Decls, func(d ast.Decl) bool { return decl == d }) + } + } +} + //---------------------------------------- // utility methods diff --git a/gnovm/tests/integ/typecheck_missing_return/gno.mod b/gnovm/tests/integ/typecheck_missing_return/gno.mod new file mode 100644 index 00000000000..3eaaa374994 --- /dev/null +++ b/gnovm/tests/integ/typecheck_missing_return/gno.mod @@ -0,0 +1 @@ +module gno.land/p/integ/valid diff --git a/gnovm/tests/integ/typecheck_missing_return/main.gno b/gnovm/tests/integ/typecheck_missing_return/main.gno new file mode 100644 index 00000000000..5d6e547097c --- /dev/null +++ b/gnovm/tests/integ/typecheck_missing_return/main.gno @@ -0,0 +1,5 @@ +package valid + +func Hello() int { + // no return +} From 1bd64192a170fdf7ca904fb6bf27f10e2acc8ed5 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:16:05 +0100 Subject: [PATCH 315/345] feat(github-bot): add a fork condition and handle PR reviews (#3303) This PR improves the bot on two points: - it now handle `pull_request_review` events to address [this concern](https://github.com/gnolang/gno/issues/3238#issuecomment-2526206058) https://github.com/gnolang/gno/commit/4f7b0b80c6e726806a473556b02444fded707254 - a new condition allows to check if a PR was created from a fork to address [this concern](https://github.com/gnolang/gno/issues/3238#issuecomment-2524018469) https://github.com/gnolang/gno/commit/f491d95d68c755d7154cbbee48a9cebc7693fa89 --- .github/workflows/bot.yml | 4 +++ .../github-bot/internal/conditions/fork.go | 27 ++++++++++++++++ .../internal/conditions/fork_test.go | 31 +++++++++++++++++++ contribs/github-bot/internal/config/config.go | 2 +- contribs/github-bot/internal/matrix/matrix.go | 2 +- .../github-bot/internal/matrix/matrix_test.go | 9 ++++++ contribs/github-bot/internal/utils/actions.go | 2 +- .../github-bot/internal/utils/github_const.go | 1 + 8 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 contribs/github-bot/internal/conditions/fork.go create mode 100644 contribs/github-bot/internal/conditions/fork_test.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 644540c1aaf..300a5928e25 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -14,6 +14,10 @@ on: - converted_to_draft - ready_for_review + # Watch for changes on PR reviews + pull_request_review: + types: [submitted, edited, dismissed] + # Watch for changes on PR comment issue_comment: types: [created, edited, deleted] diff --git a/contribs/github-bot/internal/conditions/fork.go b/contribs/github-bot/internal/conditions/fork.go new file mode 100644 index 00000000000..72cbae12004 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork.go @@ -0,0 +1,27 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// CreatedFromFork Condition. +type createdFromFork struct{} + +var _ Condition = &createdFromFork{} + +func (b *createdFromFork) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName(), + fmt.Sprintf("The pull request was created from a fork (head branch repo: %s)", pr.GetHead().GetRepo().GetFullName()), + details, + ) +} + +func CreatedFromFork() Condition { + return &createdFromFork{} +} diff --git a/contribs/github-bot/internal/conditions/fork_test.go b/contribs/github-bot/internal/conditions/fork_test.go new file mode 100644 index 00000000000..fe7e9a95bf1 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork_test.go @@ -0,0 +1,31 @@ +package conditions + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestCreatedFromFork(t *testing.T) { + t.Parallel() + + var ( + repo = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("main")}, Name: github.String("repo"), FullName: github.String("main/repo")}} + fork = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("fork")}, Name: github.String("repo"), FullName: github.String("fork/repo")}} + ) + + prFromMain := &github.PullRequest{Base: repo, Head: repo} + prFromFork := &github.PullRequest{Base: repo, Head: fork} + + details := treeprint.New() + assert.False(t, CreatedFromFork().IsMet(prFromMain, details)) + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") + + details = treeprint.New() + assert.True(t, CreatedFromFork().IsMet(prFromFork, details)) + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") +} diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index c1d89e4cde5..2d595c7ce51 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -28,7 +28,7 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { auto := []AutomaticCheck{ { Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", - If: c.Always(), + If: c.CreatedFromFork(), Then: r.MaintainerCanModify(), }, { diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go index 9c8f12e4214..02840721c80 100644 --- a/contribs/github-bot/internal/matrix/matrix.go +++ b/contribs/github-bot/internal/matrix/matrix.go @@ -113,7 +113,7 @@ func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContex // Event triggered by an issue / PR comment being created / edited / deleted // or any update on a PR. - case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget: + case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestReview, utils.EventPullRequestTarget: // For these events, retrieve the number of the associated PR from the context. prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) if err != nil { diff --git a/contribs/github-bot/internal/matrix/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go index fe5b7452a49..f6b34f16c24 100644 --- a/contribs/github-bot/internal/matrix/matrix_test.go +++ b/contribs/github-bot/internal/matrix/matrix_test.go @@ -54,6 +54,15 @@ func TestProcessEvent(t *testing.T) { prs, utils.PRList{1}, false, + }, { + "valid pull_request_review event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestReview, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, }, { "valid pull_request_target event", &githubactions.GitHubContext{ diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go index 3e08a8e1548..0686e8c29c5 100644 --- a/contribs/github-bot/internal/utils/actions.go +++ b/contribs/github-bot/internal/utils/actions.go @@ -30,7 +30,7 @@ func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) switch actionCtx.EventName { case EventIssueComment: firstKey = "issue" - case EventPullRequest, EventPullRequestTarget: + case EventPullRequest, EventPullRequestReview, EventPullRequestTarget: firstKey = "pull_request" default: return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName) diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go index 26d7d54d477..f030d9365f7 100644 --- a/contribs/github-bot/internal/utils/github_const.go +++ b/contribs/github-bot/internal/utils/github_const.go @@ -5,6 +5,7 @@ const ( // GitHub Actions Event Names. EventIssueComment = "issue_comment" EventPullRequest = "pull_request" + EventPullRequestReview = "pull_request_review" EventPullRequestTarget = "pull_request_target" EventWorkflowDispatch = "workflow_dispatch" From 9bd9e47e7354d98cbc2980abbd6ee61096efd92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 9 Dec 2024 16:39:22 +0100 Subject: [PATCH 316/345] chore: update portal loop archiver version (#3308) ## Description This PR updates the portal loop tx-archiver version to `v0.4.2`, which eliminates the millisecond timestamp issue --- misc/loop/go.mod | 2 +- misc/loop/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/loop/go.mod b/misc/loop/go.mod index 70e9d21734b..af7783e57bb 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 github.com/gnolang/gno v0.1.0-nightly.20240627 - github.com/gnolang/tx-archive v0.4.0 + github.com/gnolang/tx-archive v0.4.2 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 ) diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 8e0feb11e4a..0d235f2cfb1 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -68,8 +68,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/tx-archive v0.4.0 h1:+1Rgo0U0HjLQLq/xqeGdJwtAzo9xWj09t1oZLvrL3bU= -github.com/gnolang/tx-archive v0.4.0/go.mod h1:seKHGnvxUnDgH/mSsCEdwG0dHY/FrpbUm6Hd0+KMd9w= +github.com/gnolang/tx-archive v0.4.2 h1:xBBqLLKY9riv9yxpQgVhItCWxIji2rX6xNFmCY1cEOQ= +github.com/gnolang/tx-archive v0.4.2/go.mod h1:AGUBGO+DCLuKL80a1GJRnpcJ5gxVd9L4jEJXQB9uXp4= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From bb38fb1942aac60999bed90c904711c46fe7b783 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:15:27 +0100 Subject: [PATCH 317/345] fix(gnovm): improve error message for nil assignment in variable declaration (#3068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …aration
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/preprocess.go | 199 ++++++++++++++------------- gnovm/pkg/gnolang/type_check.go | 53 ++++--- gnovm/pkg/gnolang/type_check_test.go | 2 +- gnovm/pkg/gnolang/types.go | 38 ++--- gnovm/tests/files/add3.gno | 9 ++ gnovm/tests/files/assign38.gno | 10 ++ gnovm/tests/files/fun28.gno | 10 ++ gnovm/tests/files/slice3.gno | 9 ++ gnovm/tests/files/var35.gno | 8 ++ 9 files changed, 200 insertions(+), 138 deletions(-) create mode 100644 gnovm/tests/files/add3.gno create mode 100644 gnovm/tests/files/assign38.gno create mode 100644 gnovm/tests/files/fun28.gno create mode 100644 gnovm/tests/files/slice3.gno create mode 100644 gnovm/tests/files/var35.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 78b11a4ebc5..6e749053d72 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -743,7 +743,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { for i, cx := range n.Cases { cx = Preprocess( store, last, cx).(Expr) - checkOrConvertType(store, last, &cx, tt, false) // #nosec G601 + checkOrConvertType(store, last, n, &cx, tt, false) // #nosec G601 n.Cases[i] = cx } } @@ -882,7 +882,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Preprocess and convert tag if const. if n.X != nil { n.X = Preprocess(store, last, n.X).(Expr) - convertIfConst(store, last, n.X) + convertIfConst(store, last, n, n.X) } } return n, TRANS_CONTINUE @@ -1102,10 +1102,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // First, convert untyped as necessary. if !shouldSwapOnSpecificity(lcx.T, rcx.T) { // convert n.Left to right type. - checkOrConvertType(store, last, &n.Left, rcx.T, false) + checkOrConvertType(store, last, n, &n.Left, rcx.T, false) } else { // convert n.Right to left type. - checkOrConvertType(store, last, &n.Right, lcx.T, false) + checkOrConvertType(store, last, n, &n.Right, lcx.T, false) } // Then, evaluate the expression. cx := evalConst(store, last, n) @@ -1125,7 +1125,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { rnt.String())) } // convert n.Left to pt type, - checkOrConvertType(store, last, &n.Left, pt, false) + checkOrConvertType(store, last, n, &n.Left, pt, false) // if check pass, convert n.Right to (gno) pt type, rn := Expr(Call(pt.String(), n.Right)) // and convert result back. @@ -1154,7 +1154,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } if !isUntyped(rt) { // right is typed - checkOrConvertType(store, last, &n.Left, rt, false) + checkOrConvertType(store, last, n, &n.Left, rt, false) } else { if shouldSwapOnSpecificity(lt, rt) { checkUntypedShiftExpr(n.Right) @@ -1165,10 +1165,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } } else if lcx.T == nil { // LHS is nil. // convert n.Left to typed-nil type. - checkOrConvertType(store, last, &n.Left, rt, false) + checkOrConvertType(store, last, n, &n.Left, rt, false) } else { if isUntyped(rt) { - checkOrConvertType(store, last, &n.Right, lt, false) + checkOrConvertType(store, last, n, &n.Right, lt, false) } } } else if ric { // right is const, left is not @@ -1186,7 +1186,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // convert n.Left to (gno) pt type, ln := Expr(Call(pt.String(), n.Left)) // convert n.Right to pt type, - checkOrConvertType(store, last, &n.Right, pt, false) + checkOrConvertType(store, last, n, &n.Right, pt, false) // and convert result back. tx := constType(n, lnt) // reset/create n2 to preprocess left child. @@ -1212,7 +1212,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // both untyped, e.g. 1< float64. // (const) untyped bigint -> int. if !constConverted { - convertConst(store, last, arg0, nil) + convertConst(store, last, n, arg0, nil) } // evaluate the new expression. cx := evalConst(store, last, n) @@ -1397,15 +1397,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if isUntyped(at) { switch arg0.Op { case EQL, NEQ, LSS, GTR, LEQ, GEQ: - assertAssignableTo(at, ct, false) + assertAssignableTo(n, at, ct, false) break default: - checkOrConvertType(store, last, &n.Args[0], ct, false) + checkOrConvertType(store, last, n, &n.Args[0], ct, false) } } case *UnaryExpr: if isUntyped(at) { - checkOrConvertType(store, last, &n.Args[0], ct, false) + checkOrConvertType(store, last, n, &n.Args[0], ct, false) } default: // do nothing @@ -1549,7 +1549,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { panic("should not happen") } // Specify function param/result generics. - sft := ft.Specify(store, argTVs, isVarg) + sft := ft.Specify(store, n, argTVs, isVarg) spts := sft.Params srts := FieldTypeList(sft.Results).Types() // If generics were specified, override attr @@ -1575,12 +1575,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { for i, tv := range argTVs { if hasVarg { if (len(spts) - 1) <= i { - assertAssignableTo(tv.T, spts[len(spts)-1].Type.Elem(), true) + assertAssignableTo(n, tv.T, spts[len(spts)-1].Type.Elem(), true) } else { - assertAssignableTo(tv.T, spts[i].Type, true) + assertAssignableTo(n, tv.T, spts[i].Type, true) } } else { - assertAssignableTo(tv.T, spts[i].Type, true) + assertAssignableTo(n, tv.T, spts[i].Type, true) } } } else { @@ -1591,16 +1591,16 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if len(spts) <= i { panic("expected final vargs slice but got many") } - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } else { - checkOrConvertType(store, last, &n.Args[i], + checkOrConvertType(store, last, n, &n.Args[i], spts[len(spts)-1].Type.Elem(), true) } } else { - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } } else { - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } } } @@ -1621,10 +1621,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case StringKind, ArrayKind, SliceKind: // Replace const index with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n.Index) + checkOrConvertIntegerKind(store, last, n, n.Index) case MapKind: mt := baseOf(gnoTypeOf(store, dt)).(*MapType) - checkOrConvertType(store, last, &n.Index, mt.Key, false) + checkOrConvertType(store, last, n, &n.Index, mt.Key, false) default: panic(fmt.Sprintf( "unexpected index base kind for type %s", @@ -1635,15 +1635,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case *SliceExpr: // Replace const L/H/M with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n.Low) - checkOrConvertIntegerKind(store, last, n.High) - checkOrConvertIntegerKind(store, last, n.Max) + checkOrConvertIntegerKind(store, last, n, n.Low) + checkOrConvertIntegerKind(store, last, n, n.High) + checkOrConvertIntegerKind(store, last, n, n.Max) // if n.X is untyped, convert to corresponding type t := evalStaticTypeOf(store, last, n.X) if isUntyped(t) { dt := defaultTypeOf(t) - checkOrConvertType(store, last, &n.X, dt, false) + checkOrConvertType(store, last, n, &n.X, dt, false) } // TRANS_LEAVE ----------------------- @@ -1722,28 +1722,28 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { key := n.Elts[i].Key.(*NameExpr).Name path := cclt.GetPathForName(key) ft := cclt.GetStaticTypeOfAt(path) - checkOrConvertType(store, last, &n.Elts[i].Value, ft, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, ft, false) } } else { for i := 0; i < len(n.Elts); i++ { ft := cclt.Fields[i].Type - checkOrConvertType(store, last, &n.Elts[i].Value, ft, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, ft, false) } } case *ArrayType: for i := 0; i < len(n.Elts); i++ { - convertType(store, last, &n.Elts[i].Key, IntType) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Elt, false) + convertType(store, last, n, &n.Elts[i].Key, IntType) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } case *SliceType: for i := 0; i < len(n.Elts); i++ { - convertType(store, last, &n.Elts[i].Key, IntType) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Elt, false) + convertType(store, last, n, &n.Elts[i].Key, IntType) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } case *MapType: for i := 0; i < len(n.Elts); i++ { - checkOrConvertType(store, last, &n.Elts[i].Key, cclt.Key, false) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Value, false) + checkOrConvertType(store, last, n, &n.Elts[i].Key, cclt.Key, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Value, false) } case *NativeType: clt = cclt.GnoType(store) @@ -1943,7 +1943,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *FieldTypeExpr: // Replace const Tag with default *ConstExpr. - convertIfConst(store, last, n.Tag) + convertIfConst(store, last, n, n.Tag) // TRANS_LEAVE ----------------------- case *ArrayTypeExpr: @@ -1952,7 +1952,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } else { // Replace const Len with int *ConstExpr. cx := evalConst(store, last, n.Len) - convertConst(store, last, cx, IntType) + convertConst(store, last, n, cx, IntType) n.Len = cx } // NOTE: For all TypeExprs, the node is not replaced @@ -1993,7 +1993,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Rhs consts become default *ConstExprs. for _, rx := range n.Rhs { // NOTE: does nothing if rx is "nil". - convertIfConst(store, last, rx) + convertIfConst(store, last, n, rx) } nameExprs := make(NameExprs, len(n.Lhs)) @@ -2001,7 +2001,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { nameExprs[i] = *n.Lhs[i].(*NameExpr) } - defineOrDecl(store, last, false, nameExprs, nil, n.Rhs) + defineOrDecl(store, last, n, false, nameExprs, nil, n.Rhs) } else { // ASSIGN, or assignment operation (+=, -=, <<=, etc.) // NOTE: Keep in sync with DEFINE above. if len(n.Lhs) > len(n.Rhs) { @@ -2090,11 +2090,11 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } else { // len(Lhs) == len(Rhs) if n.Op == SHL_ASSIGN || n.Op == SHR_ASSIGN { // Special case if shift assign <<= or >>=. - convertType(store, last, &n.Rhs[0], UintType) + convertType(store, last, n, &n.Rhs[0], UintType) } else if n.Op == ADD_ASSIGN || n.Op == SUB_ASSIGN || n.Op == MUL_ASSIGN || n.Op == QUO_ASSIGN || n.Op == REM_ASSIGN { // e.g. a += b, single value for lhs and rhs, lt := evalStaticTypeOf(store, last, n.Lhs[0]) - checkOrConvertType(store, last, &n.Rhs[0], lt, true) + checkOrConvertType(store, last, n, &n.Rhs[0], lt, true) } else { // all else, like BAND_ASSIGN, etc // General case: a, b = x, y. for i, lx := range n.Lhs { @@ -2104,7 +2104,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // if lt is interface, nothing will happen - checkOrConvertType(store, last, &n.Rhs[i], lt, true) + checkOrConvertType(store, last, n, &n.Rhs[i], lt, true) } } } @@ -2181,12 +2181,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *ForStmt: // Cond consts become bool *ConstExprs. - checkOrConvertBoolKind(store, last, n.Cond) + checkOrConvertBoolKind(store, last, n, n.Cond) // TRANS_LEAVE ----------------------- case *IfStmt: // Cond consts become bool *ConstExprs. - checkOrConvertBoolKind(store, last, n.Cond) + checkOrConvertBoolKind(store, last, n, n.Cond) // TRANS_LEAVE ----------------------- case *RangeStmt: @@ -2242,7 +2242,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // XXX how to deal? panic("not yet implemented") } else { - checkOrConvertType(store, last, &n.Results[i], rt, false) + checkOrConvertType(store, last, n, &n.Results[i], rt, false) } } } @@ -2250,7 +2250,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *SendStmt: // Value consts become default *ConstExprs. - checkOrConvertType(store, last, &n.Value, nil, false) + checkOrConvertType(store, last, n, &n.Value, nil, false) // TRANS_LEAVE ----------------------- case *SelectCaseStmt: @@ -2303,7 +2303,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // runDeclaration(), as this uses OpStaticTypeOf. } - defineOrDecl(store, last, n.Const, n.NameExprs, n.Type, n.Values) + defineOrDecl(store, last, n, n.Const, n.NameExprs, n.Type, n.Values) // TODO make note of constance in static block for // future use, or consider "const paths". set as @@ -2383,6 +2383,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { func defineOrDecl( store Store, bn BlockNode, + n Node, isConst bool, nameExprs []NameExpr, typeExpr Expr, @@ -2399,9 +2400,9 @@ func defineOrDecl( tvs := make([]TypedValue, numNames) if numVals == 1 && numNames > 1 { - parseMultipleAssignFromOneExpr(sts, tvs, store, bn, nameExprs, typeExpr, valueExprs[0]) + parseMultipleAssignFromOneExpr(store, bn, n, sts, tvs, nameExprs, typeExpr, valueExprs[0]) } else { - parseAssignFromExprList(sts, tvs, store, bn, isConst, nameExprs, typeExpr, valueExprs) + parseAssignFromExprList(store, bn, n, sts, tvs, isConst, nameExprs, typeExpr, valueExprs) } node := skipFile(bn) @@ -2420,10 +2421,11 @@ func defineOrDecl( // parseAssignFromExprList parses assignment to multiple variables from a list of expressions. // This function will alter the value of sts, tvs. func parseAssignFromExprList( - sts []Type, - tvs []TypedValue, store Store, bn BlockNode, + n Node, + sts []Type, + tvs []TypedValue, isConst bool, nameExprs []NameExpr, typeExpr Expr, @@ -2450,7 +2452,7 @@ func parseAssignFromExprList( } // Convert if const to nt. for i := range valueExprs { - checkOrConvertType(store, bn, &valueExprs[i], nt, false) + checkOrConvertType(store, bn, n, &valueExprs[i], nt, false) } } else if isConst { // Derive static type from values. @@ -2462,10 +2464,10 @@ func parseAssignFromExprList( // Convert n.Value to default type. for i, vx := range valueExprs { if cx, ok := vx.(*ConstExpr); ok { - convertConst(store, bn, cx, nil) + convertConst(store, bn, n, cx, nil) // convertIfConst(store, last, vx) } else { - checkOrConvertType(store, bn, &vx, nil, false) + checkOrConvertType(store, bn, n, &vx, nil, false) } vt := evalStaticTypeOf(store, bn, vx) sts[i] = vt @@ -2506,10 +2508,11 @@ func parseAssignFromExprList( // - a, b := n.(T) // - a, b := n[i], where n is a map func parseMultipleAssignFromOneExpr( - sts []Type, - tvs []TypedValue, store Store, bn BlockNode, + n Node, + sts []Type, + tvs []TypedValue, nameExprs []NameExpr, typeExpr Expr, valueExpr Expr, @@ -2567,7 +2570,7 @@ func parseMultipleAssignFromOneExpr( if st != nil { tt := tuple.Elts[i] - if checkAssignableTo(tt, st, false) != nil { + if checkAssignableTo(n, tt, st, false) != nil { panic( fmt.Sprintf( "cannot use %v (value of type %s) as %s value in assignment", @@ -3491,14 +3494,14 @@ func isConstType(x Expr) bool { } // check before convert type -func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative bool) { +func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type, autoNative bool) { if debug { debug.Printf("checkOrConvertType, *x: %v:, t:%v \n", *x, t) } if cx, ok := (*x).(*ConstExpr); ok { if _, ok := t.(*NativeType); !ok { // not native type, refer to time4_native.gno. // e.g. int(1) == int8(1) - assertAssignableTo(cx.T, t, autoNative) + assertAssignableTo(n, cx.T, t, autoNative) } } else if bx, ok := (*x).(*BinaryExpr); ok && (bx.Op == SHL || bx.Op == SHR) { xt := evalStaticTypeOf(store, last, *x) @@ -3507,22 +3510,22 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative } if isUntyped(xt) { // check assignable first, see: types/shift_b6.gno - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) if t == nil || t.Kind() == InterfaceKind { t = defaultTypeOf(xt) } bx.assertShiftExprCompatible2(t) - checkOrConvertType(store, last, &bx.Left, t, autoNative) + checkOrConvertType(store, last, n, &bx.Left, t, autoNative) } else { - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) } return } else if *x != nil { xt := evalStaticTypeOf(store, last, *x) if t != nil { - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) } if isUntyped(xt) { // Push type into expr if qualifying binary expr. @@ -3534,8 +3537,8 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative rt := evalStaticTypeOf(store, last, bx.Right) if t != nil { // push t into bx.Left and bx.Right - checkOrConvertType(store, last, &bx.Left, t, autoNative) - checkOrConvertType(store, last, &bx.Right, t, autoNative) + checkOrConvertType(store, last, n, &bx.Left, t, autoNative) + checkOrConvertType(store, last, n, &bx.Right, t, autoNative) return } else { if shouldSwapOnSpecificity(lt, rt) { @@ -3546,11 +3549,11 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative // without a specific context type, '1.0< + (const (undefined)) (mismatched types int and untyped nil) diff --git a/gnovm/tests/files/assign38.gno b/gnovm/tests/files/assign38.gno new file mode 100644 index 00000000000..5ef3549ccf6 --- /dev/null +++ b/gnovm/tests/files/assign38.gno @@ -0,0 +1,10 @@ +package main + +func main() { + a := 1 + a = nil + println(a) +} + +// Error: +// main/files/assign38.gno:5:2: cannot use nil as int value in assignment diff --git a/gnovm/tests/files/fun28.gno b/gnovm/tests/files/fun28.gno new file mode 100644 index 00000000000..cf969f9f34b --- /dev/null +++ b/gnovm/tests/files/fun28.gno @@ -0,0 +1,10 @@ +package main + +func f(i int) {} + +func main() { + f(nil) +} + +// Error: +// main/files/fun28.gno:6:2: cannot use nil as int value in argument to f diff --git a/gnovm/tests/files/slice3.gno b/gnovm/tests/files/slice3.gno new file mode 100644 index 00000000000..1132da01420 --- /dev/null +++ b/gnovm/tests/files/slice3.gno @@ -0,0 +1,9 @@ +package main + +func main() { + i := []string{nil} + println(i) +} + +// Error: +// main/files/slice3.gno:4:7: cannot use nil as string value in array, slice literal or map literal diff --git a/gnovm/tests/files/var35.gno b/gnovm/tests/files/var35.gno new file mode 100644 index 00000000000..87b1cc68590 --- /dev/null +++ b/gnovm/tests/files/var35.gno @@ -0,0 +1,8 @@ +package main + +func main() { + var i int = nil +} + +// Error: +// main/files/var35.gno:4:6: cannot use nil as int value in variable declaration From ed4ebe826b24189d0fcaa50c57eab03ff178c9fa Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Tue, 10 Dec 2024 02:36:38 -0800 Subject: [PATCH 318/345] fix(gnovm): do not allow nil as type declaration (#3309) Fix https://github.com/gnolang/gno/issues/3307 --------- Co-authored-by: hieu.ha --- gnovm/pkg/gnolang/preprocess.go | 6 ++++++ gnovm/tests/files/type40.gno | 2 +- gnovm/tests/files/type41.gno | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 gnovm/tests/files/type41.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 6e749053d72..a3e498710bb 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4283,6 +4283,12 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { if isBlankIdentifier(tx) { panic("cannot use _ as value or type") } + + // do not allow nil as type. + if tx.Name == "nil" { + panic("nil is not a type") + } + if tv := last.GetValueRef(store, tx.Name, true); tv != nil { t = tv.GetType() if dt, ok := t.(*DeclaredType); ok { diff --git a/gnovm/tests/files/type40.gno b/gnovm/tests/files/type40.gno index 65210798007..fe312e220e0 100644 --- a/gnovm/tests/files/type40.gno +++ b/gnovm/tests/files/type40.gno @@ -43,4 +43,4 @@ func main() { // 5 // 6 // 7 -// yo \ No newline at end of file +// yo diff --git a/gnovm/tests/files/type41.gno b/gnovm/tests/files/type41.gno new file mode 100644 index 00000000000..ea1a3b1df24 --- /dev/null +++ b/gnovm/tests/files/type41.gno @@ -0,0 +1,9 @@ +package main + +type A nil + +func main() { +} + +// Error: +// main/files/type41.gno:3:6: nil is not a type From 8e7fb503adc7a728ea30fedd0891f27d80a2fe1b Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:02:55 +0100 Subject: [PATCH 319/345] feat(github-bot): refactor comment + add force skip (#3311) This PR significantly modifies the github-bot's comment and adds a button to force the success of its CI check, even it the requirements provided in the config are not met. Related to https://github.com/gnolang/gno/issues/3238#issuecomment-2526174402 **Edit**: I updated [the comment below](https://github.com/gnolang/gno/pull/3311#issuecomment-2528477336) by running the bot on my laptop if you want to see the result (so the skip button is not working yet). --- contribs/github-bot/internal/check/check.go | 32 +++++++++++++++---- contribs/github-bot/internal/check/comment.go | 30 +++++++++-------- .../github-bot/internal/check/comment.tmpl | 32 ++++++++++++++----- .../github-bot/internal/check/comment_test.go | 19 +++++++++-- contribs/github-bot/internal/config/config.go | 9 ++++++ 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/contribs/github-bot/internal/check/check.go b/contribs/github-bot/internal/check/check.go index 5ca2235e823..cb1848b757c 100644 --- a/contribs/github-bot/internal/check/check.go +++ b/contribs/github-bot/internal/check/check.go @@ -101,7 +101,8 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { go func(pr *github.PullRequest) { defer wg.Done() commentContent := CommentContent{} - commentContent.allSatisfied = true + commentContent.AutoAllSatisfied = true + commentContent.ManualAllSatisfied = true // Iterate over all automatic rules in config. for _, autoRule := range autoRules { @@ -120,7 +121,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) c.Satisfied = true } else { - commentContent.allSatisfied = false + commentContent.AutoAllSatisfied = false } c.ConditionDetails = ifDetails.String() @@ -160,8 +161,14 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { }, ) - if checkedBy == "" { - commentContent.allSatisfied = false + // If this check is the special one, store its state in the dedicated var. + if manualRule.Description == config.ForceSkipDescription { + if checkedBy != "" { + commentContent.ForceSkip = true + } + } else if checkedBy == "" { + // Or if its a normal check, just verify if it was checked by someone. + commentContent.ManualAllSatisfied = false } } @@ -224,9 +231,20 @@ func logResults(logger logger.Logger, prNum int, commentContent CommentContent) } logger.Infof("Conclusion:") - if commentContent.allSatisfied { - logger.Infof("%s All requirements are satisfied\n", utils.Success) + + if commentContent.AutoAllSatisfied { + logger.Infof("%s All automated checks are satisfied", utils.Success) + } else { + logger.Infof("%s Some automated checks are not satisfied", utils.Fail) + } + + if commentContent.ManualAllSatisfied { + logger.Infof("%s All manual checks are satisfied\n", utils.Success) } else { - logger.Infof("%s Not all requirements are satisfied\n", utils.Fail) + logger.Infof("%s Some manual checks are not satisfied\n", utils.Fail) + } + + if commentContent.ForceSkip { + logger.Infof("%s Bot checks are force skipped\n", utils.Success) } } diff --git a/contribs/github-bot/internal/check/comment.go b/contribs/github-bot/internal/check/comment.go index 297395ffe4b..d2b386cfa2e 100644 --- a/contribs/github-bot/internal/check/comment.go +++ b/contribs/github-bot/internal/check/comment.go @@ -24,9 +24,9 @@ var errTriggeredByBot = errors.New("event triggered by bot") // Compile regex only once. var ( // Regex for capturing the entire line of a manual check. - manualCheckLine = regexp.MustCompile(`(?m:^-\s\[([ xX])\]\s+(.+?)\s*(\(checked by @(\w+)\))?$)`) + manualCheckLine = regexp.MustCompile(`(?m:^- \[([ xX])\] (.+?)(?: \(checked by @([A-Za-z0-9-]+)\))?$)`) // Regex for capturing only the checkboxes. - checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) + checkboxes = regexp.MustCompile(`(?m:^- \[[ xX]\])`) // Regex used to capture markdown links. markdownLink = regexp.MustCompile(`\[(.*)\]\([^)]*\)`) ) @@ -46,9 +46,11 @@ type ManualContent struct { Teams []string } type CommentContent struct { - AutoRules []AutoContent - ManualRules []ManualContent - allSatisfied bool + AutoRules []AutoContent + ManualRules []ManualContent + AutoAllSatisfied bool + ManualAllSatisfied bool + ForceSkip bool } type manualCheckDetails struct { @@ -64,10 +66,10 @@ func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { // For each line that matches the "Manual check" regex. for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { description := match[2] - status := match[1] + status := strings.ToLower(match[1]) // if X captured, convert it to x. checkedBy := "" - if len(match) > 4 { - checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x. + if len(match) > 3 { + checkedBy = match[3] } checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} @@ -261,13 +263,15 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen var ( context = "Merge Requirements" targetURL = comment.GetHTMLURL() - state = "failure" - description = "Some requirements are not satisfied yet. See bot comment." + state = "success" + description = "All requirements are satisfied." ) - if content.allSatisfied { - state = "success" - description = "All requirements are satisfied." + if content.ForceSkip { + description = "Bot checks are skipped for this PR." + } else if !content.AutoAllSatisfied || !content.ManualAllSatisfied { + state = "failure" + description = "Some requirements are not satisfied yet. See bot comment." } // Update or create commit status. diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl index 4312019dd2e..d9b633a69d5 100644 --- a/contribs/github-bot/internal/check/comment.tmpl +++ b/contribs/github-bot/internal/check/comment.tmpl @@ -1,19 +1,34 @@ -I'm a bot that assists the Gno Core team in maintaining this repository. My role is to ensure that contributors understand and follow our guidelines, helping to streamline the development process. +#### 🛠 PR Checks Summary +{{ if and .AutoRules (not .AutoAllSatisfied) }}{{ range .AutoRules }}{{ if not .Satisfied }} 🔴 {{ .Description }} +{{end}}{{end}}{{ else }}All **Automated Checks** passed. ✅{{end}} -The following requirements must be fulfilled before a pull request can be merged. -Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member. +##### Manual Checks (for Reviewers): +{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} +{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }} -These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). +
Read More -## Automated Checks +🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. +##### ✅ Automated Checks (for Contributors): {{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} {{ end }}{{ else }}*No automated checks match this pull request.*{{ end }} -## Manual Checks +##### ☑️ Contributor Actions: +1. Fix any issues flagged by automated checks. +2. Follow the Contributor Checklist to ensure your PR is ready for review. + - Add new tests, or document why they are unnecessary. + - Provide clear examples/screenshots, if necessary. + - Update documentation, if required. + - Ensure no breaking changes, or include `BREAKING CHANGE` notes. + - Link related issues/PRs, where applicable. -{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} -{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }} +##### ☑️ Reviewer Actions: +1. Complete manual checks for the PR, including the guidelines and additional checks if applicable. + +##### 📚 Resources: +- [Report a bug with the bot](https://github.com/gnolang/gno/issues/3238). +- [View the bot’s configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). {{ if or .AutoRules .ManualRules }}
Debug
{{ if .AutoRules }}
Automated Checks
@@ -52,3 +67,4 @@ These requirements are defined in this [configuration file](https://github.com/g {{ end }}
{{ end }} +
diff --git a/contribs/github-bot/internal/check/comment_test.go b/contribs/github-bot/internal/check/comment_test.go index 0334b76f95c..29886f80f43 100644 --- a/contribs/github-bot/internal/check/comment_test.go +++ b/contribs/github-bot/internal/check/comment_test.go @@ -31,31 +31,44 @@ func TestGeneratedComment(t *testing.T) { {Description: "Test automatic 5", Satisfied: false}, } manualRules := []ManualContent{ - {Description: "Test manual 1", CheckedBy: "user_1"}, + {Description: "Test manual 1", CheckedBy: "user-1"}, {Description: "Test manual 2", CheckedBy: ""}, {Description: "Test manual 3", CheckedBy: ""}, - {Description: "Test manual 4", CheckedBy: "user_4"}, - {Description: "Test manual 5", CheckedBy: "user_5"}, + {Description: "Test manual 4", CheckedBy: "user-4"}, + {Description: "Test manual 5", CheckedBy: "user-5"}, } commentText, err := generateComment(content) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") content.AutoRules = autoRules + content.AutoAllSatisfied = true commentText, err = generateComment(content) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + content.AutoAllSatisfied = false + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3+3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + content.ManualRules = manualRules commentText, err = generateComment(content) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") manualChecks := getCommentManualChecks(commentText) assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found") diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index 2d595c7ce51..fd29f5e5f57 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -22,6 +22,10 @@ type ManualCheck struct { Teams Teams // Members of these teams can check the checkbox to make the check pass. } +// This is the description for a persistent rule with a non-standard behavior +// that allow maintainer to force the "success" state of the CI check +const ForceSkipDescription = "**SKIP**: Do not block the CI for this PR" + // This function returns the configuration of the bot consisting of automatic and manual checks // in which the GitHub client is injected. func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { @@ -53,6 +57,11 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { } manual := []ManualCheck{ + { + // WARN: Do not edit this special rule which must remain persistent. + Description: ForceSkipDescription, + If: c.Always(), + }, { Description: "The pull request description provides enough details", If: c.Not(c.AuthorInTeam(gh, "core-contributors")), From 4e7305b67ba23f079e33e73e31e0abd90c33d4b9 Mon Sep 17 00:00:00 2001 From: matijamarjanovic <93043005+matijamarjanovic@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:15:51 +0100 Subject: [PATCH 320/345] feat: add Matija's Homepage realm to examples (#2916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This pull request adds a new realm example to the Gno `examples` repository—Matija's Homepage. It showcases a personal homepage built on the Gno chain where users can interact by voting with GNOT tokens to change the page's color. The more tokens users send, the greater influence they have on the color scheme, providing an interactive and dynamic experience. ### Key Features - **Profile Section**: Displays a personal profile with an image and description. - **Color Voting**: Users can vote for the page's color (red, green, blue) by sending GNOT tokens. RGB values are adjusted based on the amount sent. - **Dynamic Updates**: The homepage dynamically updates the color based on votes, showcasing real-time interaction on the Gno blockchain. - **Links to GitHub and LinkedIn**: Includes buttons for GitHub and LinkedIn, making it easy for users to connect. ### Tools & Technologies - Utilizes Gno's native functions to handle voting and token transfers. - Provides a simple, yet effective example of how personal realms can be interactive and engaging on the Gno platform. ### Why this is valuable This example highlights the possibilities of personal realms on Gno, showing how users can create unique and interactive profiles. It’s a fun and approachable entry point for anyone new to Gno development, while also demonstrating the platform's flexibility and potential for creative expression. --------- Co-authored-by: Morgan Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../r/matijamarjanovic/home/config.gno | 64 +++++ .../gno.land/r/matijamarjanovic/home/gno.mod | 1 + .../gno.land/r/matijamarjanovic/home/home.gno | 238 ++++++++++++++++++ .../r/matijamarjanovic/home/home_test.gno | 134 ++++++++++ 4 files changed, 437 insertions(+) create mode 100644 examples/gno.land/r/matijamarjanovic/home/config.gno create mode 100644 examples/gno.land/r/matijamarjanovic/home/gno.mod create mode 100644 examples/gno.land/r/matijamarjanovic/home/home.gno create mode 100644 examples/gno.land/r/matijamarjanovic/home/home_test.gno diff --git a/examples/gno.land/r/matijamarjanovic/home/config.gno b/examples/gno.land/r/matijamarjanovic/home/config.gno new file mode 100644 index 00000000000..2a9669c0b58 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/config.gno @@ -0,0 +1,64 @@ +package home + +import ( + "errors" + "std" +) + +var ( + mainAddr = std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y") // matija's main address + backupAddr std.Address // backup address + + errorInvalidAddr = errors.New("config: invalid address") + errorUnauthorized = errors.New("config: unauthorized") +) + +func Address() std.Address { + return mainAddr +} + +func Backup() std.Address { + return backupAddr +} + +func SetAddress(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + mainAddr = newAddress + return nil +} + +func SetBackup(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backupAddr = newAddress + return nil +} + +func checkAuthorized() error { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + return errorUnauthorized + } + + return nil +} + +func AssertAuthorized() { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + panic(errorUnauthorized) + } +} diff --git a/examples/gno.land/r/matijamarjanovic/home/gno.mod b/examples/gno.land/r/matijamarjanovic/home/gno.mod new file mode 100644 index 00000000000..0457c947c01 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/matijamarjanovic/home diff --git a/examples/gno.land/r/matijamarjanovic/home/home.gno b/examples/gno.land/r/matijamarjanovic/home/home.gno new file mode 100644 index 00000000000..3757324108a --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home.gno @@ -0,0 +1,238 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/r/leon/hof" +) + +var ( + pfp string // link to profile picture + pfpCaption string // profile picture caption + abtMe string + + modernVotes int64 + classicVotes int64 + minimalVotes int64 + currentTheme string + + modernLink string + classicLink string + minimalLink string +) + +func init() { + pfp = "https://static.artzone.ai/media/38734/conversions/IPF9dR7ro7n05CmMLLrXIojycr1qdLFxgutaaanG-w768.webp" + pfpCaption = "My profile picture - Tarantula Nebula" + abtMe = `Motivated Computer Science student with strong + analytical and problem-solving skills. Proficient in + programming and version control, with a high level of + focus and attention to detail. Eager to apply academic + knowledge to real-world projects and contribute to + innovative technology solutions. + In addition to my academic pursuits, + I enjoy traveling and staying active through weightlifting. + I have a keen interest in electronic music and often explore various genres. + I believe in maintaining a balanced lifestyle that complements my professional development.` + + modernVotes = 0 + classicVotes = 0 + minimalVotes = 0 + currentTheme = "classic" + modernLink = "https://www.google.com" + classicLink = "https://www.google.com" + minimalLink = "https://www.google.com" + hof.Register() +} + +func UpdatePFP(url, caption string) { + AssertAuthorized() + pfp = url + pfpCaption = caption +} + +func UpdateAboutMe(col1 string) { + AssertAuthorized() + abtMe = col1 +} + +func maxOfThree(a, b, c int64) int64 { + max := a + if b > max { + max = b + } + if c > max { + max = c + } + return max +} + +func VoteModern() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + modernVotes += votes + updateCurrentTheme() +} + +func VoteClassic() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + classicVotes += votes + updateCurrentTheme() +} + +func VoteMinimal() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + minimalVotes += votes + updateCurrentTheme() +} + +func updateCurrentTheme() { + maxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes) + + if maxVotes == modernVotes { + currentTheme = "modern" + } else if maxVotes == classicVotes { + currentTheme = "classic" + } else { + currentTheme = "minimal" + } +} + +func CollectBalance() { + AssertAuthorized() + + banker := std.GetBanker(std.BankerTypeRealmSend) + ownerAddr := Address() + + banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr())) +} + +func Render(path string) string { + var sb strings.Builder + + // Theme-specific header styling + switch currentTheme { + case "modern": + // Modern theme - Clean and minimalist with emojis + sb.WriteString(md.H1("🚀 Matija's Space")) + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(md.Italic(pfpCaption)) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(abtMe) + sb.WriteString("\n") + + case "minimal": + // Minimal theme - No emojis, minimal formatting + sb.WriteString(md.H1("Matija Marjanovic")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + + default: // classic + // Classic theme - Traditional blog style with decorative elements + sb.WriteString(md.H1("✨ Welcome to Matija's Homepage ✨")) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("About me")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + } + + // Theme-specific voting section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("🎨 Theme Selector")) + sb.WriteString("Choose your preferred viewing experience:\n") + items := []string{ + md.Link(ufmt.Sprintf("Modern Design (%d votes)", modernVotes), modernLink), + md.Link(ufmt.Sprintf("Classic Style (%d votes)", classicVotes), classicLink), + md.Link(ufmt.Sprintf("Minimal Look (%d votes)", minimalVotes), minimalLink), + } + sb.WriteString(md.BulletList(items)) + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.H3("Theme Selection")) + sb.WriteString(ufmt.Sprintf("Current theme: %s\n", currentTheme)) + sb.WriteString(ufmt.Sprintf("Votes - Modern: %d | Classic: %d | Minimal: %d\n", + modernVotes, classicVotes, minimalVotes)) + sb.WriteString(md.Link("Modern", modernLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Classic", classicLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Minimal", minimalLink)) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("✨ Theme Customization ✨")) + sb.WriteString(md.Bold("Choose Your Preferred Theme:")) + sb.WriteString("\n\n") + items := []string{ + ufmt.Sprintf("Modern 🚀 (%d votes) - %s", modernVotes, md.Link("Vote", modernLink)), + ufmt.Sprintf("Classic ✨ (%d votes) - %s", classicVotes, md.Link("Vote", classicLink)), + ufmt.Sprintf("Minimal ⚡ (%d votes) - %s", minimalVotes, md.Link("Vote", minimalLink)), + } + sb.WriteString(md.BulletList(items)) + } + + // Theme-specific footer/links section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H3("✨ Connect With Me")) + items := []string{ + md.Link("🌟 GitHub", "https://github.com/matijamarjanovic"), + md.Link("💼 LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"), + } + sb.WriteString(md.BulletList(items)) + } + + return sb.String() +} + +func UpdateModernLink(link string) { + AssertAuthorized() + modernLink = link +} + +func UpdateClassicLink(link string) { + AssertAuthorized() + classicLink = link +} + +func UpdateMinimalLink(link string) { + AssertAuthorized() + minimalLink = link +} diff --git a/examples/gno.land/r/matijamarjanovic/home/home_test.gno b/examples/gno.land/r/matijamarjanovic/home/home_test.gno new file mode 100644 index 00000000000..8cc6e6e5608 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home_test.gno @@ -0,0 +1,134 @@ +package home + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +// Helper function to set up test environment +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) +} + +func TestUpdatePFP(t *testing.T) { + setupTest() + pfp = "" + pfpCaption = "" + + UpdatePFP("https://example.com/pic.png", "New Caption") + + urequire.Equal(t, pfp, "https://example.com/pic.png", "Profile picture URL should be updated") + urequire.Equal(t, pfpCaption, "New Caption", "Profile picture caption should be updated") +} + +func TestUpdateAboutMe(t *testing.T) { + setupTest() + abtMe = "" + + UpdateAboutMe("This is my new bio.") + + urequire.Equal(t, abtMe, "This is my new bio.", "About Me should be updated") +} + +func TestVoteModern(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteModern() + + uassert.Equal(t, int64(75000000), modernVotes, "Modern votes should be calculated correctly") + uassert.Equal(t, "modern", currentTheme, "Theme should be updated to modern") +} + +func TestVoteClassic(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteClassic() + + uassert.Equal(t, int64(75000000), classicVotes, "Classic votes should be calculated correctly") + uassert.Equal(t, "classic", currentTheme, "Theme should be updated to classic") +} + +func TestVoteMinimal(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteMinimal() + + uassert.Equal(t, int64(75000000), minimalVotes, "Minimal votes should be calculated correctly") + uassert.Equal(t, "minimal", currentTheme, "Theme should be updated to minimal") +} + +func TestRender(t *testing.T) { + setupTest() + // Reset the state to known values + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + currentTheme = "classic" + pfp = "https://example.com/pic.png" + pfpCaption = "Test Caption" + abtMe = "Test About Me" + + out := Render("") + urequire.NotEqual(t, out, "", "Render output should not be empty") + + // Test classic theme specific content + uassert.True(t, strings.Contains(out, "✨ Welcome to Matija's Homepage ✨"), "Classic theme should have correct header") + uassert.True(t, strings.Contains(out, pfp), "Should contain profile picture URL") + uassert.True(t, strings.Contains(out, pfpCaption), "Should contain profile picture caption") + uassert.True(t, strings.Contains(out, "About me"), "Should contain About me section") + uassert.True(t, strings.Contains(out, abtMe), "Should contain about me content") + uassert.True(t, strings.Contains(out, "Theme Customization"), "Should contain theme customization section") + uassert.True(t, strings.Contains(out, "Connect With Me"), "Should contain connect section") +} + +func TestRenderModernTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 100, 0, 0 + currentTheme = "modern" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "🚀 Matija's Space"), "Modern theme should have correct header") +} + +func TestRenderMinimalTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 100 + currentTheme = "minimal" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "Matija Marjanovic"), "Minimal theme should have correct header") +} + +func TestUpdateLinks(t *testing.T) { + setupTest() + + newLink := "https://example.com/vote" + + UpdateModernLink(newLink) + urequire.Equal(t, modernLink, newLink, "Modern link should be updated") + + UpdateClassicLink(newLink) + urequire.Equal(t, classicLink, newLink, "Classic link should be updated") + + UpdateMinimalLink(newLink) + urequire.Equal(t, minimalLink, newLink, "Minimal link should be updated") +} From 5c31552b05c78575f45876ab07abd73c1d96bf27 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Tue, 10 Dec 2024 21:50:53 +0800 Subject: [PATCH 321/345] fix(gnovm): make static-analysis handle block stmt (#3313) as the title says. it give incorrect error before fix: ``` unexpected panic: main/files/block0.gno:3:1: [function "foo" does not terminate] ``` --- gnovm/pkg/gnolang/static_analysis.go | 2 ++ gnovm/tests/files/block0.gno | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 gnovm/tests/files/block0.gno diff --git a/gnovm/pkg/gnolang/static_analysis.go b/gnovm/pkg/gnolang/static_analysis.go index 311a0d42feb..7094ccbb4c8 100644 --- a/gnovm/pkg/gnolang/static_analysis.go +++ b/gnovm/pkg/gnolang/static_analysis.go @@ -108,6 +108,8 @@ func (s *staticAnalysis) staticAnalysisExpr(expr Expr) bool { // indicating whether a statement is terminating or not func (s *staticAnalysis) staticAnalysisStmt(stmt Stmt) bool { switch n := stmt.(type) { + case *BlockStmt: + return s.staticAnalysisBlockStmt(n.Body) case *BranchStmt: switch n.Op { case BREAK: diff --git a/gnovm/tests/files/block0.gno b/gnovm/tests/files/block0.gno new file mode 100644 index 00000000000..b6d554ce500 --- /dev/null +++ b/gnovm/tests/files/block0.gno @@ -0,0 +1,14 @@ +package main + +func foo() int { + { + return 1 + } +} + +func main() { + println(foo()) +} + +// Output: +// 1 From c33cf676daed03a29ca85f7386daf98e35d2b38f Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:20:52 -0800 Subject: [PATCH 322/345] fix: catch the out of gas exception in preprocess (#2638)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- .../gnoland/testdata/addpkg_outofgas.txtar | 57 +++++++++++++++++++ gnovm/pkg/gnolang/preprocess.go | 7 +++ 2 files changed, 64 insertions(+) create mode 100644 gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar diff --git a/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar new file mode 100644 index 00000000000..56050f4733b --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar @@ -0,0 +1,57 @@ +# ensure users get proper out of gas errors when they add packages + +# start a new node +gnoland start + +# add foo package +gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 220000 -broadcast -chainid=tendermint_test test1 + + +# add bar package +# out of gas at store.GetPackage() with gas 60000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 60000 -broadcast -chainid=tendermint_test test1 + +# Out of gas error + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + + +# out of gas at store.store.GetTypeSafe() with gas 63000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 63000 -broadcast -chainid=tendermint_test test1 + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + +-- foo/foo.gno -- +package foo + +type Counter int + +func Inc(i Counter) Counter{ + i = i+1 + return i +} + +-- bar/bar.gno -- +package bar + +import "gno.land/r/foo" + +type NewCounter foo.Counter + +func Add2(i NewCounter) NewCounter{ + i=i+2 + + return i +} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a3e498710bb..15f268f6321 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -10,6 +10,7 @@ import ( "sync/atomic" "github.com/gnolang/gno/tm2/pkg/errors" + tmstore "github.com/gnolang/gno/tm2/pkg/store" ) const ( @@ -365,6 +366,12 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { func doRecover(stack []BlockNode, n Node) { if r := recover(); r != nil { + // Catch the out-of-gas exception and throw it + if exp, ok := r.(tmstore.OutOfGasException); ok { + exp.Descriptor = fmt.Sprintf("in preprocess: %v", r) + panic(exp) + } + if _, ok := r.(*PreprocessError); ok { // re-panic directly if this is a PreprocessError already. panic(r) From 7185cefe2e091cb1795aef1d3d4c4d938a81778c Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 11 Dec 2024 09:05:18 +0100 Subject: [PATCH 323/345] fix(gnovm): in op_binary, return typed booleans where appropriate (#3298) bool8.gno was failing, because the result of the `==` expression is an untyped boolean, while the first value is a typed boolean. This PR ensures that if either of the values in a binary expression is typed, we return a typed bool instead of an untyped bool. --------- Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/preprocess.go | 4 ++++ gnovm/tests/files/bool8.gno | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 gnovm/tests/files/bool8.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 15f268f6321..4ff182670cd 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3574,6 +3574,10 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type, au checkOrConvertType(store, last, n, &bx.Left, rt, autoNative) checkOrConvertType(store, last, n, &bx.Right, rt, autoNative) } + // this is not a constant expression; the result here should + // always be a BoolType. (in this scenario, we may have some + // UntypedBoolTypes) + t = BoolType default: // do nothing } diff --git a/gnovm/tests/files/bool8.gno b/gnovm/tests/files/bool8.gno new file mode 100644 index 00000000000..9efbbbe6da2 --- /dev/null +++ b/gnovm/tests/files/bool8.gno @@ -0,0 +1,17 @@ +package main + +// results from comparisons should not be untyped bools + +var a interface{} = true + +func main() { + buf := "hello=" + isEqual(a, (buf[len(buf)-1] == '=')) +} + +func isEqual(v1, v2 interface{}) { + println("v1 == v2", v1 == v2) +} + +// Output: +// v1 == v2 true From 6f48a5b6eb26e6dcb7cd2797c57f4545cb02f744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 11 Dec 2024 11:42:06 +0100 Subject: [PATCH 324/345] feat: generic datasource package (#3318) The new package is a generic implementation for datasources. It aims to be one possible solution to integrate/aggregate data from different realms. --- .../p/jeronimoalbi/datasource/datasource.gno | 103 +++++++++++ .../datasource/datasource_test.gno | 171 ++++++++++++++++++ .../p/jeronimoalbi/datasource/gno.mod | 1 + .../p/jeronimoalbi/datasource/query.gno | 70 +++++++ .../p/jeronimoalbi/datasource/query_test.gno | 104 +++++++++++ 5 files changed, 449 insertions(+) create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/datasource.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/gno.mod create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/query.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/query_test.gno diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno new file mode 100644 index 00000000000..bf80964a9a0 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno @@ -0,0 +1,103 @@ +// Package datasource defines generic interfaces for datasources. +// +// Datasources contain a set of records which can optionally be +// taggable. Tags can optionally be used to filter records by taxonomy. +// +// Datasources can help in cases where the data sent during +// communication between different realms needs to be generic +// to avoid direct dependencies. +package datasource + +import "errors" + +// ErrInvalidRecord indicates that a datasource contains invalid records. +var ErrInvalidRecord = errors.New("datasource records is not valid") + +type ( + // Fields defines an interface for read-only fields. + Fields interface { + // Has checks whether a field exists. + Has(name string) bool + + // Get retrieves the value associated with the given field. + Get(name string) (value interface{}, found bool) + } + + // Record defines a datasource record. + Record interface { + // ID returns the unique record's identifier. + ID() string + + // String returns a string representation of the record. + String() string + + // Fields returns record fields and values. + Fields() (Fields, error) + } + + // TaggableRecord defines a datasource record that supports tags. + // Tags can be used to build a taxonomy to filter records by category. + TaggableRecord interface { + // Tags returns a list of tags for the record. + Tags() []string + } + + // ContentRecord defines a datasource record that can return content. + ContentRecord interface { + // Content returns the record content. + Content() (string, error) + } + + // Iterator defines an iterator of datasource records. + Iterator interface { + // Next returns true when a new record is available. + Next() bool + + // Err returns any error raised when reading records. + Err() error + + // Record returns the current record. + Record() Record + } + + // Datasource defines a generic datasource. + Datasource interface { + // Records returns a new datasource records iterator. + Records(Query) Iterator + + // Size returns the total number of records in the datasource. + // When -1 is returned it means datasource doesn't support size. + Size() int + + // Record returns a single datasource record. + Record(id string) (Record, error) + } +) + +// NewIterator returns a new record iterator for a datasource query. +func NewIterator(ds Datasource, options ...QueryOption) Iterator { + return ds.Records(NewQuery(options...)) +} + +// QueryRecords return a slice of records for a datasource query. +func QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) { + var ( + records []Record + query = NewQuery(options...) + iter = ds.Records(query) + ) + + for i := 0; i < query.Count && iter.Next(); i++ { + r := iter.Record() + if r == nil { + return nil, ErrInvalidRecord + } + + records = append(records, r) + } + + if err := iter.Err(); err != nil { + return nil, err + } + return records, nil +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno new file mode 100644 index 00000000000..304a311ced7 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno @@ -0,0 +1,171 @@ +package datasource + +import ( + "errors" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNewIterator(t *testing.T) { + cases := []struct { + name string + records []Record + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + }, + { + name: "error", + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + iter := NewIterator(ds) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, iter.Err()) + return + } + + uassert.NoError(t, iter.Err()) + + for i := 0; iter.Next(); i++ { + r := iter.Record() + urequire.NotEqual(t, nil, r, "valid record") + urequire.True(t, i < len(tc.records), "iteration count") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +func TestQueryRecords(t *testing.T) { + cases := []struct { + name string + records []Record + recordCount int + options []QueryOption + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 3, + }, + { + name: "with count", + options: []QueryOption{WithCount(2)}, + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 2, + }, + { + name: "invalid record", + records: []Record{ + testRecord{id: "1"}, + nil, + testRecord{id: "3"}, + }, + err: ErrInvalidRecord, + }, + { + name: "iterator error", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "3"}, + }, + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + records, err := QueryRecords(ds, tc.options...) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, err) + return + } + + uassert.NoError(t, err) + + urequire.Equal(t, tc.recordCount, len(records), "record count") + for i, r := range records { + urequire.NotEqual(t, nil, r, "valid record") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +type testDatasource struct { + records []Record + err error +} + +func (testDatasource) Size() int { return -1 } +func (testDatasource) Record(string) (Record, error) { return nil, nil } +func (ds testDatasource) Records(Query) Iterator { return &testIter{records: ds.records, err: ds.err} } + +type testRecord struct { + id string + fields Fields + err error +} + +func (r testRecord) ID() string { return r.id } +func (r testRecord) String() string { return "str" + r.id } +func (r testRecord) Fields() (Fields, error) { return r.fields, r.err } + +type testIter struct { + index int + records []Record + current Record + err error +} + +func (it testIter) Err() error { return it.err } +func (it testIter) Record() Record { return it.current } + +func (it *testIter) Next() bool { + count := len(it.records) + if it.err != nil || count == 0 || it.index >= count { + return false + } + it.current = it.records[it.index] + it.index++ + return true +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/gno.mod b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod new file mode 100644 index 00000000000..3b398971b41 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod @@ -0,0 +1 @@ +module gno.land/p/jeronimoalbi/datasource diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query.gno b/examples/gno.land/p/jeronimoalbi/datasource/query.gno new file mode 100644 index 00000000000..f971f9c64db --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query.gno @@ -0,0 +1,70 @@ +package datasource + +import "gno.land/p/demo/avl" + +// DefaultQueryRecords defines the default number of records returned by queries. +const DefaultQueryRecords = 50 + +var defaultQuery = Query{Count: DefaultQueryRecords} + +type ( + // QueryOption configures datasource queries. + QueryOption func(*Query) + + // Query contains datasource query options. + Query struct { + // Offset of the first record to return during iteration. + Offset int + + // Count contains the number to records that query should return. + Count int + + // Tag contains a tag to use as filter for the records. + Tag string + + // Filters contains optional query filters by field value. + Filters avl.Tree + } +) + +// WithOffset configures query to return records starting from an offset. +func WithOffset(offset int) QueryOption { + return func(q *Query) { + q.Offset = offset + } +} + +// WithCount configures the number of records that query returns. +func WithCount(count int) QueryOption { + return func(q *Query) { + if count < 1 { + count = DefaultQueryRecords + } + q.Count = count + } +} + +// ByTag configures query to filter by tag. +func ByTag(tag string) QueryOption { + return func(q *Query) { + q.Tag = tag + } +} + +// WithFilter assigns a new filter argument to a query. +// This option can be used multiple times if more than one +// filter has to be given to the query. +func WithFilter(field string, value interface{}) QueryOption { + return func(q *Query) { + q.Filters.Set(field, value) + } +} + +// NewQuery creates a new datasource query. +func NewQuery(options ...QueryOption) Query { + q := defaultQuery + for _, apply := range options { + apply(&q) + } + return q +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno new file mode 100644 index 00000000000..6f78d41bb35 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno @@ -0,0 +1,104 @@ +package datasource + +import ( + "fmt" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestNewQuery(t *testing.T) { + cases := []struct { + name string + options []QueryOption + setup func() Query + }{ + { + name: "default", + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "with offset", + options: []QueryOption{WithOffset(100)}, + setup: func() Query { + return Query{ + Offset: 100, + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with count", + options: []QueryOption{WithCount(10)}, + setup: func() Query { + return Query{Count: 10} + }, + }, + { + name: "with invalid count", + options: []QueryOption{WithCount(0)}, + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "by tag", + options: []QueryOption{ByTag("foo")}, + setup: func() Query { + return Query{ + Tag: "foo", + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with filter", + options: []QueryOption{WithFilter("foo", 42)}, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + return q + }, + }, + { + name: "with multiple filters", + options: []QueryOption{ + WithFilter("foo", 42), + WithFilter("bar", "baz"), + }, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + q.Filters.Set("bar", "baz") + return q + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + want := tc.setup() + + // Act + q := NewQuery(tc.options...) + + // Assert + uassert.Equal(t, want.Offset, q.Offset) + uassert.Equal(t, want.Count, q.Count) + uassert.Equal(t, want.Tag, q.Tag) + uassert.Equal(t, want.Filters.Size(), q.Filters.Size()) + + want.Filters.Iterate("", "", func(k string, v interface{}) bool { + got, exists := q.Filters.Get(k) + uassert.True(t, exists) + if exists { + uassert.Equal(t, fmt.Sprint(v), fmt.Sprint(got)) + } + return false + }) + }) + } +} From a85a53d5b38f0a21d66262a823a8b07f4f836b68 Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:19:04 -0800 Subject: [PATCH 325/345] fix: prevent false positive return for guarding dao member store (#3121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we want to guard the MemStore by checking the active DAO realm, m.daoPkgPath must first be assigned a realm package path; otherwise, the isCallerDAORealm() method may return a false positive, failing to protect the MemStore.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Miloš Živković --- examples/gno.land/p/demo/membstore/membstore.gno | 2 +- examples/gno.land/r/gov/dao/v2/dao.gno | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno index 6e1932978d9..ca721d078e6 100644 --- a/examples/gno.land/p/demo/membstore/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -205,5 +205,5 @@ func (m *MembStore) TotalPower() uint64 { // the API of the member store is public and callable // by anyone who has a reference to the member store instance. func (m *MembStore) isCallerDAORealm() bool { - return m.daoPkgPath == "" || std.CurrentRealm().PkgPath() == m.daoPkgPath + return m.daoPkgPath != "" && std.CurrentRealm().PkgPath() == m.daoPkgPath } diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 9263d8d440b..5ee8e63236a 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -13,6 +13,8 @@ var ( members membstore.MemberStore // the member store ) +const daoPkgPath = "gno.land/r/gov/dao/v2" + func init() { // Example initial member set (just test addresses) set := []membstore.Member{ @@ -23,7 +25,7 @@ func init() { } // Set the member store - members = membstore.NewMembStore(membstore.WithInitialMembers(set)) + members = membstore.NewMembStore(membstore.WithInitialMembers(set), membstore.WithDAOPkgPath(daoPkgPath)) // Set the DAO implementation d = simpledao.New(members) From 79ca9a958dea7c94aaf6c3dc74f522c5f05e791e Mon Sep 17 00:00:00 2001 From: Mikael VALLENET Date: Thu, 12 Dec 2024 16:47:27 +0100 Subject: [PATCH 326/345] fix(std): add full denom in banker issue & remove coin (#3239) fix #2107 -------------------------- ## Problem > From [#875 (review)](https://github.com/gnolang/gno/pull/875#pullrequestreview-2043984930): > > > That said, soon after this is merged, I think we'll need to change this API again. This current implementation creates an inconsistency within the Banker API. All other banker methods now require you to pass in the full realm path to the token you're referring to, but IssueCoin and RemoveCoin do not. > > Thus, I think a few more changes are in order: > > > > 1. There should be a `RealmDenom(pkgpath, denom string)` function in `std`, which creates a realm denomination (ie. `/gno.land/r/morgan:bitcoin`). There can be a helper method `Realm.Denom(denom string)` (so you can do `std.CurrentRealm().Denom("bitcoin")` > > 2. Instead of modifying `denom`'s value in the native function, we should check it matches what we expect. ie. `strings.HasPrefix(denom, RealmDenom(std.CurrentRealm().PkgPath())`, then check the last part of the denom to see that it matches the Gno regex. (This can all be done in gno, without needing to put it in native code) > > Related with #1475 #1576 ------------------------- ## Solution BREAKING CHANGE: All previous realm calling IssueCoin or RemoveCoin are now expected to append the prefix "/" + realmPkgPath + ":" before the denom, it should be done by using ``std.CurrentRealm().CoinDenom(denom string)`` or by using ``std.CoinDenom(pkgPath, denom string)`` For now to avoid to mix coins and fix security issues like being able to issue coins from other realm, when a realm issue a coin, the pkg path of the realm is added as a prefix to the coin. the thing is some function expect only the base denom ``bitcoin`` (issue & remove) but the others like get require the qualified denom ``gno.land/r/demo/banktest:bitcoin``. it can be confusing I also answer the requirements of the comment @thehowl made: - Two functions are now available ``std.CoinDenom(pkgpath, demon string)`` && the method ``std.Realm.CoinDenom(denom string)`` - the denom's value is changed in the `.gno` file and not the native. Here is an example of how it looks like: ```go func IssueNewCoin(denom string, amount int64) string { std.AssertOriginCall() banker := std.GetBanker(std.BankerTypeRealmIssue) addr := std.PrevRealm().Addr() banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) return std.CurrentRealm().Denom(denom) } func RemoveCoin(denom string, amount int64) { std.AssertOriginCall() banker := std.GetBanker(std.BankerTypeRealmIssue) addr := std.PrevRealm().Addr() banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func GetCoins(denom string) uint64 { banker := std.GetBanker(std.BankerTypeReadonly) addr := std.PrevRealm().Addr() coins := banker.GetCoins(addr) for _, coin := range coins { if coin.Denom == std.CurrentRealm().CoinDenom(denom) { return uint64(coin.Amount) } } return 0 } ```
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan Bazalgette Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- docs/reference/stdlibs/std/banker.md | 4 ++ docs/reference/stdlibs/std/chain.md | 16 ++++++++ docs/reference/stdlibs/std/realm.md | 13 ++++++ .../gnoland/testdata/assertorigincall.txtar | 40 +++++++++---------- .../cmd/gnoland/testdata/grc20_registry.txtar | 8 ++-- gno.land/cmd/gnoland/testdata/prevrealm.txtar | 22 +++++----- .../realm_banker_issued_coin_denom.txtar | 40 ++++++++++++++++++- gno.land/pkg/gnoclient/integration_test.go | 14 +++---- gnovm/stdlibs/std/banker.gno | 32 +++++++++++++++ gnovm/stdlibs/std/banker.go | 30 +------------- gnovm/stdlibs/std/frame.gno | 12 ++++++ gnovm/tests/files/std5.gno | 2 +- gnovm/tests/files/std8.gno | 2 +- gnovm/tests/files/zrealm_natbind0.gno | 2 +- 14 files changed, 162 insertions(+), 75 deletions(-) diff --git a/docs/reference/stdlibs/std/banker.md b/docs/reference/stdlibs/std/banker.md index 71eb3709ea2..b60b55ee93b 100644 --- a/docs/reference/stdlibs/std/banker.md +++ b/docs/reference/stdlibs/std/banker.md @@ -38,6 +38,10 @@ Returns `Banker` of the specified type. ```go banker := std.GetBanker(std.) ``` + +:::info `Banker` methods expect qualified denomination of the coins. Read more [here](./realm.md#coindenom). +::: + --- ## GetCoins diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index b1791e65608..6a1da6483fd 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -162,3 +162,19 @@ Derives the Realm address from its `pkgpath` parameter. ```go realmAddr := std.DerivePkgAddr("gno.land/r/demo/tamagotchi") // g1a3tu874agjlkrpzt9x90xv3uzncapcn959yte4 ``` +--- + +## CoinDenom +```go +func CoinDenom(pkgPath, coinName string) string +``` +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. It can also be used as a method of the `Realm` object, Read more[here](./realm.md#coindenom). + +#### Parameters +- `pkgPath` **string** - package path of the realm +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +denom := std.CoinDenom("gno.land/r/demo/blog", "blgcoin") // /gno.land/r/demo/blog:blgcoin +``` diff --git a/docs/reference/stdlibs/std/realm.md b/docs/reference/stdlibs/std/realm.md index 0c99b7134ea..f69cd874c75 100644 --- a/docs/reference/stdlibs/std/realm.md +++ b/docs/reference/stdlibs/std/realm.md @@ -14,6 +14,7 @@ type Realm struct { func (r Realm) Addr() Address {...} func (r Realm) PkgPath() string {...} func (r Realm) IsUser() bool {...} +func (r Realm) CoinDenom(coinName string) string {...} ``` ## Addr @@ -39,3 +40,15 @@ Checks if the realm it was called upon is a user realm. ```go if r.IsUser() {...} ``` +--- +## CoinDenom +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. + +#### Parameters +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +// in "gno.land/r/gnoland/blog" +denom := r.CoinDenom("blgcoin") // /gno.land/r/gnoland/blog:blgcoin +``` diff --git a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar index 62d660a9215..1a5664d6bef 100644 --- a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar +++ b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar @@ -33,85 +33,85 @@ gnoland start # Test cases ## 1. MsgCall -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## 2. MsgCall -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 3. MsgCall -> myrlm.C: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 4. MsgCall -> r/foo.A -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## 5. MsgCall -> r/foo.B -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 6. MsgCall -> r/foo.C -> myrlm.C: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## remove due to update to maketx call can only call realm (case 7,8,9) ## 7. MsgCall -> p/demo/bar.A: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' ## 8. MsgCall -> p/demo/bar.B: PASS -## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' ## 9. MsgCall -> p/demo/bar.C: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' ## 10. MsgRun -> run.main -> myrlm.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno stderr 'invalid non-origin call' ## 11. MsgRun -> run.main -> myrlm.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno stdout 'OK!' ## 12. MsgRun -> run.main -> myrlm.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno stderr 'invalid non-origin call' ## 13. MsgRun -> run.main -> foo.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno stderr 'invalid non-origin call' ## 14. MsgRun -> run.main -> foo.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout 'OK!' ## 15. MsgRun -> run.main -> foo.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno stderr 'invalid non-origin call' ## 16. MsgRun -> run.main -> bar.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stderr 'invalid non-origin call' ## 17. MsgRun -> run.main -> bar.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout 'OK!' ## 18. MsgRun -> run.main -> bar.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno stderr 'invalid non-origin call' ## remove testcase 19 due to maketx call forced to call a realm ## 19. MsgCall -> std.AssertOriginCall: pass -## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' ## 20. MsgRun -> std.AssertOriginCall: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stderr 'invalid non-origin call' diff --git a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar index a5f7ad5eee3..417ab04539d 100644 --- a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar +++ b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar @@ -6,15 +6,15 @@ loadpkg gno.land/r/registry $WORK/registry gnoland start # we call Transfer with foo20, before it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 stdout 'not found' # add foo20, and foo20wrapper -gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 -gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 # we call Transfer with foo20, after it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 stdout 'same address, success!' -- registry/registry.gno -- diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar index 4a7cece6d62..58b0cdce1d6 100644 --- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar +++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar @@ -34,19 +34,19 @@ env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 # Test cases ## 1. MsgCall -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${USER_ADDR_test1} ## 2. MsgCall -> myrealm.B -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${USER_ADDR_test1} ## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${RFOO_ADDR} ## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${RFOO_ADDR} ## remove due to update to maketx call can only call realm (case 5, 6, 13) @@ -59,27 +59,27 @@ stdout ${RFOO_ADDR} ## stdout ${USER_ADDR_test1} ## 7. MsgRun -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno stdout ${USER_ADDR_test1} ## 8. MsgRun -> myrealm.B -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno stdout ${USER_ADDR_test1} ## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno stdout ${RFOO_ADDR} ## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout ${RFOO_ADDR} ## 11. MsgRun -> p/demo/bar.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stdout ${USER_ADDR_test1} ## 12. MsgRun -> p/demo/bar.B: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout ${USER_ADDR_test1} ## 13. MsgCall -> std.PrevRealm(): user address @@ -87,7 +87,7 @@ stdout ${USER_ADDR_test1} ## stdout ${USER_ADDR_test1} ## 14. MsgRun -> std.PrevRealm(): user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stdout ${USER_ADDR_test1} -- r/myrlm/myrlm.gno -- diff --git a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar index 71ef6400471..be9a686bac6 100644 --- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar +++ b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar @@ -12,6 +12,9 @@ gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker - ## add realm_banker with long package_name gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +## add invalid realm_denom +gnokey maketx addpkg -pkgdir $WORK/invalid_realm_denom -pkgpath gno.land/r/test/invalid_realm_denom -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + ## test2 spend all balance gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 @@ -52,6 +55,22 @@ gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456 gnokey query bank/balances/g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7 stdout '"100/gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890:ugnot"' +## mint invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## burn invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## mint invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + +## burn invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + -- short/realm_banker.gno -- package realm_banker @@ -61,12 +80,12 @@ import ( func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.IssueCoin(addr, denom, amount) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func Burn(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.RemoveCoin(addr, denom, amount) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } -- long/realm_banker.gno -- @@ -77,6 +96,23 @@ import ( "std" ) +func Mint(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +func Burn(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +-- invalid_realm_denom/realm_banker.gno -- +package invalid_realm_denom + +import ( + "std" +) + func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) banker.IssueCoin(addr, denom, amount) diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 0a06eb4756a..945121fbacf 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -40,7 +40,7 @@ func TestCallSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -93,7 +93,7 @@ func TestCallMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -155,7 +155,7 @@ func TestSendSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -219,7 +219,7 @@ func TestSendMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -291,7 +291,7 @@ func TestRunSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -452,7 +452,7 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -537,7 +537,7 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", diff --git a/gnovm/stdlibs/std/banker.gno b/gnovm/stdlibs/std/banker.gno index 5412b73281c..4c20e8d4b61 100644 --- a/gnovm/stdlibs/std/banker.gno +++ b/gnovm/stdlibs/std/banker.gno @@ -2,6 +2,7 @@ package std import ( "strconv" + "strings" ) // Realm functions can call std.GetBanker(options) to get @@ -126,6 +127,7 @@ func (b banker) IssueCoin(addr Address, denom string, amount int64) { if b.bt != BankerTypeRealmIssue { panic(b.bt.String() + " cannot issue coins") } + assertCoinDenom(denom) bankerIssueCoin(uint8(b.bt), string(addr), denom, amount) } @@ -133,5 +135,35 @@ func (b banker) RemoveCoin(addr Address, denom string, amount int64) { if b.bt != BankerTypeRealmIssue { panic(b.bt.String() + " cannot remove coins") } + assertCoinDenom(denom) bankerRemoveCoin(uint8(b.bt), string(addr), denom, amount) } + +func assertCoinDenom(denom string) { + prefix := "/" + CurrentRealm().PkgPath() + ":" + if !strings.HasPrefix(denom, prefix) { + panic("invalid denom, can only issue/remove coins with the realm's prefix: " + prefix) + } + + baseDenom := denom[len(prefix):] + if !isValidBaseDenom(baseDenom) { + panic("cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits") + } +} + +// check start by a lowercase letter and be followed by 2-15 lowercase letters or digits +func isValidBaseDenom(denom string) bool { + length := len(denom) + if length < 3 || length > 16 { + return false + } + for i, c := range denom { + switch { + case c >= 'a' && c <= 'z', + i > 0 && (c >= '0' && c <= '9'): // continue + default: + return false + } + } + return true +} diff --git a/gnovm/stdlibs/std/banker.go b/gnovm/stdlibs/std/banker.go index 892af94777f..c57ba8529ed 100644 --- a/gnovm/stdlibs/std/banker.go +++ b/gnovm/stdlibs/std/banker.go @@ -2,7 +2,6 @@ package std import ( "fmt" - "regexp" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -33,9 +32,6 @@ const ( btRealmIssue ) -// regexp for denom format -var reDenom = regexp.MustCompile("[a-z][a-z0-9]{2,15}") - func X_bankerGetCoins(m *gno.Machine, bt uint8, addr string) (denoms []string, amounts []int64) { coins := GetContext(m).Banker.GetCoins(crypto.Bech32Address(addr)) return ExpandCoins(coins) @@ -74,31 +70,9 @@ func X_bankerTotalCoin(m *gno.Machine, bt uint8, denom string) int64 { } func X_bankerIssueCoin(m *gno.Machine, bt uint8, addr string, denom string, amount int64) { - // gno checks for bt == RealmIssue - - // check origin denom format - matched := reDenom.MatchString(denom) - if !matched { - m.Panic(typedString("invalid denom format to issue coin, must be " + reDenom.String())) - return - } - - // Similar to ibc spec - // ibc_denom := 'ibc/' + hash('path' + 'base_denom') - // gno_realm_denom := '/' + 'pkg_path' + ':' + 'base_denom' - newDenom := "/" + m.Realm.Path + ":" + denom - GetContext(m).Banker.IssueCoin(crypto.Bech32Address(addr), newDenom, amount) + GetContext(m).Banker.IssueCoin(crypto.Bech32Address(addr), denom, amount) } func X_bankerRemoveCoin(m *gno.Machine, bt uint8, addr string, denom string, amount int64) { - // gno checks for bt == RealmIssue - - matched := reDenom.MatchString(denom) - if !matched { - m.Panic(typedString("invalid denom format to remove coin, must be " + reDenom.String())) - return - } - - newDenom := "/" + m.Realm.Path + ":" + denom - GetContext(m).Banker.RemoveCoin(crypto.Bech32Address(addr), newDenom, amount) + GetContext(m).Banker.RemoveCoin(crypto.Bech32Address(addr), denom, amount) } diff --git a/gnovm/stdlibs/std/frame.gno b/gnovm/stdlibs/std/frame.gno index bc3a000f5a0..1709f8cb8b5 100644 --- a/gnovm/stdlibs/std/frame.gno +++ b/gnovm/stdlibs/std/frame.gno @@ -16,3 +16,15 @@ func (r Realm) PkgPath() string { func (r Realm) IsUser() bool { return r.pkgPath == "" } + +func (r Realm) CoinDenom(coinName string) string { + return CoinDenom(r.pkgPath, coinName) +} + +func CoinDenom(pkgPath, coinName string) string { + // TODO: Possibly remove after https://github.com/gnolang/gno/issues/3164 + // Similar to ibc spec + // ibc_denom := 'ibc/' + hash('path' + 'base_denom') + // gno_qualified_denom := '/' + 'pkg_path' + ':' + 'base_denom' + return "/" + pkgPath + ":" + coinName +} diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 2baba6b5005..e339d7a6364 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,7 +13,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) // std/native.gno:45 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index 4f749c3a6e1..ee717bf16be 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,7 +23,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) // std/native.gno:45 diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index 16a374164d5..8e5f641e734 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -69,7 +69,7 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:8" +// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:9" // }, // "FileName": "native.gno", // "IsMethod": false, From c48219a1f3782d318e7753e1df7f1da0c5fd10c6 Mon Sep 17 00:00:00 2001 From: Nemanja Aleksic Date: Fri, 13 Dec 2024 07:12:06 +0100 Subject: [PATCH 327/345] docs: add bad contribution section (#3329) Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Morgan --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc125a6da73..b58d63c6c75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -469,6 +469,18 @@ Resources for idiomatic Go docs: - [godoc](https://go.dev/blog/godoc) - [Go Doc Comments](https://tip.golang.org/doc/comment) +## Avoding Unhelpful Contributions + +While we welcome all contributions to the Gno project, it's important to ensure that your changes provide meaningful value or improve the quality of the codebase. Contributions that fail to meet these criteria may not be accepted. Examples of unhelpful contributions include (but not limited to): + +- Airdrop farming & karma farming: Making minimal, superficial changes, with the goal of becoming eligible for airdrops and GovDAO participation. +- Incomplete submissions: Changes that lack adequate context, link to a related issue, documentation, or test coverage. + +Before submitting a pull request, ask yourself: +- Does this change solve a specific problem or add clear value? +- Is the implementation aligned with the gno.land's goals and style guide? +- Have I tested my changes and included relevant documentation? + ## Additional Notes ### Issue and Pull Request Labels @@ -502,3 +514,4 @@ automatic label management. | info needed | Issue is lacking information needed for resolving | | investigating | Issue is still being investigated by the team | | question | Issue starts a discussion or raises a question | + From 705f424470933b45e5666d5939845c2e8306a45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 16 Dec 2024 12:05:34 +0100 Subject: [PATCH 328/345] feat: datasource for `gno.land/r/leon/hof` integration with Gno.me (#3247) This is quick initial PoC of a Gno.me integration idea. --- examples/gno.land/r/leon/hof/datasource.gno | 77 +++++++++ .../gno.land/r/leon/hof/datasource_test.gno | 157 ++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 examples/gno.land/r/leon/hof/datasource.gno create mode 100644 examples/gno.land/r/leon/hof/datasource_test.gno diff --git a/examples/gno.land/r/leon/hof/datasource.gno b/examples/gno.land/r/leon/hof/datasource.gno new file mode 100644 index 00000000000..180c4880177 --- /dev/null +++ b/examples/gno.land/r/leon/hof/datasource.gno @@ -0,0 +1,77 @@ +package hof + +import ( + "errors" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" + "gno.land/p/jeronimoalbi/datasource" +) + +func NewDatasource() Datasource { + return Datasource{exhibition} +} + +type Datasource struct { + exhibition *Exhibition +} + +func (ds Datasource) Size() int { return ds.exhibition.itemsSorted.Size() } + +func (ds Datasource) Records(q datasource.Query) datasource.Iterator { + return &iterator{ + exhibition: ds.exhibition, + index: q.Offset, + maxIndex: q.Offset + q.Count, + } +} + +func (ds Datasource) Record(id string) (datasource.Record, error) { + v, found := ds.exhibition.itemsSorted.Get(id) + if !found { + return nil, errors.New("realm submission not found") + } + return record{v.(*Item)}, nil +} + +type record struct { + item *Item +} + +func (r record) ID() string { return r.item.id.String() } +func (r record) String() string { return r.item.pkgpath } + +func (r record) Fields() (datasource.Fields, error) { + fields := avl.NewTree() + fields.Set( + "details", + ufmt.Sprintf("Votes: ⏶ %d - ⏷ %d", r.item.upvote.Size(), r.item.downvote.Size()), + ) + return fields, nil +} + +func (r record) Content() (string, error) { + content := ufmt.Sprintf("# Submission #%d\n\n", int(r.item.id)) + content += r.item.Render(false) + return content, nil +} + +type iterator struct { + exhibition *Exhibition + index, maxIndex int + record *record +} + +func (it iterator) Record() datasource.Record { return it.record } +func (it iterator) Err() error { return nil } + +func (it *iterator) Next() bool { + if it.index >= it.maxIndex || it.index >= it.exhibition.itemsSorted.Size() { + return false + } + + _, v := it.exhibition.itemsSorted.GetByIndex(it.index) + it.record = &record{v.(*Item)} + it.index++ + return true +} diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno new file mode 100644 index 00000000000..376f981875f --- /dev/null +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -0,0 +1,157 @@ +package hof + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/jeronimoalbi/datasource" +) + +var ( + _ datasource.Datasource = (*Datasource)(nil) + _ datasource.Record = (*record)(nil) + _ datasource.ContentRecord = (*record)(nil) + _ datasource.Iterator = (*iterator)(nil) +) + +func TestDatasourceRecords(t *testing.T) { + cases := []struct { + name string + items []*Item + recordIDs []string + options []datasource.QueryOption + }{ + { + name: "all items", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000001", "0000002", "0000003"}, + }, + { + name: "with offset", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000002", "0000003"}, + options: []datasource.QueryOption{datasource.WithOffset(1)}, + }, + { + name: "with count", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000001", "0000002"}, + options: []datasource.QueryOption{datasource.WithCount(2)}, + }, + { + name: "with offset and count", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000002"}, + options: []datasource.QueryOption{ + datasource.WithOffset(1), + datasource.WithCount(1), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Initialize a local instance of exhibition + exhibition := &Exhibition{itemsSorted: avl.NewTree()} + for _, item := range tc.items { + exhibition.itemsSorted.Set(item.id.String(), item) + } + + // Get a records iterator + ds := Datasource{exhibition} + query := datasource.NewQuery(tc.options...) + iter := ds.Records(query) + + // Start asserting + urequire.Equal(t, len(tc.items), ds.Size(), "datasource size") + + var records []datasource.Record + for iter.Next() { + records = append(records, iter.Record()) + } + urequire.Equal(t, len(tc.recordIDs), len(records), "record count") + + for i, r := range records { + uassert.Equal(t, tc.recordIDs[i], r.ID()) + } + }) + } +} + +func TestDatasourceRecord(t *testing.T) { + cases := []struct { + name string + items []*Item + id string + err string + }{ + { + name: "found", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + id: "0000001", + }, + { + name: "no found", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + id: "42", + err: "realm submission not found", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Initialize a local instance of exhibition + exhibition := &Exhibition{itemsSorted: avl.NewTree()} + for _, item := range tc.items { + exhibition.itemsSorted.Set(item.id.String(), item) + } + + // Get a single record + ds := Datasource{exhibition} + r, err := ds.Record(tc.id) + + // Start asserting + if tc.err != "" { + uassert.ErrorContains(t, err, tc.err) + return + } + + urequire.NoError(t, err, "no error") + urequire.NotEqual(t, nil, r, "record not nil") + uassert.Equal(t, tc.id, r.ID()) + }) + } +} + +func TestItemRecord(t *testing.T) { + pkgpath := "gno.land/r/demo/test" + item := Item{ + id: 1, + pkgpath: pkgpath, + blockNum: 42, + upvote: avl.NewTree(), + downvote: avl.NewTree(), + } + item.downvote.Set("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", struct{}{}) + item.upvote.Set("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", struct{}{}) + item.upvote.Set("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", struct{}{}) + + r := record{&item} + + uassert.Equal(t, "0000001", r.ID()) + uassert.Equal(t, pkgpath, r.String()) + + fields, _ := r.Fields() + details, found := fields.Get("details") + urequire.True(t, found, "details field") + uassert.Equal(t, "Votes: ⏶ 2 - ⏷ 1", details) + + content, _ := r.Content() + wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" + + "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" + + "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land/r/demo/test) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land/r/demo/test)\n\n" + uassert.Equal(t, wantContent, content) +} From 4b0c341792ad50d4d86ab5ec6926d1309fe5dc9a Mon Sep 17 00:00:00 2001 From: Nathan Toups <612924+n2p5@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:00:27 -0700 Subject: [PATCH 329/345] feat(examples): add {p,r}/n2p5/loci (#3338) # loci (package and realm) This is a realm I've developed as part of a larger project I have in the works. While I have a specific purpose for it, the loci realm is free to be used by anyone who wants to have a mutable data store for placing a byte slice tied to their caller address. This can be useful for pointing to other immutable data. `loci` is a single purpose datastore keyed by the caller's address. It has two functions: Set and Get. loci is plural for locus, which is a central or core place where something is found or from which it originates. In this case, it's a simple key-value store where an address (the key) can store exactly one value (in the form of a byte slice). Only the caller can set the value for their address, but anyone can retrieve the value for any address. --- examples/gno.land/p/n2p5/loci/gno.mod | 1 + examples/gno.land/p/n2p5/loci/loci.gno | 44 +++++++++++ examples/gno.land/p/n2p5/loci/loci_test.gno | 84 +++++++++++++++++++++ examples/gno.land/r/n2p5/loci/gno.mod | 1 + examples/gno.land/r/n2p5/loci/loci.gno | 68 +++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 examples/gno.land/p/n2p5/loci/gno.mod create mode 100644 examples/gno.land/p/n2p5/loci/loci.gno create mode 100644 examples/gno.land/p/n2p5/loci/loci_test.gno create mode 100644 examples/gno.land/r/n2p5/loci/gno.mod create mode 100644 examples/gno.land/r/n2p5/loci/loci.gno diff --git a/examples/gno.land/p/n2p5/loci/gno.mod b/examples/gno.land/p/n2p5/loci/gno.mod new file mode 100644 index 00000000000..ec30d72d752 --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/loci diff --git a/examples/gno.land/p/n2p5/loci/loci.gno b/examples/gno.land/p/n2p5/loci/loci.gno new file mode 100644 index 00000000000..7bd5c29c3af --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/loci.gno @@ -0,0 +1,44 @@ +// loci is a single purpose datastore keyed by the caller's address. It has two +// functions: Set and Get. loci is plural for locus, which is a central or core +// place where something is found or from which it originates. In this case, +// it's a simple key-value store where an address (the key) can store exactly +// one value (in the form of a byte slice). Only the caller can set the value +// for their address, but anyone can retrieve the value for any address. +package loci + +import ( + "std" + + "gno.land/p/demo/avl" +) + +// LociStore is a simple key-value store that uses +// an AVL tree to store the data. +type LociStore struct { + internal *avl.Tree +} + +// New creates a reference to a new LociStore. +func New() *LociStore { + return &LociStore{ + internal: avl.NewTree(), + } +} + +// Set stores a byte slice in the AVL tree using the `std.PrevRealm().Addr()` +// string as the key. +func (s *LociStore) Set(value []byte) { + key := string(std.PrevRealm().Addr()) + s.internal.Set(key, value) +} + +// Get retrieves a byte slice from the AVL tree using the provided address. +// The return values are the byte slice value and a boolean indicating +// whether the value exists. +func (s *LociStore) Get(addr std.Address) []byte { + value, exists := s.internal.Get(string(addr)) + if !exists { + return nil + } + return value.([]byte) +} diff --git a/examples/gno.land/p/n2p5/loci/loci_test.gno b/examples/gno.land/p/n2p5/loci/loci_test.gno new file mode 100644 index 00000000000..bb216a8539e --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/loci_test.gno @@ -0,0 +1,84 @@ +package loci + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestLociStore(t *testing.T) { + t.Parallel() + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u1") + + t.Run("TestSet", func(t *testing.T) { + t.Parallel() + store := New() + u1 := testutils.TestAddress("u1") + + m1 := []byte("hello") + m2 := []byte("world") + std.TestSetOrigCaller(u1) + + // Ensure that the value is nil before setting it. + r1 := store.Get(u1) + if r1 != nil { + t.Errorf("expected value to be nil, got '%s'", r1) + } + store.Set(m1) + // Ensure that the value is correct after setting it. + r2 := store.Get(u1) + if string(r2) != "hello" { + t.Errorf("expected value to be 'hello', got '%s'", r2) + } + store.Set(m2) + // Ensure that the value is correct after overwriting it. + r3 := store.Get(u1) + if string(r3) != "world" { + t.Errorf("expected value to be 'world', got '%s'", r3) + } + }) + t.Run("TestGet", func(t *testing.T) { + t.Parallel() + store := New() + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + u3 := testutils.TestAddress("u3") + u4 := testutils.TestAddress("u4") + + m1 := []byte("hello") + m2 := []byte("world") + m3 := []byte("goodbye") + + std.TestSetOrigCaller(u1) + store.Set(m1) + std.TestSetOrigCaller(u2) + store.Set(m2) + std.TestSetOrigCaller(u3) + store.Set(m3) + + // Ensure that the value is correct after setting it. + r0 := store.Get(u4) + if r0 != nil { + t.Errorf("expected value to be nil, got '%s'", r0) + } + // Ensure that the value is correct after setting it. + r1 := store.Get(u1) + if string(r1) != "hello" { + t.Errorf("expected value to be 'hello', got '%s'", r1) + } + // Ensure that the value is correct after setting it. + r2 := store.Get(u2) + if string(r2) != "world" { + t.Errorf("expected value to be 'world', got '%s'", r2) + } + // Ensure that the value is correct after setting it. + r3 := store.Get(u3) + if string(r3) != "goodbye" { + t.Errorf("expected value to be 'goodbye', got '%s'", r3) + } + }) + +} diff --git a/examples/gno.land/r/n2p5/loci/gno.mod b/examples/gno.land/r/n2p5/loci/gno.mod new file mode 100644 index 00000000000..131e0d73467 --- /dev/null +++ b/examples/gno.land/r/n2p5/loci/gno.mod @@ -0,0 +1 @@ +module gno.land/r/n2p5/loci diff --git a/examples/gno.land/r/n2p5/loci/loci.gno b/examples/gno.land/r/n2p5/loci/loci.gno new file mode 100644 index 00000000000..36f282e729f --- /dev/null +++ b/examples/gno.land/r/n2p5/loci/loci.gno @@ -0,0 +1,68 @@ +package loci + +import ( + "encoding/base64" + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/n2p5/loci" +) + +var store *loci.LociStore + +func init() { + store = loci.New() +} + +// Set takes a base64 encoded string and stores it in the Loci store. +// Keyed by the address of the caller. It also emits a "set" event with +// the address of the caller. +func Set(value string) { + b, err := base64.StdEncoding.DecodeString(value) + if err != nil { + panic(err) + } + store.Set(b) + std.Emit("SetValue", "ForAddr", string(std.PrevRealm().Addr())) +} + +// Get retrieves the value stored at the provided address and +// returns it as a base64 encoded string. +func Get(addr std.Address) string { + return base64.StdEncoding.EncodeToString(store.Get(addr)) +} + +func Render(path string) string { + if path == "" { + return about + } + return renderGet(std.Address(path)) +} + +func renderGet(addr std.Address) string { + value := "```\n" + Get(addr) + "\n```" + + return ufmt.Sprintf(` +# Loci Value Viewer + +**Address:** %s + +%s + +`, addr, value) +} + +const about = ` +# Welcome to Loci + +Loci is a simple key-value store keyed by the caller's gno.land address. +Only the caller can set the value for their address, but anyone can +retrieve the value for any address. There are only two functions: Set and Get. +If you'd like to set a value, simply base64 encode any message you'd like and +it will be stored in in Loci. If you'd like to retrieve a value, simply provide +the address of the value you'd like to retrieve. + +For convenience, you can also use gnoweb to view the value for a given address, +if one exists. For instance append :g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t to +this URL to view the value stored at that address. +` From 3d431887e7ee426afe178f5dba91321edcd9e945 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:32:44 +0100 Subject: [PATCH 330/345] feat(gnoweb): rework & Implement new gnoweb design (#3195) address #3191 Reworking the `gnoweb` package: - Implement `gnoweb` new interface design(cc @alexiscolin). - Move Markdown rendering to the server to enhance speed and security. This change also simplifies the implementation of new components, making it more standardized as a Go library. - Aim to keep dependencies minimal, using only `goldmark` for Markdown and `chroma` for code highlighting, with almost no (in)direct dependencies. - Transition to Tailwind for simplicity and maintainability. - Retain all features from the previous `gnoweb` iteration. ### Preview - Home ![Screenshot 2024-11-25 at 19 39 54](https://github.com/user-attachments/assets/7a4b99d9-c223-49e7-9ae6-6561be85d1d3) - Source ![Screenshot 2024-11-25 at 19 41 25](https://github.com/user-attachments/assets/cb650eca-70d6-48f5-9c25-d247aecf45c3) - Docs ![Screenshot 2024-11-25 at 19 45 16](https://github.com/user-attachments/assets/1d79bb25-e431-42db-bc0e-0fdefca85339) ### TODO: - [x] port and adapt all previous tests to ensure compatibility (it should not take too long) - [x] Some cleanup and restructuring + linting. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: alexiscolin Co-authored-by: Morgan Bazalgette --- .github/workflows/gnoland.yml | 22 + Dockerfile | 1 - contribs/gnodev/cmd/gnodev/main.go | 48 +- contribs/gnodev/cmd/gnodev/setup_web.go | 29 +- contribs/gnodev/go.mod | 10 +- contribs/gnodev/go.sum | 28 +- gno.land/Makefile | 6 + gno.land/cmd/gnoweb/CONTRIBUTING.md | 20 - gno.land/cmd/gnoweb/README.md | 10 +- gno.land/cmd/gnoweb/main.go | 204 ++++- gno.land/cmd/gnoweb/main_test.go | 25 +- gno.land/pkg/gnoweb/.gitignore | 3 + gno.land/pkg/gnoweb/Makefile | 100 ++ gno.land/pkg/gnoweb/README.md | 45 + gno.land/pkg/gnoweb/alias.go | 36 +- gno.land/pkg/gnoweb/app.go | 152 +++ .../gnoweb/{gnoweb_test.go => app_test.go} | 74 +- gno.land/pkg/gnoweb/components/breadcrumb.go | 18 + .../pkg/gnoweb/components/breadcrumb.gohtml | 12 + gno.land/pkg/gnoweb/components/directory.go | 15 + .../pkg/gnoweb/components/directory.gohtml | 39 + gno.land/pkg/gnoweb/components/help.go | 51 ++ gno.land/pkg/gnoweb/components/help.gohtml | 100 ++ gno.land/pkg/gnoweb/components/index.go | 47 + gno.land/pkg/gnoweb/components/index.gohtml | 155 ++++ gno.land/pkg/gnoweb/components/logosvg.gohtml | 21 + gno.land/pkg/gnoweb/components/realm.go | 32 + gno.land/pkg/gnoweb/components/realm.gohtml | 37 + gno.land/pkg/gnoweb/components/redirect.go | 12 + .../pkg/gnoweb/components/redirect.gohtml | 16 + gno.land/pkg/gnoweb/components/source.go | 20 + gno.land/pkg/gnoweb/components/source.gohtml | 51 ++ .../pkg/gnoweb/components/spritesvg.gohtml | 134 +++ gno.land/pkg/gnoweb/components/status.gohtml | 12 + gno.land/pkg/gnoweb/components/template.go | 77 ++ gno.land/pkg/gnoweb/formatter.go | 25 + gno.land/pkg/gnoweb/frontend/css/input.css | 352 +++++++ gno.land/pkg/gnoweb/frontend/css/tx.config.js | 76 ++ gno.land/pkg/gnoweb/frontend/js/copy.ts | 103 +++ gno.land/pkg/gnoweb/frontend/js/index.ts | 42 + gno.land/pkg/gnoweb/frontend/js/realmhelp.ts | 125 +++ gno.land/pkg/gnoweb/frontend/js/searchbar.ts | 74 ++ .../img => frontend/static}/favicon.ico | Bin .../static/fonts/intervar/Inter.var.woff2 | Bin 0 -> 324864 bytes .../fonts/roboto/roboto-mono-normal.woff | Bin 0 -> 15832 bytes .../fonts/roboto/roboto-mono-normal.woff2 | Bin 0 -> 12764 bytes .../gnoweb/frontend/static/imgs/gnoland.svg | 4 + gno.land/pkg/gnoweb/gnoweb.go | 608 ------------ gno.land/pkg/gnoweb/handler.go | 381 ++++++++ gno.land/pkg/gnoweb/markdown/highlighting.go | 588 ++++++++++++ .../pkg/gnoweb/markdown/highlighting_test.go | 568 ++++++++++++ gno.land/pkg/gnoweb/markdown/toc.go | 137 +++ gno.land/pkg/gnoweb/public/favicon.ico | Bin 0 -> 7406 bytes .../public/fonts/intervar/Inter.var.woff2 | Bin 0 -> 324864 bytes .../fonts/roboto/roboto-mono-normal.woff | Bin 0 -> 15832 bytes .../fonts/roboto/roboto-mono-normal.woff2 | Bin 0 -> 12764 bytes gno.land/pkg/gnoweb/public/imgs/gnoland.svg | 4 + gno.land/pkg/gnoweb/public/js/copy.js | 1 + gno.land/pkg/gnoweb/public/js/index.js | 1 + gno.land/pkg/gnoweb/public/js/realmhelp.js | 1 + gno.land/pkg/gnoweb/public/js/searchbar.js | 1 + gno.land/pkg/gnoweb/public/styles.css | 3 + gno.land/pkg/gnoweb/static.go | 28 + gno.land/pkg/gnoweb/static/css/app.css | 862 ------------------ gno.land/pkg/gnoweb/static/css/normalize.css | 379 -------- gno.land/pkg/gnoweb/static/font/README.md | 5 - .../pkg/gnoweb/static/font/roboto/LICENSE.txt | 201 ---- .../static/font/roboto/RobotoMono-Bold.woff | Bin 65396 -> 0 bytes .../font/roboto/RobotoMono-BoldItalic.woff | Bin 72000 -> 0 bytes .../static/font/roboto/RobotoMono-Italic.woff | Bin 71476 -> 0 bytes .../static/font/roboto/RobotoMono-Light.woff | Bin 67428 -> 0 bytes .../font/roboto/RobotoMono-LightItalic.woff | Bin 72748 -> 0 bytes .../static/font/roboto/RobotoMono-Medium.woff | Bin 65392 -> 0 bytes .../font/roboto/RobotoMono-MediumItalic.woff | Bin 72168 -> 0 bytes .../font/roboto/RobotoMono-Regular.woff | Bin 65336 -> 0 bytes .../static/font/roboto/RobotoMono-Thin.woff | Bin 67976 -> 0 bytes .../font/roboto/RobotoMono-ThinItalic.woff | Bin 71144 -> 0 bytes .../gnoweb/static/img/apple-touch-icon.png | Bin 1502 -> 0 bytes .../pkg/gnoweb/static/img/favicon-16x16.png | Bin 172 -> 0 bytes .../pkg/gnoweb/static/img/favicon-32x32.png | Bin 317 -> 0 bytes .../gnoweb/static/img/github-mark-32px.png | Bin 1714 -> 0 bytes .../gnoweb/static/img/github-mark-64px.png | Bin 2625 -> 0 bytes .../pkg/gnoweb/static/img/ico-discord.svg | 3 - gno.land/pkg/gnoweb/static/img/ico-email.svg | 3 - .../pkg/gnoweb/static/img/ico-telegram.svg | 3 - .../pkg/gnoweb/static/img/ico-twitter.svg | 3 - .../pkg/gnoweb/static/img/ico-youtube.svg | 3 - gno.land/pkg/gnoweb/static/img/list-alt.png | Bin 232 -> 0 bytes gno.land/pkg/gnoweb/static/img/list.png | Bin 200 -> 0 bytes .../pkg/gnoweb/static/img/logo-square.png | Bin 13018 -> 0 bytes .../pkg/gnoweb/static/img/logo-square.svg | 6 - gno.land/pkg/gnoweb/static/img/logo-v1.png | Bin 11122 -> 0 bytes gno.land/pkg/gnoweb/static/img/og-gnoland.png | Bin 4739 -> 0 bytes .../gnoweb/static/img/safari-pinned-tab.svg | 29 - gno.land/pkg/gnoweb/static/invites.txt | 48 - .../pkg/gnoweb/static/js/highlight.min.js | 331 ------- gno.land/pkg/gnoweb/static/js/marked.min.js | 14 - gno.land/pkg/gnoweb/static/js/purify.min.js | 3 - gno.land/pkg/gnoweb/static/js/realm_help.js | 111 --- gno.land/pkg/gnoweb/static/js/renderer.js | 225 ----- gno.land/pkg/gnoweb/static/js/umbrella.js | 807 ---------------- gno.land/pkg/gnoweb/static/js/umbrella.min.js | 3 - gno.land/pkg/gnoweb/static/static.go | 8 - gno.land/pkg/gnoweb/status.go | 76 ++ .../pkg/gnoweb/tools/cmd/logname/colors.go | 60 ++ gno.land/pkg/gnoweb/tools/cmd/logname/main.go | 41 + gno.land/pkg/gnoweb/tools/go.mod | 25 + gno.land/pkg/gnoweb/tools/go.sum | 45 + gno.land/pkg/gnoweb/tools/tools.go | 5 + gno.land/pkg/gnoweb/url.go | 148 +++ gno.land/pkg/gnoweb/url_test.go | 135 +++ gno.land/pkg/gnoweb/views/404.html | 18 - gno.land/pkg/gnoweb/views/faucet.html | 139 --- gno.land/pkg/gnoweb/views/funcs.html | 337 ------- gno.land/pkg/gnoweb/views/generic.html | 24 - gno.land/pkg/gnoweb/views/package_dir.html | 37 - gno.land/pkg/gnoweb/views/package_file.html | 28 - gno.land/pkg/gnoweb/views/realm_help.html | 100 -- gno.land/pkg/gnoweb/views/realm_render.html | 40 - gno.land/pkg/gnoweb/views/redirect.html | 16 - gno.land/pkg/gnoweb/webclient.go | 127 +++ go.mod | 7 +- go.sum | 22 +- .../staging.gno.land/docker-compose.yml | 5 +- misc/loop/docker-compose.yml | 5 +- 125 files changed, 4696 insertions(+), 4575 deletions(-) delete mode 100644 gno.land/cmd/gnoweb/CONTRIBUTING.md create mode 100644 gno.land/pkg/gnoweb/.gitignore create mode 100644 gno.land/pkg/gnoweb/Makefile create mode 100644 gno.land/pkg/gnoweb/README.md create mode 100644 gno.land/pkg/gnoweb/app.go rename gno.land/pkg/gnoweb/{gnoweb_test.go => app_test.go} (67%) create mode 100644 gno.land/pkg/gnoweb/components/breadcrumb.go create mode 100644 gno.land/pkg/gnoweb/components/breadcrumb.gohtml create mode 100644 gno.land/pkg/gnoweb/components/directory.go create mode 100644 gno.land/pkg/gnoweb/components/directory.gohtml create mode 100644 gno.land/pkg/gnoweb/components/help.go create mode 100644 gno.land/pkg/gnoweb/components/help.gohtml create mode 100644 gno.land/pkg/gnoweb/components/index.go create mode 100644 gno.land/pkg/gnoweb/components/index.gohtml create mode 100644 gno.land/pkg/gnoweb/components/logosvg.gohtml create mode 100644 gno.land/pkg/gnoweb/components/realm.go create mode 100644 gno.land/pkg/gnoweb/components/realm.gohtml create mode 100644 gno.land/pkg/gnoweb/components/redirect.go create mode 100644 gno.land/pkg/gnoweb/components/redirect.gohtml create mode 100644 gno.land/pkg/gnoweb/components/source.go create mode 100644 gno.land/pkg/gnoweb/components/source.gohtml create mode 100644 gno.land/pkg/gnoweb/components/spritesvg.gohtml create mode 100644 gno.land/pkg/gnoweb/components/status.gohtml create mode 100644 gno.land/pkg/gnoweb/components/template.go create mode 100644 gno.land/pkg/gnoweb/formatter.go create mode 100644 gno.land/pkg/gnoweb/frontend/css/input.css create mode 100644 gno.land/pkg/gnoweb/frontend/css/tx.config.js create mode 100644 gno.land/pkg/gnoweb/frontend/js/copy.ts create mode 100644 gno.land/pkg/gnoweb/frontend/js/index.ts create mode 100644 gno.land/pkg/gnoweb/frontend/js/realmhelp.ts create mode 100644 gno.land/pkg/gnoweb/frontend/js/searchbar.ts rename gno.land/pkg/gnoweb/{static/img => frontend/static}/favicon.ico (100%) create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 create mode 100644 gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg delete mode 100644 gno.land/pkg/gnoweb/gnoweb.go create mode 100644 gno.land/pkg/gnoweb/handler.go create mode 100644 gno.land/pkg/gnoweb/markdown/highlighting.go create mode 100644 gno.land/pkg/gnoweb/markdown/highlighting_test.go create mode 100644 gno.land/pkg/gnoweb/markdown/toc.go create mode 100644 gno.land/pkg/gnoweb/public/favicon.ico create mode 100644 gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 create mode 100644 gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff create mode 100644 gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 create mode 100644 gno.land/pkg/gnoweb/public/imgs/gnoland.svg create mode 100644 gno.land/pkg/gnoweb/public/js/copy.js create mode 100644 gno.land/pkg/gnoweb/public/js/index.js create mode 100644 gno.land/pkg/gnoweb/public/js/realmhelp.js create mode 100644 gno.land/pkg/gnoweb/public/js/searchbar.js create mode 100644 gno.land/pkg/gnoweb/public/styles.css create mode 100644 gno.land/pkg/gnoweb/static.go delete mode 100644 gno.land/pkg/gnoweb/static/css/app.css delete mode 100644 gno.land/pkg/gnoweb/static/css/normalize.css delete mode 100644 gno.land/pkg/gnoweb/static/font/README.md delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/img/apple-touch-icon.png delete mode 100644 gno.land/pkg/gnoweb/static/img/favicon-16x16.png delete mode 100644 gno.land/pkg/gnoweb/static/img/favicon-32x32.png delete mode 100644 gno.land/pkg/gnoweb/static/img/github-mark-32px.png delete mode 100644 gno.land/pkg/gnoweb/static/img/github-mark-64px.png delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-discord.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-email.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-telegram.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-twitter.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-youtube.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/list-alt.png delete mode 100644 gno.land/pkg/gnoweb/static/img/list.png delete mode 100644 gno.land/pkg/gnoweb/static/img/logo-square.png delete mode 100644 gno.land/pkg/gnoweb/static/img/logo-square.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/logo-v1.png delete mode 100644 gno.land/pkg/gnoweb/static/img/og-gnoland.png delete mode 100644 gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg delete mode 100644 gno.land/pkg/gnoweb/static/invites.txt delete mode 100644 gno.land/pkg/gnoweb/static/js/highlight.min.js delete mode 100644 gno.land/pkg/gnoweb/static/js/marked.min.js delete mode 100644 gno.land/pkg/gnoweb/static/js/purify.min.js delete mode 100644 gno.land/pkg/gnoweb/static/js/realm_help.js delete mode 100644 gno.land/pkg/gnoweb/static/js/renderer.js delete mode 100644 gno.land/pkg/gnoweb/static/js/umbrella.js delete mode 100644 gno.land/pkg/gnoweb/static/js/umbrella.min.js delete mode 100644 gno.land/pkg/gnoweb/static/static.go create mode 100644 gno.land/pkg/gnoweb/status.go create mode 100644 gno.land/pkg/gnoweb/tools/cmd/logname/colors.go create mode 100644 gno.land/pkg/gnoweb/tools/cmd/logname/main.go create mode 100644 gno.land/pkg/gnoweb/tools/go.mod create mode 100644 gno.land/pkg/gnoweb/tools/go.sum create mode 100644 gno.land/pkg/gnoweb/tools/tools.go create mode 100644 gno.land/pkg/gnoweb/url.go create mode 100644 gno.land/pkg/gnoweb/url_test.go delete mode 100644 gno.land/pkg/gnoweb/views/404.html delete mode 100644 gno.land/pkg/gnoweb/views/faucet.html delete mode 100644 gno.land/pkg/gnoweb/views/funcs.html delete mode 100644 gno.land/pkg/gnoweb/views/generic.html delete mode 100644 gno.land/pkg/gnoweb/views/package_dir.html delete mode 100644 gno.land/pkg/gnoweb/views/package_file.html delete mode 100644 gno.land/pkg/gnoweb/views/realm_help.html delete mode 100644 gno.land/pkg/gnoweb/views/realm_render.html delete mode 100644 gno.land/pkg/gnoweb/views/redirect.html create mode 100644 gno.land/pkg/gnoweb/webclient.go diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 4817e2db0e3..59050f1baa4 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -16,3 +16,25 @@ jobs: tests-extra-args: "-coverpkg=github.com/gnolang/gno/gno.land/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} + + gnoweb_generate: + strategy: + fail-fast: false + matrix: + go-version: ["1.22.x"] + # unittests: TODO: matrix with contracts + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/setup-node@v4 + with: + node-version: lts/Jod # (22.x) https://github.com/nodejs/Release + - uses: actions/checkout@v4 + - run: | + make -C gno.land/pkg/gnoweb fclean generate + # Check if there are changes after running generate.gnoweb + git diff --exit-code || \ + (echo "\`gnoweb generate\` out of date, please run \`make gnoweb.generate\` within './gno.land'" && exit 1) diff --git a/Dockerfile b/Dockerfile index b858589640f..effc30ca32f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,6 @@ ENTRYPOINT ["/usr/bin/gno"] # gnoweb FROM base AS gnoweb COPY --from=build-gno /gnoroot/build/gnoweb /usr/bin/gnoweb -COPY --from=build-gno /opt/gno/src/gno.land/cmd/gnoweb /opt/gno/src/gnoweb EXPOSE 8888 ENTRYPOINT ["/usr/bin/gnoweb"] diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 082d0cb8270..95f1d95e0a6 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -57,9 +57,10 @@ type devCfg struct { txsFile string // Web Configuration + noWeb bool + webHTML bool webListenerAddr string webRemoteHelperAddr string - webWithHTML bool // Node Configuration minimal bool @@ -123,6 +124,20 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "gno root directory", ) + fs.BoolVar( + &c.noWeb, + "no-web", + defaultDevOptions.noWeb, + "disable gnoweb", + ) + + fs.BoolVar( + &c.webHTML, + "web-html", + defaultDevOptions.webHTML, + "gnoweb: enable unsafe HTML parsing in markdown rendering", + ) + fs.StringVar( &c.webListenerAddr, "web-listener", @@ -137,13 +152,6 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "gnoweb: web server help page's remote addr (default to )", ) - fs.BoolVar( - &c.webWithHTML, - "web-with-html", - defaultDevOptions.webWithHTML, - "gnoweb: enable HTML parsing in markdown rendering", - ) - fs.StringVar( &c.nodeRPCListenerAddr, "node-rpc-listener", @@ -323,7 +331,10 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { defer server.Close() // Setup gnoweb - webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + webhandler, err := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + if err != nil { + return fmt.Errorf("unable to setup gnoweb server: %w", err) + } // Setup unsafe APIs if enabled if cfg.unsafeAPI { @@ -351,14 +362,17 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { mux.Handle("/", webhandler) } - go func() { - err := server.ListenAndServe() - cancel(err) - }() + // Serve gnoweb + if !cfg.noWeb { + go func() { + err := server.ListenAndServe() + cancel(err) + }() - logger.WithGroup(WebLogName). - Info("gnoweb started", - "lisn", fmt.Sprintf("http://%s", server.Addr)) + logger.WithGroup(WebLogName). + Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", server.Addr)) + } watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) if err != nil { @@ -377,7 +391,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { return runEventLoop(ctx, logger, book, rt, devNode, watcher) } -var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: +var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev P Previous TX - Go to the previous tx diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index d55814142a6..e509768d2a1 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "net/http" @@ -9,19 +10,25 @@ import ( ) // setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) http.Handler { - webConfig := gnoweb.NewDefaultConfig() +func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (http.Handler, error) { + if cfg.noWeb { + return http.HandlerFunc(http.NotFound), nil + } + + remote := dnode.GetRemoteAddress() - webConfig.HelpChainID = cfg.chainId - webConfig.RemoteAddr = dnode.GetRemoteAddress() - webConfig.HelpRemote = cfg.webRemoteHelperAddr - webConfig.WithHTML = cfg.webWithHTML + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.UnsafeHTML = cfg.webHTML + appcfg.NodeRemote = remote + appcfg.ChainID = cfg.chainId + if cfg.webRemoteHelperAddr != "" { + appcfg.RemoteHelp = cfg.webRemoteHelperAddr + } - // If `HelpRemote` is empty default it to `RemoteAddr` - if webConfig.HelpRemote == "" { - webConfig.HelpRemote = webConfig.RemoteAddr + router, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to create router app: %w", err) } - app := gnoweb.MakeApp(logger, webConfig) - return app.Router + return router, nil } diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 2053a61db6c..3b895975950 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -27,7 +27,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect - github.com/alecthomas/chroma/v2 v2.8.0 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -48,7 +48,7 @@ require ( github.com/creack/pty v1.1.21 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -57,10 +57,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.2.1 // indirect - github.com/gotuna/gotuna v0.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -81,7 +77,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark v1.7.2 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index f9250d34462..bab6e5364e8 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -1,12 +1,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= -github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= -github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -91,8 +91,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -128,16 +128,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -233,8 +225,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= -github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= +github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/gno.land/Makefile b/gno.land/Makefile index 7b2afd5779f..075560f44a9 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -47,6 +47,12 @@ install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb install.gnokey:; go install ./cmd/gnokey +.PHONY: dev.gnoweb generate.gnoweb +dev.gnoweb: + make -C ./pkg/gnoweb dev +generate.gnoweb: + make -C ./pkg/gnoweb generate + .PHONY: fclean fclean: clean rm -rf gnoland-data genesis.json diff --git a/gno.land/cmd/gnoweb/CONTRIBUTING.md b/gno.land/cmd/gnoweb/CONTRIBUTING.md deleted file mode 100644 index 7d7663e8bf7..00000000000 --- a/gno.land/cmd/gnoweb/CONTRIBUTING.md +++ /dev/null @@ -1,20 +0,0 @@ -# gno.land Website - -The gno.land website has 3 main dependencies: - -1. [UmbrellaJs](https://umbrellajs.com/) for DOM operations -2. [MarkedJs](https://marked.js.org/) for Markdown to html compilation -3. [HighlightJs](https://highlightjs.org/) for golang syntax highlighting -4. [DOMPurify](https://github.com/cure53/DOMPurify) to sanitize html (and avoid xss) - -Some security considerations: -| | Umbrella Js | Marked Js | HighlightJs | DOMPurify | -|---|---|---|---|---| -| dependencies | 0 | 0 | 0 | 0 | -| sanitize content | | [no](https://marked.js.org/#usage) | [throws an error](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741) | [yes](https://github.com/cure53/DOMPurify#readme) | - -Best Practices: - -- **When using MarkedJs**: Always run the output of the marked compiler inside `DOMPurify.sanitize` before inserting it in the dom with `.innerHtml = `. -- **When using DOMPurify**: Preferably use `{ USE_PROFILES: { html: true } }` option to allow html only. Content passed in the sanitizer must not be modified afterwards, and must directly be inserted in the DOM with innerHtml. Do not call `DOMPurify.sanitize` with the output of a previous `DOMPurify.sanitize` to avoid any mutation XSS risks. -- **When using HighlightJs**: always configure it before with `hljs.configure({throwUnescapedHTML: true})` to throw before inserting html in the page if any unexpected html children are detected. The check is done [here](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741). diff --git a/gno.land/cmd/gnoweb/README.md b/gno.land/cmd/gnoweb/README.md index 6379d3f6c43..ccd538c8f70 100644 --- a/gno.land/cmd/gnoweb/README.md +++ b/gno.land/cmd/gnoweb/README.md @@ -2,12 +2,4 @@ The gno.land web interface. -Live demo: https://gno.land/ - -## Install `gnoweb` - -Install and run a local [`gnoland`](../gnoland) instance first. - - $> git clone git@github.com:gnolang/gno.git - $> cd ./gno/gno.land - $> make install.gnoweb +Live demo: [https://gno.land/](https://gno.land/) or using `gnodev` from the directory [gnodev](../../../contribs/gnodev). diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 5cec7257ebe..80a8667ae6b 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -1,61 +1,197 @@ package main import ( + "context" "flag" "fmt" + "net" "net/http" "os" "time" - // for static files "github.com/gnolang/gno/gno.land/pkg/gnoweb" "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/tm2/pkg/commands" + "go.uber.org/zap" "go.uber.org/zap/zapcore" - // for error types - // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) ) +type webCfg struct { + chainid string + remote string + remoteHelp string + bind string + faucetURL string + assetsDir string + analytics bool + json bool + html bool + verbose bool +} + +var defaultWebOptions = webCfg{ + chainid: "dev", + assetsDir: "public", + remote: "127.0.0.1:26657", + bind: ":8888", +} + func main() { - err := runMain(os.Args[1:]) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) - os.Exit(1) - } + var cfg webCfg + + stdio := commands.NewDefaultIO() + cmd := commands.NewCommand( + commands.Metadata{ + Name: "gnoweb", + ShortUsage: "gnoweb [flags] [path ...]", + ShortHelp: "runs gno.land web interface", + LongHelp: `gnoweb web interface`, + }, + &cfg, + func(ctx context.Context, args []string) error { + run, err := setupWeb(&cfg, args, stdio) + if err != nil { + return err + } + + return run() + }) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +func (c *webCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + defaultWebOptions.remote, + "remote gno.land node address", + ) + + fs.StringVar( + &c.remoteHelp, + "help-remote", + defaultWebOptions.remoteHelp, + "help page's remote address", + ) + + fs.StringVar( + &c.assetsDir, + "assets-dir", + defaultWebOptions.assetsDir, + "if not empty, will be use as assets directory", + ) + + fs.StringVar( + &c.chainid, + "help-chainid", + defaultWebOptions.chainid, + "Deprecated: use `chainid` instead", + ) + + fs.StringVar( + &c.chainid, + "chainid", + defaultWebOptions.chainid, + "target chain id", + ) + + fs.StringVar( + &c.bind, + "bind", + defaultWebOptions.bind, + "gnoweb listener", + ) + + fs.StringVar( + &c.faucetURL, + "faucet-url", + defaultWebOptions.faucetURL, + "The faucet URL will redirect the user when they access `/faucet`.", + ) + + fs.BoolVar( + &c.json, + "json", + defaultWebOptions.json, + "display log in json format", + ) + + fs.BoolVar( + &c.html, + "html", + defaultWebOptions.html, + "enable unsafe html", + ) + + fs.BoolVar( + &c.analytics, + "with-analytics", + defaultWebOptions.analytics, + "nable privacy-first analytics", + ) + + fs.BoolVar( + &c.verbose, + "v", + defaultWebOptions.verbose, + "verbose logging mode", + ) } -func runMain(args []string) error { - var ( - fs = flag.NewFlagSet("gnoweb", flag.ContinueOnError) - cfg = gnoweb.NewDefaultConfig() - bindAddress string - ) - fs.StringVar(&cfg.RemoteAddr, "remote", cfg.RemoteAddr, "remote gnoland node address") - fs.StringVar(&cfg.CaptchaSite, "captcha-site", cfg.CaptchaSite, "recaptcha site key (if empty, captcha are disabled)") - fs.StringVar(&cfg.FaucetURL, "faucet-url", cfg.FaucetURL, "faucet server URL") - fs.StringVar(&cfg.ViewsDir, "views-dir", cfg.ViewsDir, "views directory location") // XXX: replace with goembed - fs.StringVar(&cfg.HelpChainID, "help-chainid", cfg.HelpChainID, "help page's chainid") - fs.StringVar(&cfg.HelpRemote, "help-remote", cfg.HelpRemote, "help page's remote addr") - fs.BoolVar(&cfg.WithAnalytics, "with-analytics", cfg.WithAnalytics, "enable privacy-first analytics") - fs.StringVar(&bindAddress, "bind", "127.0.0.1:8888", "server listening address") - fs.BoolVar(&cfg.WithHTML, "with-html", cfg.WithHTML, "Enable HTML parsing in markdown rendering") - - if err := fs.Parse(args); err != nil { - return err +func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) { + // Setup logger + level := zapcore.InfoLevel + if cfg.verbose { + level = zapcore.DebugLevel + } + + var zapLogger *zap.Logger + if cfg.json { + zapLogger = log.NewZapJSONLogger(io.Out(), level) + } else { + zapLogger = log.NewZapConsoleLogger(io.Out(), level) } + defer zapLogger.Sync() - zapLogger := log.NewZapConsoleLogger(os.Stdout, zapcore.DebugLevel) logger := log.ZapLoggerToSlog(zapLogger) - logger.Info("Running", "listener", "http://"+bindAddress) + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.ChainID = cfg.chainid + appcfg.NodeRemote = cfg.remote + appcfg.RemoteHelp = cfg.remoteHelp + appcfg.Analytics = cfg.analytics + appcfg.UnsafeHTML = cfg.html + appcfg.FaucetURL = cfg.faucetURL + appcfg.AssetsDir = cfg.assetsDir + if appcfg.RemoteHelp == "" { + appcfg.RemoteHelp = appcfg.NodeRemote + } + + app, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to start gnoweb app: %w", err) + } + + bindaddr, err := net.ResolveTCPAddr("tcp", cfg.bind) + if err != nil { + return nil, fmt.Errorf("unable to resolve listener %q: %w", cfg.bind, err) + } + + logger.Info("Running", "listener", bindaddr.String()) + server := &http.Server{ - Addr: bindAddress, + Handler: app, + Addr: bindaddr.String(), ReadHeaderTimeout: 60 * time.Second, - Handler: gnoweb.MakeApp(logger, cfg).Router, } - if err := server.ListenAndServe(); err != nil { - logger.Error("HTTP server stopped", " error:", err) - } + return func() error { + if err := server.ListenAndServe(); err != nil { + logger.Error("HTTP server stopped", " error:", err) + return commands.ExitCodeError(1) + } - return zapLogger.Sync() + return nil + }, nil } diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 640c4763140..37006c18c93 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -1,14 +1,25 @@ package main import ( - "errors" - "flag" + "os" "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" ) -func TestFlagHelp(t *testing.T) { - err := runMain([]string{"-h"}) - if !errors.Is(err, flag.ErrHelp) { - t.Errorf("should display usage") - } +func TestSetupWeb(t *testing.T) { + opts := defaultWebOptions + opts.bind = "127.0.0.1:0" // random port + stdio := commands.NewDefaultIO() + + // Open /dev/null as a write-only file + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0o644) + require.NoError(t, err) + defer devNull.Close() + + stdio.SetOut(devNull) + + _, err = setupWeb(&opts, []string{}, stdio) + require.NoError(t, err) } diff --git a/gno.land/pkg/gnoweb/.gitignore b/gno.land/pkg/gnoweb/.gitignore new file mode 100644 index 00000000000..dd09eb49099 --- /dev/null +++ b/gno.land/pkg/gnoweb/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +tmp/ +.cache diff --git a/gno.land/pkg/gnoweb/Makefile b/gno.land/pkg/gnoweb/Makefile new file mode 100644 index 00000000000..61397fef54f --- /dev/null +++ b/gno.land/pkg/gnoweb/Makefile @@ -0,0 +1,100 @@ +# Configurable arguments +DEV_REMOTE ?= 127.0.0.1:26657 +CHAIN_ID ?= test3 +PUBLIC_DIR ?= public + +# Variable Declarations +tools_run := go run -modfile ./tools/go.mod +run_reflex := $(tools_run) github.com/cespare/reflex +run_logname := go -C ./tools run ./cmd/logname + +# css config +input_css := frontend/css/input.css +output_css := $(PUBLIC_DIR)/styles.css +tw_version := 3.4.14 +tw_config_path := frontend/css/tx.config.js + +# static config +src_dir_static := frontend/static +out_dir_static := $(PUBLIC_DIR) +input_static := $(shell find $(src_dir_static) -type f) +output_static := $(patsubst $(src_dir_static)/%, $(out_dir_static)/%, $(input_static)) + +# esbuild config +src_dir_js := frontend/js +out_dir_js := $(PUBLIC_DIR)/js +input_js := $(shell find $(src_dir_js) -name '*.ts') +output_js := $(patsubst $(src_dir_js)/%.ts,$(out_dir_js)/%.js,$(input_js)) +esbuild_version := 0.24.0 + +# cache +cache_dir := .cache + +############# +# Targets +############# +.PHONY: all generate fmt css ts + +# Install dependencies +all: generate + +# Generate process +generate: css ts static + +css: $(output_css) +$(output_css): $(input_css) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) -i $< -o $@ --minify # tailwind + touch $@ + +ts: $(output_js) +$(out_dir_js)/%.js: $(src_dir_js)/%.ts + npx -y esbuild $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --minify + +# Rule to copy static files while preserving directory structure +static: $(output_static) +$(out_dir_static)/%: $(src_dir_static)/% + @mkdir -p $(dir $@) + @cp -v $< $@ + +# Format process +fmt: + go fmt ./... + + ############################### + # Developments + ############################### +.PHONY: dev dev.server dev.css dev.ts deps + +# Run the development dependencies in parallel +dev: + @echo "-- starting development tools" + @PUBLIC_DIR=$(cache_dir)/public $(MAKE) -j 3 \ + dev.gnoweb \ + dev.ts \ + dev.css + +# Go server in development mode +dev.gnoweb: generate + $(run_reflex) -s -r '.*\.go(html)?' -- \ + go run ../../cmd/gnoweb -assets-dir=${PUBLIC_DIR} -chainid=${CHAIN_ID} -remote=${DEV_REMOTE} \ + 2>&1 | $(run_logname) gnoweb + +# Tailwind CSS in development mode +dev.css: generate | $(PUBLIC_DIR) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) --verbose -i $(input_css) -o $(output_css) --watch \ + 2>&1 | $(run_logname) tailwind + +# XXX: add versioning on esbuild +# TS in development mode +dev.ts: generate | $(PUBLIC_DIR) + npx -y esbuild@$(esbuild_version) $(input_js) --bundle --outdir=$(out_dir_js) --sourcemap --format=esm --watch \ + 2>&1 | $(run_logname) esbuild + +# Cleanup +clean: + rm -rf $(cache_dir) tmp +fclean: clean + rm -rf $(PUBLIC_DIR) + +# Dirs +$(PUBLIC_DIR):; mkdir -p $@ diff --git a/gno.land/pkg/gnoweb/README.md b/gno.land/pkg/gnoweb/README.md new file mode 100644 index 00000000000..287279538d8 --- /dev/null +++ b/gno.land/pkg/gnoweb/README.md @@ -0,0 +1,45 @@ +# gnoweb + +`gnoweb` is a universal web frontend for the gno.land blockchain. + +This README provides instructions on how to set up and run `gnoweb` for development purposes. + +## Prerequisites + +Before you begin, ensure you have the following software installed on your machine: + +- **Node.js**: Required for running JavaScript and CSS build tools. +- **Go**: Required for building `gnoweb` + +## Development + +To start the development environment, which runs multiple development tools in parallel, +use the following command: + +```sh +make dev +``` + +This will: + +- Start a Go server in development mode and watch for any Go files change (targeting [localhost](http://localhost:8888)). +- Enable Tailwind CSS in watch mode to automatically compile CSS changes. +- Use esbuild in watch mode to automatically transpile and bundle TypeScript changes. + +You can customize the behavior of the Go server using the `DEV_REMOTE` and +`CHAIN_ID` environment variables. For example, to use `portal-loop` as the +target, run: + +```sh +CHAIN_ID=portal-loop DEV_REMOTE=https://rpc.gno.land make dev +``` + +## Generate + +To generate the public assets for the project, including static assets (fonts, CSS and JavaScript... +files), run the following command. This should be used while editing CSS, JS, or +any asset files: + +```sh +make generate +``` diff --git a/gno.land/pkg/gnoweb/alias.go b/gno.land/pkg/gnoweb/alias.go index 7fb28d5cbc3..06bb3941e41 100644 --- a/gno.land/pkg/gnoweb/alias.go +++ b/gno.land/pkg/gnoweb/alias.go @@ -1,6 +1,12 @@ package gnoweb -// realm aliases +import ( + "net/http" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" +) + +// Aliases are gnoweb paths that are rewritten using [AliasAndRedirectMiddleware]. var Aliases = map[string]string{ "/": "/r/gnoland/home", "/about": "/r/gnoland/pages:p/about", @@ -14,7 +20,7 @@ var Aliases = map[string]string{ "/events": "/r/gnoland/events", } -// http redirects +// Redirect are gnoweb paths that are redirected using [AliasAndRedirectMiddleware]. var Redirects = map[string]string{ "/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary "/blog": "/r/gnoland/blog", @@ -23,5 +29,29 @@ var Redirects = map[string]string{ "/grants": "/partners", "/language": "/gnolang", "/getting-started": "/start", - "/gophercon24": "https://docs.gno.land", +} + +// AliasAndRedirectMiddleware redirects all incoming requests whose path matches +// any of the [Redirects] to the corresponding URL; and rewrites the URL path +// for incoming requests which match any of the [Aliases]. +func AliasAndRedirectMiddleware(next http.Handler, analytics bool) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request path matches a redirect + if newPath, ok := Redirects[r.URL.Path]; ok { + http.Redirect(w, r, newPath, http.StatusFound) + components.RenderRedirectComponent(w, components.RedirectData{ + To: newPath, + WithAnalytics: analytics, + }) + return + } + + // Check if the request path matches an alias + if newPath, ok := Aliases[r.URL.Path]; ok { + r.URL.Path = newPath + } + + // Call the next handler + next.ServeHTTP(w, r) + }) } diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go new file mode 100644 index 00000000000..dc13253468e --- /dev/null +++ b/gno.land/pkg/gnoweb/app.go @@ -0,0 +1,152 @@ +package gnoweb + +import ( + "fmt" + "log/slog" + "net/http" + "path" + "strings" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/styles" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/yuin/goldmark" + mdhtml "github.com/yuin/goldmark/renderer/html" +) + +// AppConfig contains configuration for the gnoweb. +type AppConfig struct { + // UnsafeHTML, if enabled, allows to use HTML in the markdown. + UnsafeHTML bool + // Analytics enables SimpleAnalytics. + Analytics bool + // NodeRemote is the remote address of the gno.land node. + NodeRemote string + // RemoteHelp is the remote of the gno.land node, as used in the help page. + RemoteHelp string + // ChainID is the chain id, used for constructing the help page. + ChainID string + // AssetsPath is the base path to the gnoweb assets. + AssetsPath string + // AssetDir, if set, will be used for assets instead of the embedded public directory + AssetsDir string + // FaucetURL, if specified, will be the URL to which `/faucet` redirects. + FaucetURL string +} + +// NewDefaultAppConfig returns a new default [AppConfig]. The default sets +// 127.0.0.1:26657 as the remote node, "dev" as the chain ID and sets up Assets +// to be served on /public/. +func NewDefaultAppConfig() *AppConfig { + const defaultRemote = "127.0.0.1:26657" + + return &AppConfig{ + // same as Remote by default + NodeRemote: defaultRemote, + RemoteHelp: defaultRemote, + ChainID: "dev", + AssetsPath: "/public/", + } +} + +var chromaStyle = mustGetStyle("friendly") + +func mustGetStyle(name string) *chroma.Style { + s := styles.Get(name) + if s == nil { + panic("unable to get chroma style") + } + return s +} + +// NewRouter initializes the gnoweb router, with the given logger and config. +func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { + chromaOptions := []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + chromahtml.WithLinkableLineNumbers(true, "L"), + chromahtml.WithClasses(true), + chromahtml.ClassPrefix("chroma-"), + } + + mdopts := []goldmark.Option{ + goldmark.WithExtensions( + markdown.NewHighlighting( + markdown.WithFormatOptions(chromaOptions...), + ), + ), + } + if cfg.UnsafeHTML { + mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe())) + } + + md := goldmark.New(mdopts...) + + client, err := client.NewHTTPClient(cfg.NodeRemote) + if err != nil { + return nil, fmt.Errorf("unable to create http client: %w", err) + } + webcli := NewWebClient(logger, client, md) + + formatter := chromahtml.New(chromaOptions...) + chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css") + + var webConfig WebHandlerConfig + + webConfig.RenderClient = webcli + webConfig.Formatter = newFormatterWithStyle(formatter, chromaStyle) + + // Static meta + webConfig.Meta.AssetsPath = cfg.AssetsPath + webConfig.Meta.ChromaPath = chromaStylePath + webConfig.Meta.RemoteHelp = cfg.RemoteHelp + webConfig.Meta.ChainId = cfg.ChainID + webConfig.Meta.Analytics = cfg.Analytics + + // Setup main handler + webhandler := NewWebHandler(logger, webConfig) + + mux := http.NewServeMux() + + // Setup Webahndler along Alias Middleware + mux.Handle("/", AliasAndRedirectMiddleware(webhandler, cfg.Analytics)) + + // Register faucet URL to `/faucet` if specified + if cfg.FaucetURL != "" { + mux.Handle("/faucet", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, cfg.FaucetURL, http.StatusFound) + components.RenderRedirectComponent(w, components.RedirectData{ + To: cfg.FaucetURL, + WithAnalytics: cfg.Analytics, + }) + })) + } + + // setup assets + mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Setup Formatter + w.Header().Set("Content-Type", "text/css") + if err := formatter.WriteCSS(w, chromaStyle); err != nil { + logger.Error("unable to write css", "err", err) + http.NotFound(w, r) + } + })) + + // Normalize assets path + assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/" + + // Handle assets path + if cfg.AssetsDir != "" { + logger.Debug("using assets dir instead of embed assets", "dir", cfg.AssetsDir) + mux.Handle(assetsBase, DevAssetHandler(assetsBase, cfg.AssetsDir)) + } else { + mux.Handle(assetsBase, AssetHandler()) + } + + // Handle status page + mux.Handle("/status.json", handlerStatusJSON(logger, client)) + + return mux, nil +} diff --git a/gno.land/pkg/gnoweb/gnoweb_test.go b/gno.land/pkg/gnoweb/app_test.go similarity index 67% rename from gno.land/pkg/gnoweb/gnoweb_test.go rename to gno.land/pkg/gnoweb/app_test.go index 99eb86ea07e..78fe197a134 100644 --- a/gno.land/pkg/gnoweb/gnoweb_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -4,13 +4,13 @@ import ( "fmt" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gotuna/gotuna/test/assert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRoutes(t *testing.T) { @@ -27,12 +27,12 @@ func TestRoutes(t *testing.T) { {"/", ok, "Welcome"}, // assert / gives 200 (OK). assert / contains "Welcome". {"/about", ok, "blockchain"}, {"/r/gnoland/blog", ok, ""}, // whatever content - {"/r/gnoland/blog$help", ok, "exposed"}, + {"/r/gnoland/blog$help", ok, "AdminSetAdminAddr"}, {"/r/gnoland/blog/", ok, "admin.gno"}, - {"/r/gnoland/blog/admin.gno", ok, "func "}, - {"/r/gnoland/blog$help&func=Render", ok, "Render(...)"}, - {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `input type="text" value="foo/bar"`}, - {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, + {"/r/gnoland/blog/admin.gno", ok, ">func<"}, + {"/r/gnoland/blog$help&func=Render", ok, "Render(path)"}, + {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `value="foo/bar"`}, + // {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, // XXX(TODO) {"/r/demo/users:administrator", ok, "address"}, {"/r/demo/users", ok, "moul"}, {"/r/demo/users/users.gno", ok, "// State"}, @@ -40,18 +40,18 @@ func TestRoutes(t *testing.T) { {"/r/demo/deep/very/deep?arg1=val1&arg2=val2", ok, "hi ?arg1=val1&arg2=val2"}, {"/r/demo/deep/very/deep:bob", ok, "hi bob"}, {"/r/demo/deep/very/deep:bob?arg1=val1&arg2=val2", ok, "hi bob?arg1=val1&arg2=val2"}, - {"/r/demo/deep/very/deep$help", ok, "exposed"}, + {"/r/demo/deep/very/deep$help", ok, "Render"}, {"/r/demo/deep/very/deep/", ok, "render.gno"}, - {"/r/demo/deep/very/deep/render.gno", ok, "func Render("}, + {"/r/demo/deep/very/deep/render.gno", ok, ">package<"}, {"/contribute", ok, "Game of Realms"}, {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, - {"/404-not-found", notFound, "/404-not-found"}, - {"/아스키문자가아닌경로", notFound, "/아스키문자가아닌경로"}, - {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, "/테스트"}, - {"/グノー", notFound, "/グノー"}, - {"/⚛️", notFound, "/⚛️"}, + {"/404/not/found/", notFound, ""}, + {"/아스키문자가아닌경로", notFound, ""}, + {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, ""}, + {"/グノー", notFound, ""}, + {"/⚛️", notFound, ""}, {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"}, } @@ -61,20 +61,21 @@ func TestRoutes(t *testing.T) { node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) defer node.Stop() - cfg := NewDefaultConfig() + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr logger := log.NewTestingLogger(t) // set the `remoteAddr` of the client to the listening address of the // node, which is randomly assigned. - cfg.RemoteAddr = remoteAddr - app := MakeApp(logger, cfg) + router, err := NewRouter(logger, cfg) + require.NoError(t, err) for _, r := range routes { t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, r.route, nil) response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) + router.ServeHTTP(response, request) assert.Equal(t, r.status, response.Code) assert.Contains(t, response.Body.String(), r.substring) }) @@ -110,34 +111,39 @@ func TestAnalytics(t *testing.T) { node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) defer node.Stop() - cfg := NewDefaultConfig() - cfg.RemoteAddr = remoteAddr - - logger := log.NewTestingLogger(t) - - t.Run("with", func(t *testing.T) { + t.Run("enabled", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { - ccfg := cfg // clone config - ccfg.WithAnalytics = true - app := MakeApp(logger, ccfg) + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Analytics = true + logger := log.NewTestingLogger(t) + + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) + router.ServeHTTP(response, request) + fmt.Println("HELLO:", response.Body.String()) assert.Contains(t, response.Body.String(), "sa.gno.services") }) } }) - t.Run("without", func(t *testing.T) { + t.Run("disabled", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { - ccfg := cfg // clone config - ccfg.WithAnalytics = false - app := MakeApp(logger, ccfg) + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Analytics = false + logger := log.NewTestingLogger(t) + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) - assert.Equal(t, strings.Contains(response.Body.String(), "sa.gno.services"), false) + router.ServeHTTP(response, request) + assert.NotContains(t, response.Body.String(), "sa.gno.services") }) } }) diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.go b/gno.land/pkg/gnoweb/components/breadcrumb.go new file mode 100644 index 00000000000..9e7a97b2fae --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.go @@ -0,0 +1,18 @@ +package components + +import ( + "io" +) + +type BreadcrumbPart struct { + Name string + Path string +} + +type BreadcrumbData struct { + Parts []BreadcrumbPart +} + +func RenderBreadcrumpComponent(w io.Writer, data BreadcrumbData) error { + return tmpl.ExecuteTemplate(w, "Breadcrumb", data) +} diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.gohtml b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml new file mode 100644 index 00000000000..a3301cb037e --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml @@ -0,0 +1,12 @@ +{{ define "breadcrumb" }} +
    + {{- range $index, $part := .Parts }} + {{- if $index }} +
  1. + {{- else }} +
  2. + {{- end }} + {{ $part.Name }}
  3. + {{- end }} +
+{{ end }} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/components/directory.go b/gno.land/pkg/gnoweb/components/directory.go new file mode 100644 index 00000000000..6e47db3b2c4 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.go @@ -0,0 +1,15 @@ +package components + +import ( + "io" +) + +type DirData struct { + PkgPath string + Files []string + FileCounter int +} + +func RenderDirectoryComponent(w io.Writer, data DirData) error { + return tmpl.ExecuteTemplate(w, "renderDir", data) +} diff --git a/gno.land/pkg/gnoweb/components/directory.gohtml b/gno.land/pkg/gnoweb/components/directory.gohtml new file mode 100644 index 00000000000..4cdeff12a38 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.gohtml @@ -0,0 +1,39 @@ +{{ define "renderDir" }} +
+
+ + + {{ $pkgpath := .PkgPath }} +
+
+
+

{{ $pkgpath }}

+
+
+ Directory · {{ .FileCounter }} Files +
+
+ +
+ +
+
+
+ +
+{{ end }} + diff --git a/gno.land/pkg/gnoweb/components/help.go b/gno.land/pkg/gnoweb/components/help.go new file mode 100644 index 00000000000..e819705006b --- /dev/null +++ b/gno.land/pkg/gnoweb/components/help.go @@ -0,0 +1,51 @@ +package components + +import ( + "html/template" + "io" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types +) + +type HelpData struct { + // Selected function + SelectedFunc string + SelectedArgs map[string]string + + RealmName string + Functions []vm.FunctionSignature + ChainId string + Remote string + PkgPath string +} + +func registerHelpFuncs(funcs template.FuncMap) { + funcs["helpFuncSignature"] = func(fsig vm.FunctionSignature) (string, error) { + var fsigStr strings.Builder + + fsigStr.WriteString(fsig.FuncName) + fsigStr.WriteRune('(') + for i, param := range fsig.Params { + if i > 0 { + fsigStr.WriteString(", ") + } + fsigStr.WriteString(param.Name) + } + fsigStr.WriteRune(')') + + return fsigStr.String(), nil + } + + funcs["getSelectedArgValue"] = func(data HelpData, param vm.NamedType) (string, error) { + if data.SelectedArgs == nil { + return "", nil + } + + return data.SelectedArgs[param.Name], nil + } +} + +func RenderHelpComponent(w io.Writer, data HelpData) error { + return tmpl.ExecuteTemplate(w, "renderHelp", data) +} diff --git a/gno.land/pkg/gnoweb/components/help.gohtml b/gno.land/pkg/gnoweb/components/help.gohtml new file mode 100644 index 00000000000..dea4f683a0a --- /dev/null +++ b/gno.land/pkg/gnoweb/components/help.gohtml @@ -0,0 +1,100 @@ +{{ define "renderHelp" }} + {{ $data := . }} +
+
+
+
+

{{ .RealmName }}

+
+
+
+ +
+
+ + +
+
+
+ +
+ + {{ range .Functions }} +
+

{{ .FuncName }}

+
+
+

Params

+
+ {{ $funcName := .FuncName }} + {{ range .Params }} +
+
+ + +
+
+ {{ end }} +
+
+
+
+

Command

+
+ +
gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .FuncName }}" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid "{{ $.ChainId }}"{{ range .Params }} -args ""{{ end }} -remote "{{ $.Remote }}" ADDRESS
+
+
+
+ {{ end }} + +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/index.go b/gno.land/pkg/gnoweb/components/index.go new file mode 100644 index 00000000000..0cc020ae261 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/index.go @@ -0,0 +1,47 @@ +package components + +import ( + "context" + "html/template" + "io" + "net/url" +) + +type HeadData struct { + Title string + Description string + Canonical string + Image string + URL string + ChromaPath string + AssetsPath string + Analytics bool +} + +type HeaderData struct { + RealmPath string + Breadcrumb BreadcrumbData + WebQuery url.Values +} + +type FooterData struct { + Analytics bool + AssetsPath string +} + +type IndexData struct { + HeadData + HeaderData + FooterData + Body template.HTML +} + +func IndexComponent(data IndexData) Component { + return func(ctx context.Context, tmpl *template.Template, w io.Writer) error { + return tmpl.ExecuteTemplate(w, "index", data) + } +} + +func RenderIndexComponent(w io.Writer, data IndexData) error { + return tmpl.ExecuteTemplate(w, "index", data) +} diff --git a/gno.land/pkg/gnoweb/components/index.gohtml b/gno.land/pkg/gnoweb/components/index.gohtml new file mode 100644 index 00000000000..19fd1e21a6f --- /dev/null +++ b/gno.land/pkg/gnoweb/components/index.gohtml @@ -0,0 +1,155 @@ +{{ define "index" }} + + {{ template "head" .HeadData }} + + {{ template "spritesvg" }} + + + {{ template "header" .HeaderData }} + + + {{ template "main" .Body }} + + + {{ template "footer" .FooterData }} + + +{{ end }} + +{{ define "head" }} + + + + {{ .Title }} + + + + + + {{ if .Canonical }} + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ end }} + +{{ define "header" }} +
+ +
+{{ end }} + +{{ define "main" }} + {{ . }} +{{ end }} + +{{ define "footer" }} + + +{{- if .Analytics -}} {{- template "analytics" }} {{- end -}} + +{{- end }} + +{{- define "analytics" -}} + + + +{{- end -}} diff --git a/gno.land/pkg/gnoweb/components/logosvg.gohtml b/gno.land/pkg/gnoweb/components/logosvg.gohtml new file mode 100644 index 00000000000..5ebe6460ee3 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/logosvg.gohtml @@ -0,0 +1,21 @@ +{{ define "logosvg" }} + + + + + + + + + + + + + + + + + + + +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/realm.go b/gno.land/pkg/gnoweb/components/realm.go new file mode 100644 index 00000000000..027760bb382 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/realm.go @@ -0,0 +1,32 @@ +package components + +import ( + "context" + "html/template" + "io" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" +) + +type RealmTOCData struct { + Items []*markdown.TocItem +} + +func RealmTOCComponent(data *RealmTOCData) Component { + return func(ctx context.Context, tmpl *template.Template, w io.Writer) error { + return tmpl.ExecuteTemplate(w, "renderRealmToc", data) + } +} + +func RenderRealmTOCComponent(w io.Writer, data *RealmTOCData) error { + return tmpl.ExecuteTemplate(w, "renderRealmToc", data) +} + +type RealmData struct { + Content template.HTML + TocItems *RealmTOCData +} + +func RenderRealmComponent(w io.Writer, data RealmData) error { + return tmpl.ExecuteTemplate(w, "renderRealm", data) +} diff --git a/gno.land/pkg/gnoweb/components/realm.gohtml b/gno.land/pkg/gnoweb/components/realm.gohtml new file mode 100644 index 00000000000..8cd887b8ac3 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/realm.gohtml @@ -0,0 +1,37 @@ +{{ define "renderRealmToc" }} + +{{ end }} + +{{ define "renderRealm" }} +
+
+ +
+ + {{ .Content }} +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/redirect.go b/gno.land/pkg/gnoweb/components/redirect.go new file mode 100644 index 00000000000..873ddf56ff5 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/redirect.go @@ -0,0 +1,12 @@ +package components + +import "io" + +type RedirectData struct { + To string + WithAnalytics bool +} + +func RenderRedirectComponent(w io.Writer, data RedirectData) error { + return tmpl.ExecuteTemplate(w, "renderRedirect", data) +} diff --git a/gno.land/pkg/gnoweb/components/redirect.gohtml b/gno.land/pkg/gnoweb/components/redirect.gohtml new file mode 100644 index 00000000000..45dac0981cd --- /dev/null +++ b/gno.land/pkg/gnoweb/components/redirect.gohtml @@ -0,0 +1,16 @@ +{{- define "renderRedirect" -}} + + + + + + + + Redirecting to {{.To}} + + + {{.To}} + {{- if .WithAnalytics -}} {{- template "analytics" }} {{- end -}} + + +{{- end -}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/components/source.go b/gno.land/pkg/gnoweb/components/source.go new file mode 100644 index 00000000000..23170776657 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/source.go @@ -0,0 +1,20 @@ +package components + +import ( + "html/template" + "io" +) + +type SourceData struct { + PkgPath string + Files []string + FileName string + FileSize string + FileLines int + FileCounter int + FileSource template.HTML +} + +func RenderSourceComponent(w io.Writer, data SourceData) error { + return tmpl.ExecuteTemplate(w, "renderSource", data) +} diff --git a/gno.land/pkg/gnoweb/components/source.gohtml b/gno.land/pkg/gnoweb/components/source.gohtml new file mode 100644 index 00000000000..ef254bdd313 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/source.gohtml @@ -0,0 +1,51 @@ +{{ define "renderSource" }} +
+
+
+
+

{{ .FileName }}

+
+
+ {{ .FileSize }} · {{ .FileLines }} lines + +
+
+ + +
+
+ {{ .FileSource }} +
+
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/spritesvg.gohtml b/gno.land/pkg/gnoweb/components/spritesvg.gohtml new file mode 100644 index 00000000000..811ad6e6846 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/spritesvg.gohtml @@ -0,0 +1,134 @@ +{{ define "spritesvg" }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/status.gohtml b/gno.land/pkg/gnoweb/components/status.gohtml new file mode 100644 index 00000000000..2321d1110bd --- /dev/null +++ b/gno.land/pkg/gnoweb/components/status.gohtml @@ -0,0 +1,12 @@ +{{ define "status" }} +
+
+
+ gno land +

Error: {{ .Message }}

+

Something went wrong. Let’s find our way back!

+ Go Back Home +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/template.go b/gno.land/pkg/gnoweb/components/template.go new file mode 100644 index 00000000000..9c08703f460 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/template.go @@ -0,0 +1,77 @@ +package components + +import ( + "bytes" + "context" + "embed" + "html/template" + "io" + "net/url" +) + +//go:embed *.gohtml +var gohtml embed.FS + +var funcMap = template.FuncMap{ + // NOTE: this method does NOT escape HTML, use with caution + "noescape_string": func(in string) template.HTML { + return template.HTML(in) //nolint:gosec + }, + // NOTE: this method does NOT escape HTML, use with caution + "noescape_bytes": func(in []byte) template.HTML { + return template.HTML(in) //nolint:gosec + }, + "queryHas": func(vals url.Values, key string) bool { + if vals == nil { + return false + } + + return vals.Has(key) + }, +} + +var tmpl = template.New("web").Funcs(funcMap) + +func init() { + registerHelpFuncs(funcMap) + tmpl.Funcs(funcMap) + + var err error + tmpl, err = tmpl.ParseFS(gohtml, "*.gohtml") + if err != nil { + panic("unable to parse embed tempalates: " + err.Error()) + } +} + +type Component func(ctx context.Context, tmpl *template.Template, w io.Writer) error + +func (c Component) Render(ctx context.Context, w io.Writer) error { + return RenderComponent(ctx, w, c) +} + +func RenderComponent(ctx context.Context, w io.Writer, c Component) error { + var render *template.Template + funcmap := template.FuncMap{ + "render": func(cf Component) (string, error) { + var buf bytes.Buffer + if err := cf(ctx, render, &buf); err != nil { + return "", err + } + + return buf.String(), nil + }, + } + + render = tmpl.Funcs(funcmap) + return c(ctx, render, w) +} + +type StatusData struct { + Message string +} + +func RenderStatusComponent(w io.Writer, message string) error { + return tmpl.ExecuteTemplate(w, "status", StatusData{ + Message: message, + }) +} diff --git a/gno.land/pkg/gnoweb/formatter.go b/gno.land/pkg/gnoweb/formatter.go new file mode 100644 index 00000000000..e172afe9e21 --- /dev/null +++ b/gno.land/pkg/gnoweb/formatter.go @@ -0,0 +1,25 @@ +package gnoweb + +import ( + "io" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/formatters/html" +) + +type Formatter interface { + Format(w io.Writer, iterator chroma.Iterator) error +} + +type formatterWithStyle struct { + *html.Formatter + style *chroma.Style +} + +func newFormatterWithStyle(formater *html.Formatter, style *chroma.Style) Formatter { + return &formatterWithStyle{Formatter: formater, style: style} +} + +func (f *formatterWithStyle) Format(w io.Writer, iterator chroma.Iterator) error { + return f.Formatter.Format(w, f.style, iterator) +} diff --git a/gno.land/pkg/gnoweb/frontend/css/input.css b/gno.land/pkg/gnoweb/frontend/css/input.css new file mode 100644 index 00000000000..2c2e110c27c --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/input.css @@ -0,0 +1,352 @@ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("./fonts/roboto/roboto-mono-normal.woff2") format("woff2"), url("./fonts/roboto/roboto-mono-normal.woff") format("woff"); +} + +@font-face { + font-family: "Inter var"; + font-weight: 100 900; + font-display: swap; + font-style: oblique 0deg 10deg; + src: url("./fonts/intervar/Inter.var.woff2") format("woff2"); +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + @apply font-interNormal text-gray-600 bg-light text-200; + font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; + -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; + text-size-adjust: 100%; + -moz-osx-font-smoothing: grayscale; + font-smoothing: antialiased; + font-variant-ligatures: contextual common-ligatures; + font-kerning: normal; + text-rendering: optimizeLegibility; + overflow-x: hidden; + } + + @supports (font-variation-settings: normal) { + html { + @apply font-interVar; + } + } + + svg { + @apply max-w-full max-h-full; + } + + form { + @apply my-0; + } + + .realm-content { + @apply text-200; + } + + .realm-content a { + @apply text-green-600 font-medium hover:underline; + } + + .realm-content h1, + .realm-content h2, + .realm-content h3, + .realm-content h4 { + @apply text-gray-900 mt-8 leading-tight; + } + + .realm-content h2, + .realm-content h2 * { + @apply font-bold; + } + + .realm-content h3, + .realm-content h3 *, + .realm-content h4, + .realm-content h4 * { + @apply font-semibold; + } + + .realm-content h1 + h2, + .realm-content h2 + h3, + .realm-content h3 + h4 { + @apply mt-1.5; + } + + .realm-content h1 { + @apply text-800 font-bold; + } + + .realm-content h2 { + @apply text-600; + } + + .realm-content h3 { + @apply text-400 text-gray-600 mt-6; + } + + .realm-content h4 { + @apply text-300 text-gray-600 font-medium my-4; + } + + .realm-content p { + @apply my-4; + } + + .realm-content strong { + @apply font-bold text-gray-900; + } + + .realm-content strong * { + @apply font-bold; + } + + .realm-content em { + @apply italic-subtle; + } + + .realm-content blockquote { + @apply border-l-4 border-gray-300 pl-4 text-gray-600 italic-subtle my-4; + } + + .realm-content ul, + .realm-content ol { + @apply pl-4 my-4; + } + + .realm-content ul li, + .realm-content ol li { + @apply mb-1; + } + + .realm-content img { + @apply max-w-full my-6; + } + + .realm-content figure { + @apply my-6 text-center; + } + + .realm-content figcaption { + @apply text-100 text-gray-600; + } + + .realm-content :not(pre) > code { + @apply bg-gray-100 px-1 py-0.5 rounded-sm text-100 font-mono; + } + + .realm-content pre { + @apply bg-gray-50 p-4 rounded overflow-x-auto font-mono; + } + + .realm-content hr { + @apply border-t border-gray-100 my-8; + } + + .realm-content table { + @apply w-full border-collapse my-6; + } + + .realm-content th, + .realm-content td { + @apply border border-gray-300 px-4 py-2; + } + + .realm-content th { + @apply bg-gray-100 font-bold; + } + + .realm-content caption { + @apply mt-2 text-100 text-gray-600 text-left; + } + + .realm-content q { + @apply quotes; + } + + .realm-content q::before { + content: open-quote; + } + + .realm-content q::after { + content: close-quote; + } + + .realm-content ul ul, + .realm-content ul ol, + .realm-content ol ul, + .realm-content ol ol { + @apply mt-2 mb-2 pl-4; + } + + .realm-content ul { + @apply list-disc; + } + + .realm-content ol { + @apply list-decimal; + } + + .realm-content table th:first-child, + .realm-content td:first-child { + @apply pl-0; + } + + .realm-content table th:last-child, + .realm-content td:last-child { + @apply pr-0; + } + + .realm-content abbr[title] { + @apply border-b border-dotted cursor-help; + } + + .realm-content details { + @apply my-4; + } + + .realm-content summary { + @apply font-bold cursor-pointer; + } + + .realm-content a code { + @apply text-inherit; + } + + .realm-content video { + @apply max-w-full my-6; + } + + .realm-content math { + @apply font-mono; + } + + .realm-content small { + @apply text-100; + } + + .realm-content del { + @apply line-through; + } + + .realm-content sub { + @apply text-50 align-sub; + } + + .realm-content sup { + @apply text-50 align-super; + } + + .realm-content input, + .realm-content button { + @apply px-4 py-2 border border-gray-300; + } + + main :is(h1, h2, h3, h4) { + @apply scroll-mt-24; + } + + ::-moz-selection { + @apply bg-green-600 text-light; + } + ::selection { + @apply bg-green-600 text-light; + } +} + +@layer components { + /* header */ + .sidemenu .peer:checked + label > svg { + @apply text-green-600; + } + + /* toc */ + .toc-expend-btn { + @apply after:content-['open'] after:font-normal after:text-100 lg:after:content-none; + } + .toc-expend-btn:has(#toc-expend:checked) { + @apply after:content-['close'] lg:after:content-none; + } + .toc-expend-btn:has(#toc-expend:checked) + nav { + @apply block; + } + + /* sidebar */ + .main-header:has(#sidemenu-summary:checked) + main #sidebar #sidebar-summary, + .main-header:has(#sidemenu-source:checked) + main #sidebar #sidebar-source, + .main-header:has(#sidemenu-docs:checked) + main #sidebar #sidebar-docs, + .main-header:has(#sidemenu-meta:checked) + main #sidebar #sidebar-meta { + @apply block; + } + + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main .realm-content, + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .main-navigation { + @apply md:col-span-6; + } + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar, + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .sidemenu { + @apply md:col-span-4; + } + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar::before { + @apply absolute block content-[''] top-0 w-[50vw] h-full -left-7 bg-gray-100 z-min; + } + + /* chroma */ + main :is(.source-code) > pre { + @apply !bg-light overflow-scroll rounded py-4 md:py-8 px-1 md:px-3 font-mono text-100 md:text-200; + } + main .realm-content > pre a { + @apply hover:no-underline; + } + + main :is(.realm-content, .source-code) > pre .chroma-ln:target { + @apply !bg-transparent; + } + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target), + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover), + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target) .chroma-cl, + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl { + @apply !bg-gray-100 rounded; + } + main :is(.realm-content, .source-code) > pre .chroma-ln { + @apply scroll-mt-24; + } +} + +@layer utilities { + .italic-subtle { + font-style: oblique 10deg; + } + + .quotes { + @apply italic-subtle text-[#555] border-l-4 border-l-[#ccc] pl-4 my-6 [quotes:"“"_"”"_"‘"_"’"]; + } + + .quotes::before, + .quotes::after { + @apply [content:open-quote] text-600 text-gray-300 mr-1 [vertical-align:-0.4rem]; + } + + .quotes::after { + @apply [content:close-quote]; + } + + .text-stroke { + -webkit-text-stroke: currentColor; + -webkit-text-stroke-width: 0.6px; + } + + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/gno.land/pkg/gnoweb/frontend/css/tx.config.js b/gno.land/pkg/gnoweb/frontend/css/tx.config.js new file mode 100644 index 00000000000..198354c700e --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/tx.config.js @@ -0,0 +1,76 @@ +const pxToRem = (px) => px / 16; + +export default { + content: ["./components/**/*.{gohtml,ts}"], + theme: { + screens: { + xs: `${pxToRem(360)}rem`, + sm: `${pxToRem(480)}rem`, + md: `${pxToRem(640)}rem`, + lg: `${pxToRem(820)}rem`, + xl: `${pxToRem(1020)}rem`, + xxl: `${pxToRem(1366)}rem`, + max: `${pxToRem(1580)}rem`, + }, + zIndex: { + min: "-1", + 1: "1", + 2: "2", + 100: "100", + max: "9999", + }, + container: { + center: true, + padding: `${pxToRem(40)}rem`, + }, + borderRadius: { + sm: `${pxToRem(4)}rem`, + DEFAULT: `${pxToRem(6)}rem`, + }, + colors: { + light: "#FFFFFF", + gray: { + 50: "#F0F0F0", // Background color + 100: "#E2E2E2", // Title dark color + 200: "#BDBDBD", // Content dark color + 300: "#999999", // Muted color + 400: "#7C7C7C", // Border color + 600: "#54595D", // Content color + 800: "#131313", // Background dark color + 900: "#080809", // Title color + }, + green: { + 400: "#2D8D72", // Primary dark color + 600: "#226C57", // Primary light color + }, + transparent: "transparent", + current: "currentColor", + inherit: "inherit", + }, + fontFamily: { + mono: ["Roboto", 'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace;'], + interVar: [ + '"Inter var"', + 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', + ], + interNormal: [ + "Inter", + 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', + ], + }, + fontSize: { + 0: "0", + 50: `${pxToRem(12)}rem`, + 100: `${pxToRem(14)}rem`, + 200: `${pxToRem(16)}rem`, + 300: `${pxToRem(18)}rem`, + 400: `${pxToRem(20)}rem`, + 500: `${pxToRem(22)}rem`, + 600: `${pxToRem(24)}rem`, + 700: `${pxToRem(32)}rem`, + 800: `${pxToRem(38)}rem`, + 900: `${pxToRem(42)}rem`, + }, + }, + plugins: [], +}; diff --git a/gno.land/pkg/gnoweb/frontend/js/copy.ts b/gno.land/pkg/gnoweb/frontend/js/copy.ts new file mode 100644 index 00000000000..1ba725a9d3a --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/copy.ts @@ -0,0 +1,103 @@ +class Copy { + private DOM: { + el: HTMLElement | null; + }; + private static FEEDBACK_DELAY = 1500; + + private btnClicked: HTMLElement | null = null; + private btnClickedIcons: HTMLElement[] = []; + + private static SELECTORS = { + button: "[data-copy-btn]", + icon: `[data-copy-icon] > use`, + content: (id: string) => `[data-copy-content="${id}"]`, + }; + + constructor() { + this.DOM = { + el: document.querySelector("main"), + }; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("Copy: Main container not found."); + } + } + + private init(): void { + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el?.addEventListener("click", this.handleClick.bind(this)); + } + + private handleClick(event: Event): void { + const target = event.target as HTMLElement; + const button = target.closest(Copy.SELECTORS.button); + + if (!button) return; + + this.btnClicked = button; + this.btnClickedIcons = Array.from(button.querySelectorAll(Copy.SELECTORS.icon)); + + const contentId = button.getAttribute("data-copy-btn"); + if (!contentId) { + console.warn("Copy: No content ID found on the button."); + return; + } + + const codeBlock = this.DOM.el?.querySelector(Copy.SELECTORS.content(contentId)); + if (codeBlock) { + this.copyToClipboard(codeBlock); + } else { + console.warn(`Copy: No content found for ID "${contentId}".`); + } + } + + private sanitizeContent(codeBlock: HTMLElement): string { + const html = codeBlock.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g, ""); + + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = html; + + return tempDiv.textContent?.trim() || ""; + } + + private toggleIcons(): void { + this.btnClickedIcons.forEach((icon) => { + icon.classList.toggle("hidden"); + }); + } + + private showFeedback(): void { + if (!this.btnClicked) return; + + this.toggleIcons(); + window.setTimeout(() => { + this.toggleIcons(); + }, Copy.FEEDBACK_DELAY); + } + + private async copyToClipboard(codeBlock: HTMLElement): Promise { + const sanitizedText = this.sanitizeContent(codeBlock); + + if (!navigator.clipboard) { + console.error("Copy: Clipboard API is not supported in this browser."); + this.showFeedback(); + return; + } + + try { + await navigator.clipboard.writeText(sanitizedText); + console.info("Copy: Text copied successfully."); + this.showFeedback(); + } catch (err) { + console.error("Copy: Error while copying text.", err); + this.showFeedback(); + } + } +} + +export default () => new Copy(); diff --git a/gno.land/pkg/gnoweb/frontend/js/index.ts b/gno.land/pkg/gnoweb/frontend/js/index.ts new file mode 100644 index 00000000000..3927f794b94 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/index.ts @@ -0,0 +1,42 @@ +(() => { + interface Module { + selector: string; + path: string; + } + + const modules: Record = { + copy: { + selector: "[data-copy-btn]", + path: "/public/js/copy.js", + }, + help: { + selector: "#help", + path: "/public/js/realmhelp.js", + }, + searchBar: { + selector: "#header-searchbar", + path: "/public/js/searchbar.js", + }, + }; + + const loadModuleIfExists = async ({ selector, path }: Module): Promise => { + const element = document.querySelector(selector); + if (element) { + try { + const module = await import(path); + module.default(); + } catch (err) { + console.error(`Error while loading script ${path}:`, err); + } + } else { + console.warn(`Module not loaded: no element matches selector "${selector}"`); + } + }; + + const initModules = async (): Promise => { + const promises = Object.values(modules).map((module) => loadModuleIfExists(module)); + await Promise.all(promises); + }; + + document.addEventListener("DOMContentLoaded", initModules); +})(); diff --git a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts new file mode 100644 index 00000000000..980e9625875 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts @@ -0,0 +1,125 @@ +class Help { + private DOM: { + el: HTMLElement | null; + funcs: HTMLElement[]; + addressInput: HTMLInputElement | null; + cmdModeSelect: HTMLSelectElement | null; + }; + + private funcList: HelpFunc[]; + + private static SELECTORS = { + container: "#help", + func: "[data-func]", + addressInput: "[data-role='help-input-addr']", + cmdModeSelect: "[data-role='help-select-mode']", + }; + + constructor() { + this.DOM = { + el: document.querySelector(Help.SELECTORS.container), + funcs: [], + addressInput: null, + cmdModeSelect: null, + }; + + this.funcList = []; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("Help: Main container not found."); + } + } + + private init(): void { + const { el } = this.DOM; + if (!el) return; + + this.DOM.funcs = Array.from(el.querySelectorAll(Help.SELECTORS.func)); + this.DOM.addressInput = el.querySelector(Help.SELECTORS.addressInput); + this.DOM.cmdModeSelect = el.querySelector(Help.SELECTORS.cmdModeSelect); + + console.log(this.DOM); + this.funcList = this.DOM.funcs.map((funcEl) => new HelpFunc(funcEl)); + + this.bindEvents(); + } + + private bindEvents(): void { + const { addressInput, cmdModeSelect } = this.DOM; + + addressInput?.addEventListener("input", () => { + this.funcList.forEach((func) => func.updateAddr(addressInput.value)); + }); + + cmdModeSelect?.addEventListener("change", (e) => { + const target = e.target as HTMLSelectElement; + this.funcList.forEach((func) => func.updateMode(target.value)); + }); + } +} + +class HelpFunc { + private DOM: { + el: HTMLElement; + addrs: HTMLElement[]; + args: HTMLElement[]; + modes: HTMLElement[]; + }; + + private funcName: string | null; + + private static SELECTORS = { + address: "[data-role='help-code-address']", + args: "[data-role='help-code-args']", + mode: "[data-code-mode]", + paramInput: "[data-role='help-param-input']", + }; + + constructor(el: HTMLElement) { + this.DOM = { + el, + addrs: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.address)), + args: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.args)), + modes: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.mode)), + }; + + this.funcName = el.dataset.func || null; + + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el.addEventListener("input", (e) => { + const target = e.target as HTMLInputElement; + if (target.dataset.role === "help-param-input") { + this.updateArg(target.dataset.param || "", target.value); + } + }); + } + + public updateArg(paramName: string, paramValue: string): void { + this.DOM.args + .filter((arg) => arg.dataset.arg === paramName) + .forEach((arg) => { + arg.textContent = paramValue.trim() || ""; + }); + } + + public updateAddr(addr: string): void { + this.DOM.addrs.forEach((DOMaddr) => { + DOMaddr.textContent = addr.trim() || "ADDRESS"; + }); + } + + public updateMode(mode: string): void { + this.DOM.modes.forEach((cmd) => { + const isVisible = cmd.dataset.codeMode === mode; + cmd.className = isVisible ? "inline" : "hidden"; + cmd.dataset.copyContent = isVisible ? `help-cmd-${this.funcName}` : ""; + }); + } +} + +export default () => new Help(); diff --git a/gno.land/pkg/gnoweb/frontend/js/searchbar.ts b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts new file mode 100644 index 00000000000..6cca444aa0f --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts @@ -0,0 +1,74 @@ +class SearchBar { + private DOM: { + el: HTMLElement | null; + inputSearch: HTMLInputElement | null; + breadcrumb: HTMLElement | null; + }; + + private baseUrl: string; + + private static SELECTORS = { + container: "#header-searchbar", + inputSearch: "[data-role='header-input-search']", + breadcrumb: "[data-role='header-breadcrumb-search']", + }; + + constructor() { + this.DOM = { + el: document.querySelector(SearchBar.SELECTORS.container), + inputSearch: null, + breadcrumb: null, + }; + + this.baseUrl = window.location.origin; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("SearchBar: Main container not found."); + } + } + + private init(): void { + const { el } = this.DOM; + + this.DOM.inputSearch = el?.querySelector(SearchBar.SELECTORS.inputSearch) ?? null; + this.DOM.breadcrumb = el?.querySelector(SearchBar.SELECTORS.breadcrumb) ?? null; + + if (!this.DOM.inputSearch) { + console.warn("SearchBar: Input element for search not found."); + } + + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el?.addEventListener("submit", (e) => { + e.preventDefault(); + this.searchUrl(); + }); + } + + public searchUrl(): void { + const input = this.DOM.inputSearch?.value.trim(); + + if (input) { + let url = input; + + // Check if the URL has a proper scheme + if (!/^https?:\/\//i.test(url)) { + url = `${this.baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; + } + + try { + window.location.href = new URL(url).href; + } catch (error) { + console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://."); + } + } else { + console.error("SearchBar: Please enter a URL to search."); + } + } +} + +export default () => new SearchBar(); diff --git a/gno.land/pkg/gnoweb/static/img/favicon.ico b/gno.land/pkg/gnoweb/frontend/static/favicon.ico similarity index 100% rename from gno.land/pkg/gnoweb/static/img/favicon.ico rename to gno.land/pkg/gnoweb/frontend/static/favicon.ico diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..365eedc50cd0f46ea35a3176335fc67b51123fb4 GIT binary patch literal 324864 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≫bY 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≥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

6&qaNDxLwZnq2 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

ycBey{rl;kDc{Ai+(=LzE1TWNYW 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

w%k_tC9o+J?jlEWgdzIe0b`MG#H=N&ZEd~ha9 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..53d081f3a538a63578c15a5cc11219b32e6d5795 GIT binary patch literal 12764 zcmV<2F(b}*Pew8T0RR9105RME4gdfE09y0_05O380RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg new file mode 100644 index 00000000000..30d2f3ef56a --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go deleted file mode 100644 index 40d027d84b9..00000000000 --- a/gno.land/pkg/gnoweb/gnoweb.go +++ /dev/null @@ -1,608 +0,0 @@ -package gnoweb - -import ( - "bytes" - "embed" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "log/slog" - "net/http" - "net/url" - "os" - "path/filepath" - "regexp" - "runtime" - "strings" - "time" - - "github.com/gnolang/gno/gnovm" - "github.com/gnolang/gno/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gorilla/mux" - "github.com/gotuna/gotuna" - - // for static files - "github.com/gnolang/gno/gno.land/pkg/gnoweb/static" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types - // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) -) - -const ( - qFileStr = "vm/qfile" - gnowebArgsSeparator = "$" - urlQuerySeparator = "?" -) - -//go:embed views/* -var defaultViewsFiles embed.FS - -type Config struct { - RemoteAddr string - CaptchaSite string - FaucetURL string - ViewsDir string - HelpChainID string - HelpRemote string - WithAnalytics bool - WithHTML bool -} - -func NewDefaultConfig() Config { - return Config{ - RemoteAddr: "127.0.0.1:26657", - CaptchaSite: "", - FaucetURL: "http://localhost:5050", - ViewsDir: "", - HelpChainID: "dev", - HelpRemote: "127.0.0.1:26657", - WithAnalytics: false, - WithHTML: false, - } -} - -func MakeApp(logger *slog.Logger, cfg Config) gotuna.App { - var viewFiles fs.FS - - // Get specific views directory if specified - if cfg.ViewsDir != "" { - viewFiles = os.DirFS(cfg.ViewsDir) - } else { - // Get embed views - var err error - viewFiles, err = fs.Sub(defaultViewsFiles, "views") - if err != nil { - panic("unable to get views directory from embed fs: " + err.Error()) - } - } - - app := gotuna.App{ - ViewFiles: viewFiles, - Router: gotuna.NewMuxRouter(), - Static: static.EmbeddedStatic, - } - - for from, to := range Aliases { - app.Router.Handle(from, handlerRealmAlias(logger, app, &cfg, to)) - } - - for from, to := range Redirects { - app.Router.Handle(from, handlerRedirect(logger, app, &cfg, to)) - } - // realm routes - // NOTE: see rePathPart. - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:(?:.*\\.(?:gno|md|txt|mod)$)|(?:LICENSE$))?}", handlerRealmFile(logger, app, &cfg)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}{args:(?:\\$.*)?}", handlerRealmMain(logger, app, &cfg)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:[^$]*}{args:(?:\\$.*)?}", handlerRealmRender(logger, app, &cfg)) - app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(logger, app, &cfg)) - - // other - app.Router.Handle("/faucet", handlerFaucet(logger, app, &cfg)) - app.Router.Handle("/static/{path:.+}", handlerStaticFile(logger, app, &cfg)) - app.Router.Handle("/favicon.ico", handlerFavicon(logger, app, &cfg)) - - // api - app.Router.Handle("/status.json", handlerStatusJSON(logger, app, &cfg)) - - app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.RequestURI - handleNotFound(logger, app, &cfg, path, w, r) - }) - return app -} - -var ( - inlineCodePattern = regexp.MustCompile("`[^`]*`") - htmlTagPattern = regexp.MustCompile(`<\/?\w+[^>]*?>`) -) - -func sanitizeContent(cfg *Config, content string) string { - if cfg.WithHTML { - return content - } - - placeholders := map[string]string{} - contentWithPlaceholders := inlineCodePattern.ReplaceAllStringFunc(content, func(match string) string { - placeholder := fmt.Sprintf("__GNOMDCODE_%d__", len(placeholders)) - placeholders[placeholder] = match - return placeholder - }) - - sanitizedContent := htmlTagPattern.ReplaceAllString(contentWithPlaceholders, "") - - if len(placeholders) > 0 { - for placeholder, code := range placeholders { - sanitizedContent = strings.ReplaceAll(sanitizedContent, placeholder, code) - } - } - - return sanitizedContent -} - -// handlerRealmAlias is used to render official pages from realms. -// url is intended to be shorter. -// UX is intended to be more minimalistic. -// A link to the realm realm is added. -func handlerRealmAlias(logger *slog.Logger, app gotuna.App, cfg *Config, rlmpath string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rlmfullpath := "gno.land" + rlmpath - querystr := "" // XXX: "?gnoweb-alias=1" - parts := strings.Split(rlmpath, ":") - switch len(parts) { - case 1: // continue - case 2: // r/realm:querystr - rlmfullpath = "gno.land" + parts[0] - querystr = parts[1] + querystr - default: - panic("should not happen") - } - rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/") - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s:%s", rlmfullpath, querystr)) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, fmt.Errorf("gnoweb failed to query gnoland: %w", err)) - return - } - - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), - Text: part, - }) - } - - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", sanitizeContent(cfg, string(res.Data))) - tmpl.Set("Config", cfg) - tmpl.Set("IsAlias", true) - tmpl.Render(w, r, "realm_render.html", "funcs.html") - }) -} - -func handlerFaucet(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("Config", cfg). - Render(w, r, "faucet.html", "funcs.html") - }) -} - -func handlerStatusJSON(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - startedAt := time.Now() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ret struct { - Gnoland struct { - Connected bool `json:"connected"` - Error *string `json:"error,omitempty"` - Height *int64 `json:"height,omitempty"` - // processed txs - // active connections - - Version *string `json:"version,omitempty"` - // Uptime *float64 `json:"uptime-seconds,omitempty"` - // Goarch *string `json:"goarch,omitempty"` - // Goos *string `json:"goos,omitempty"` - // GoVersion *string `json:"go-version,omitempty"` - // NumCPU *int `json:"num_cpu,omitempty"` - } `json:"gnoland"` - Website struct { - // Version string `json:"version"` - Uptime float64 `json:"uptime-seconds"` - Goarch string `json:"goarch"` - Goos string `json:"goos"` - GoVersion string `json:"go-version"` - NumCPU int `json:"num_cpu"` - } `json:"website"` - } - ret.Website.Uptime = time.Since(startedAt).Seconds() - ret.Website.Goarch = runtime.GOARCH - ret.Website.Goos = runtime.GOOS - ret.Website.NumCPU = runtime.NumCPU() - ret.Website.GoVersion = runtime.Version() - - ret.Gnoland.Connected = true - res, err := makeRequest(logger, cfg, ".app/version", []byte{}) - if err != nil { - ret.Gnoland.Connected = false - errmsg := err.Error() - ret.Gnoland.Error = &errmsg - } else { - version := string(res.Value) - ret.Gnoland.Version = &version - ret.Gnoland.Height = &res.Height - } - - out, _ := json.MarshalIndent(ret, "", " ") - w.Header().Set("Content-Type", "application/json") - w.Write(out) - }) -} - -func handlerRedirect(logger *slog.Logger, app gotuna.App, cfg *Config, to string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, to, http.StatusFound) - tmpl := app.NewTemplatingEngine() - tmpl.Set("To", to) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "redirect.html", "funcs.html") - }) -} - -func handlerRealmMain(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - args, err := parseGnowebArgs(r.RequestURI) - if err != nil { - writeError(logger, w, err) - return - } - - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - - logger.Info("handling", "name", rlmname, "path", rlmpath) - if args.Has("help") { - // Render function helper. - funcName := args.Get("func") - qpath := "vm/qfuncs" - data := []byte(rlmpath) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, fmt.Errorf("request failed: %w", err)) - return - } - var fsigs vm.FunctionSignatures - amino.MustUnmarshalJSON(res.Data, &fsigs) - // Fill fsigs with query parameters. - for i := range fsigs { - fsig := &(fsigs[i]) - for j := range fsig.Params { - param := &(fsig.Params[j]) - value := args.Get(param.Name) - param.Value = value - } - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("FuncName", funcName) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("DirPath", pathOf(rlmpath)) - tmpl.Set("FunctionSignatures", fsigs) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "realm_help.html", "funcs.html") - } else { - // Ensure realm exists. TODO optimize. - qpath := qFileStr - data := []byte(rlmpath) - _, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, errors.New("error querying realm package")) - return - } - // Render blank query path, /r/REALM:. - handleRealmRender(logger, app, cfg, w, r) - } - }) -} - -type pathLink struct { - URL string - Text string -} - -func handlerRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRealmRender(logger, app, cfg, w, r) - }) -} - -func handleRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request) { - gnowebArgs, err := parseGnowebArgs(r.RequestURI) - if err != nil { - writeError(logger, w, err) - return - } - - queryArgs, err := parseQueryArgs(r.RequestURI) - if err != nil { - writeError(logger, w, err) - return - } - - var urlQuery, gnowebQuery string - if len(queryArgs) > 0 { - urlQuery = urlQuerySeparator + queryArgs.Encode() - } - if len(gnowebArgs) > 0 { - gnowebQuery = gnowebArgsSeparator + gnowebArgs.Encode() - } - - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - querystr := vars["querystr"] - if r.URL.Path == "/r/"+rlmname+":" { - // Redirect to /r/REALM if querypath is empty. - http.Redirect(w, r, "/r/"+rlmname+urlQuery+gnowebQuery, http.StatusFound) - return - } - - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s:%s", rlmpath, querystr+urlQuery)) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - // XXX hack - if strings.Contains(err.Error(), "Render not declared") { - res = &abci.ResponseQuery{} - res.Data = []byte("realm package has no Render() function") - } else { - writeError(logger, w, err) - return - } - } - - dirdata := []byte(rlmpath) - dirres, err := makeRequest(logger, cfg, qFileStr, dirdata) - if err != nil { - writeError(logger, w, err) - return - } - hasReadme := bytes.Contains(append(dirres.Data, '\n'), []byte("README.md\n")) - - // linkify querystr. - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - rlmpath := strings.Join(queryParts[:i+1], "/") - - // Add URL query arguments to the last breadcrumb item's URL - if i+1 == len(queryParts) { - rlmpath = rlmpath + urlQuery + gnowebQuery - } - - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + rlmpath, - Text: part, - }) - } - - // Render template. - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", sanitizeContent(cfg, string(res.Data))) - tmpl.Set("Config", cfg) - tmpl.Set("HasReadme", hasReadme) - tmpl.Render(w, r, "realm_render.html", "funcs.html") -} - -func handlerRealmFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - diruri := "gno.land/r/" + vars["rlmname"] - filename := vars["filename"] - renderPackageFile(logger, app, cfg, w, r, diruri, filename) - }) -} - -func handlerPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - pkgpath := "gno.land/p/" + vars["filepath"] - diruri, filename := gnovm.SplitFilepath(pkgpath) - if filename == "" && diruri == pkgpath { - // redirect to diruri + "/" - http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound) - return - } - renderPackageFile(logger, app, cfg, w, r, diruri, filename) - }) -} - -func renderPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request, diruri string, filename string) { - if filename == "" { - // Request is for a folder. - qpath := qFileStr - data := []byte(diruri) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, err) - return - } - files := strings.Split(string(res.Data), "\n") - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("Files", files) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "package_dir.html", "funcs.html") - } else { - // Request is for a file. - filepath := diruri + "/" + filename - qpath := qFileStr - data := []byte(filepath) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, err) - return - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("FileName", filename) - tmpl.Set("FileContents", string(res.Data)) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "package_file.html", "funcs.html") - } -} - -func makeRequest(log *slog.Logger, cfg *Config, qpath string, data []byte) (res *abci.ResponseQuery, err error) { - opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } - remote := cfg.RemoteAddr - cli, err := client.NewHTTPClient(remote) - if err != nil { - return nil, fmt.Errorf("unable to create HTTP client, %w", err) - } - - qres, err := cli.ABCIQueryWithOptions( - qpath, data, opts2) - if err != nil { - log.Error("request error", "path", qpath, "error", err) - return nil, fmt.Errorf("unable to query path %q: %w", qpath, err) - } - if qres.Response.Error != nil { - log.Error("response error", "path", qpath, "log", qres.Response.Log) - return nil, qres.Response.Error - } - return &qres.Response, nil -} - -func handlerStaticFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - fs := http.FS(app.Static) - fileapp := http.StripPrefix("/static", http.FileServer(fs)) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - fpath := filepath.Clean(vars["path"]) - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - stat, err := f.Stat() - if err != nil || stat.IsDir() { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - - // TODO: ModTime doesn't work for embed? - // w.Header().Set("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano())) - // w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s", "31536000")) - fileapp.ServeHTTP(w, r) - }) -} - -func handlerFavicon(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - fs := http.FS(app.Static) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fpath := "img/favicon.ico" - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - w.Header().Set("Content-Type", "image/x-icon") - w.Header().Set("Cache-Control", "public, max-age=604800") // 7d - io.Copy(w, f) - }) -} - -func handleNotFound(logger *slog.Logger, app gotuna.App, cfg *Config, path string, w http.ResponseWriter, r *http.Request) { - // decode path for non-ascii characters - decodedPath, err := url.PathUnescape(path) - if err != nil { - logger.Error("failed to decode path", "error", err) - decodedPath = path - } - w.WriteHeader(http.StatusNotFound) - app.NewTemplatingEngine(). - Set("title", "Not found"). - Set("path", decodedPath). - Set("Config", cfg). - Render(w, r, "404.html", "funcs.html") -} - -func writeError(logger *slog.Logger, w http.ResponseWriter, err error) { - if details := errors.Unwrap(err); details != nil { - logger.Error("handler", "error", err, "details", details) - } else { - logger.Error("handler", "error", err) - } - - // XXX: writeError should return an error page template. - w.WriteHeader(500) - w.Write([]byte(err.Error())) -} - -func pathOf(diruri string) string { - parts := strings.Split(diruri, "/") - if parts[0] == "gno.land" { - return "/" + strings.Join(parts[1:], "/") - } - - panic(fmt.Sprintf("invalid dir-URI %q", diruri)) -} - -// parseQueryArgs parses URL query arguments that are not specific to gnoweb. -// These are the standard arguments that comes after the "?" symbol and before -// the special "$" symbol. The "$" symbol can be used within public query -// arguments by using its encoded representation "%24". -func parseQueryArgs(rawURL string) (url.Values, error) { - if i := strings.Index(rawURL, gnowebArgsSeparator); i != -1 { - rawURL = rawURL[:i] - } - - u, err := url.Parse(rawURL) - if err != nil { - return url.Values{}, fmt.Errorf("invalid query arguments: %w", err) - } - return u.Query(), nil -} - -// parseGnowebArgs parses URL query arguments that are specific to gnoweb. -// These arguments are indicated by using the "$" symbol followed by a query -// string with the arguments. -func parseGnowebArgs(rawURL string) (url.Values, error) { - i := strings.Index(rawURL, gnowebArgsSeparator) - if i == -1 { - return url.Values{}, nil - } - - values, err := url.ParseQuery(rawURL[i+1:]) - if err != nil { - return url.Values{}, fmt.Errorf("invalid gnoweb arguments: %w", err) - } - return values, nil -} diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go new file mode 100644 index 00000000000..b3a9fcd143c --- /dev/null +++ b/gno.land/pkg/gnoweb/handler.go @@ -0,0 +1,381 @@ +package gnoweb + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "io" + "log/slog" + "net/http" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types +) + +const DefaultChainDomain = "gno.land" + +type StaticMetadata struct { + AssetsPath string + ChromaPath string + RemoteHelp string + ChainId string + Analytics bool +} + +type WebHandlerConfig struct { + Meta StaticMetadata + RenderClient *WebClient + Formatter Formatter +} + +type WebHandler struct { + formatter Formatter + + logger *slog.Logger + static StaticMetadata + webcli *WebClient +} + +func NewWebHandler(logger *slog.Logger, cfg WebHandlerConfig) *WebHandler { + if cfg.RenderClient == nil { + logger.Error("no renderer has been defined") + } + + return &WebHandler{ + formatter: cfg.Formatter, + webcli: cfg.RenderClient, + logger: logger, + static: cfg.Meta, + } +} + +func (h *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.logger.Debug("receiving request", "method", r.Method, "path", r.URL.Path) + + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + h.Get(w, r) +} + +func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) { + var body bytes.Buffer + + start := time.Now() + defer func() { + h.logger.Debug("request completed", + "url", r.URL.String(), + "elapsed", time.Since(start).String()) + }() + + var indexData components.IndexData + indexData.HeadData.AssetsPath = h.static.AssetsPath + indexData.HeadData.ChromaPath = h.static.ChromaPath + indexData.FooterData.Analytics = h.static.Analytics + indexData.FooterData.AssetsPath = h.static.AssetsPath + + // Render the page body into the buffer + var status int + gnourl, err := ParseGnoURL(r.URL) + if err != nil { + h.logger.Warn("page not found", "path", r.URL.Path, "err", err) + status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found") + } else { + // TODO: real data (title & description) + indexData.HeadData.Title = "gno.land - " + gnourl.Path + + // Header + indexData.HeaderData.RealmPath = gnourl.Path + indexData.HeaderData.Breadcrumb.Parts = generateBreadcrumbPaths(gnourl.Path) + indexData.HeaderData.WebQuery = gnourl.WebQuery + + // Render + switch gnourl.Kind() { + case KindRealm, KindPure: + status, err = h.renderPackage(&body, gnourl) + default: + h.logger.Debug("invalid page kind", "kind", gnourl.Kind) + status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found") + } + } + + if err != nil { + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(status) + + // NOTE: HTML escaping should have already been done by markdown rendering package + indexData.Body = template.HTML(body.String()) //nolint:gosec + + // Render the final page with the rendered body + if err = components.RenderIndexComponent(w, indexData); err != nil { + h.logger.Error("failed to render index component", "err", err) + } + + return +} + +func (h *WebHandler) renderPackage(w io.Writer, gnourl *GnoURL) (status int, err error) { + h.logger.Info("component render", "path", gnourl.Path, "args", gnourl.Args) + + kind := gnourl.Kind() + + // Display realm help page? + if kind == KindRealm && gnourl.WebQuery.Has("help") { + return h.renderRealmHelp(w, gnourl) + } + + // Display package source page? + switch { + case gnourl.WebQuery.Has("source"): + return h.renderRealmSource(w, gnourl) + case kind == KindPure, + strings.HasSuffix(gnourl.Path, "/"), + isFile(gnourl.Path): + i := strings.LastIndexByte(gnourl.Path, '/') + if i < 0 { + return http.StatusInternalServerError, fmt.Errorf("unable to get ending slash for %q", gnourl.Path) + } + + // Fill webquery with file infos + gnourl.WebQuery.Set("source", "") // set source + + file := gnourl.Path[i+1:] + if file == "" { + return h.renderRealmDirectory(w, gnourl) + } + + gnourl.WebQuery.Set("file", file) + gnourl.Path = gnourl.Path[:i] + + return h.renderRealmSource(w, gnourl) + } + + // Render content into the content buffer + var content bytes.Buffer + meta, err := h.webcli.Render(&content, gnourl.Path, gnourl.EncodeArgs()) + if err != nil { + if errors.Is(err, vm.InvalidPkgPathError{}) { + return http.StatusNotFound, components.RenderStatusComponent(w, "not found") + } + + h.logger.Error("unable to render markdown", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + err = components.RenderRealmComponent(w, components.RealmData{ + TocItems: &components.RealmTOCData{ + Items: meta.Items, + }, + // NOTE: `content` should have already been escaped by + Content: template.HTML(content.String()), //nolint:gosec + }) + if err != nil { + h.logger.Error("unable to render template", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + // Write the rendered content to the response writer + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmHelp(w io.Writer, gnourl *GnoURL) (status int, err error) { + fsigs, err := h.webcli.Functions(gnourl.Path) + if err != nil { + h.logger.Error("unable to fetch path functions", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + var selArgs map[string]string + var selFn string + if selFn = gnourl.WebQuery.Get("func"); selFn != "" { + for _, fn := range fsigs { + if selFn != fn.FuncName { + continue + } + + selArgs = make(map[string]string) + for _, param := range fn.Params { + selArgs[param.Name] = gnourl.WebQuery.Get(param.Name) + } + + fsigs = []vm.FunctionSignature{fn} + break + } + } + + // Catch last name of the path + // XXX: we should probably add a helper within the template + realmName := filepath.Base(gnourl.Path) + err = components.RenderHelpComponent(w, components.HelpData{ + SelectedFunc: selFn, + SelectedArgs: selArgs, + RealmName: realmName, + ChainId: h.static.ChainId, + // TODO: get chain domain and use that. + PkgPath: filepath.Join(DefaultChainDomain, gnourl.Path), + Remote: h.static.RemoteHelp, + Functions: fsigs, + }) + if err != nil { + h.logger.Error("unable to render helper", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmSource(w io.Writer, gnourl *GnoURL) (status int, err error) { + pkgPath := gnourl.Path + + files, err := h.webcli.Sources(pkgPath) + if err != nil { + h.logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + if len(files) == 0 { + h.logger.Debug("no files available", "path", gnourl.Path) + return http.StatusOK, components.RenderStatusComponent(w, "no files available") + } + + var fileName string + file := gnourl.WebQuery.Get("file") + if file == "" { + fileName = files[0] + } else if slices.Contains(files, file) { + fileName = file + } else { + h.logger.Error("unable to render source", "file", file, "err", "file does not exist") + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + source, err := h.webcli.SourceFile(pkgPath, fileName) + if err != nil { + h.logger.Error("unable to get source file", "file", fileName, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + // XXX: we should either do this on the front or in the markdown parsing side + fileLines := strings.Count(string(source), "\n") + fileSizeKb := float64(len(source)) / 1024.0 + fileSizeStr := fmt.Sprintf("%.2f Kb", fileSizeKb) + + // Highlight code source + hsource, err := h.highlightSource(fileName, source) + if err != nil { + h.logger.Error("unable to highlight source file", "file", fileName, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + err = components.RenderSourceComponent(w, components.SourceData{ + PkgPath: gnourl.Path, + Files: files, + FileName: fileName, + FileCounter: len(files), + FileLines: fileLines, + FileSize: fileSizeStr, + FileSource: template.HTML(hsource), //nolint:gosec + }) + if err != nil { + h.logger.Error("unable to render helper", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmDirectory(w io.Writer, gnourl *GnoURL) (status int, err error) { + pkgPath := gnourl.Path + + files, err := h.webcli.Sources(pkgPath) + if err != nil { + h.logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + if len(files) == 0 { + h.logger.Debug("no files available", "path", gnourl.Path) + return http.StatusOK, components.RenderStatusComponent(w, "no files available") + } + + err = components.RenderDirectoryComponent(w, components.DirData{ + PkgPath: gnourl.Path, + Files: files, + FileCounter: len(files), + }) + if err != nil { + h.logger.Error("unable to render directory", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) highlightSource(fileName string, src []byte) ([]byte, error) { + var lexer chroma.Lexer + + switch strings.ToLower(filepath.Ext(fileName)) { + case ".gno": + lexer = lexers.Get("go") + case ".md": + lexer = lexers.Get("markdown") + case ".mod": + lexer = lexers.Get("gomod") + default: + lexer = lexers.Get("txt") // file kind not supported, fallback on `.txt` + } + + if lexer == nil { + return nil, fmt.Errorf("unsupported lexer for file %q", fileName) + } + + iterator, err := lexer.Tokenise(nil, string(src)) + if err != nil { + h.logger.Error("unable to ", "fileName", fileName, "err", err) + } + + var buff bytes.Buffer + if err := h.formatter.Format(&buff, iterator); err != nil { + return nil, fmt.Errorf("unable to format source file %q: %w", fileName, err) + } + + return buff.Bytes(), nil +} + +func generateBreadcrumbPaths(path string) []components.BreadcrumbPart { + split := strings.Split(path, "/") + parts := []components.BreadcrumbPart{} + + var name string + for i := range split { + if name = split[i]; name == "" { + continue + } + + parts = append(parts, components.BreadcrumbPart{ + Name: name, + Path: strings.Join(split[:i+1], "/"), + }) + } + + return parts +} + +// IsFile checks if the last element of the path is a file (has an extension) +func isFile(path string) bool { + base := filepath.Base(path) + ext := filepath.Ext(base) + return ext != "" +} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting.go b/gno.land/pkg/gnoweb/markdown/highlighting.go new file mode 100644 index 00000000000..51c66674df1 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/highlighting.go @@ -0,0 +1,588 @@ +// This file was copied from https://github.com/yuin/goldmark-highlighting +// +// package highlighting is an extension for the goldmark(http://github.com/yuin/goldmark). +// +// This extension adds syntax-highlighting to the fenced code blocks using +// chroma(https://github.com/alecthomas/chroma). +package markdown + +import ( + "bytes" + "io" + "strconv" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" +) + +// ImmutableAttributes is a read-only interface for ast.Attributes. +type ImmutableAttributes interface { + // Get returns (value, true) if an attribute associated with given + // name exists, otherwise (nil, false) + Get(name []byte) (interface{}, bool) + + // GetString returns (value, true) if an attribute associated with given + // name exists, otherwise (nil, false) + GetString(name string) (interface{}, bool) + + // All returns all attributes. + All() []ast.Attribute +} + +type immutableAttributes struct { + n ast.Node +} + +func (a *immutableAttributes) Get(name []byte) (interface{}, bool) { + return a.n.Attribute(name) +} + +func (a *immutableAttributes) GetString(name string) (interface{}, bool) { + return a.n.AttributeString(name) +} + +func (a *immutableAttributes) All() []ast.Attribute { + if a.n.Attributes() == nil { + return []ast.Attribute{} + } + return a.n.Attributes() +} + +// CodeBlockContext holds contextual information of code highlighting. +type CodeBlockContext interface { + // Language returns (language, true) if specified, otherwise (nil, false). + Language() ([]byte, bool) + + // Highlighted returns true if this code block can be highlighted, otherwise false. + Highlighted() bool + + // Attributes return attributes of the code block. + Attributes() ImmutableAttributes +} + +type codeBlockContext struct { + language []byte + highlighted bool + attributes ImmutableAttributes +} + +func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext { + return &codeBlockContext{ + language: language, + highlighted: highlighted, + attributes: attrs, + } +} + +func (c *codeBlockContext) Language() ([]byte, bool) { + if c.language != nil { + return c.language, true + } + return nil, false +} + +func (c *codeBlockContext) Highlighted() bool { + return c.highlighted +} + +func (c *codeBlockContext) Attributes() ImmutableAttributes { + return c.attributes +} + +// WrapperRenderer renders wrapper elements like div, pre, etc. +type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool) + +// CodeBlockOptions creates Chroma options per code block. +type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option + +// Config struct holds options for the extension. +type Config struct { + html.Config + + // Style is a highlighting style. + // Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters. + Style string + + // Pass in a custom Chroma style. If this is not nil, the Style string will be ignored + CustomStyle *chroma.Style + + // If set, will try to guess language if none provided. + // If the guessing fails, we will fall back to a text lexer. + // Note that while Chroma's API supports language guessing, the implementation + // is not there yet, so you will currently always get the basic text lexer. + GuessLanguage bool + + // FormatOptions is a option related to output formats. + // See https://github.com/alecthomas/chroma#the-html-formatter for details. + FormatOptions []chromahtml.Option + + // CSSWriter is an io.Writer that will be used as CSS data output buffer. + // If WithClasses() is enabled, you can get CSS data corresponds to the style. + CSSWriter io.Writer + + // CodeBlockOptions allows set Chroma options per code block. + CodeBlockOptions CodeBlockOptions + + // WrapperRenderer allows you to change wrapper elements. + WrapperRenderer WrapperRenderer +} + +// NewConfig returns a new Config with defaults. +func NewConfig() Config { + return Config{ + Config: html.NewConfig(), + Style: "github", + FormatOptions: []chromahtml.Option{}, + CSSWriter: nil, + WrapperRenderer: nil, + CodeBlockOptions: nil, + } +} + +// SetOption implements renderer.SetOptioner. +func (c *Config) SetOption(name renderer.OptionName, value interface{}) { + switch name { + case optStyle: + c.Style = value.(string) + case optCustomStyle: + c.CustomStyle = value.(*chroma.Style) + case optFormatOptions: + if value != nil { + c.FormatOptions = value.([]chromahtml.Option) + } + case optCSSWriter: + c.CSSWriter = value.(io.Writer) + case optWrapperRenderer: + c.WrapperRenderer = value.(WrapperRenderer) + case optCodeBlockOptions: + c.CodeBlockOptions = value.(CodeBlockOptions) + case optGuessLanguage: + c.GuessLanguage = value.(bool) + default: + c.Config.SetOption(name, value) + } +} + +// Option interface is a functional option interface for the extension. +type Option interface { + renderer.Option + // SetHighlightingOption sets given option to the extension. + SetHighlightingOption(*Config) +} + +type withHTMLOptions struct { + value []html.Option +} + +func (o *withHTMLOptions) SetConfig(c *renderer.Config) { + if o.value != nil { + for _, v := range o.value { + v.(renderer.Option).SetConfig(c) + } + } +} + +func (o *withHTMLOptions) SetHighlightingOption(c *Config) { + if o.value != nil { + for _, v := range o.value { + v.SetHTMLOption(&c.Config) + } + } +} + +// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options. +func WithHTMLOptions(opts ...html.Option) Option { + return &withHTMLOptions{opts} +} + +const ( + optStyle renderer.OptionName = "HighlightingStyle" + optCustomStyle renderer.OptionName = "HighlightingCustomStyle" +) + +var highlightLinesAttrName = []byte("hl_lines") + +var ( + styleAttrName = []byte("hl_style") + nohlAttrName = []byte("nohl") + linenosAttrName = []byte("linenos") + linenosTableAttrValue = []byte("table") + linenosInlineAttrValue = []byte("inline") + linenostartAttrName = []byte("linenostart") +) + +type withStyle struct { + value string +} + +func (o *withStyle) SetConfig(c *renderer.Config) { + c.Options[optStyle] = o.value +} + +func (o *withStyle) SetHighlightingOption(c *Config) { + c.Style = o.value +} + +// WithStyle is a functional option that changes highlighting style. +func WithStyle(style string) Option { + return &withStyle{style} +} + +type withCustomStyle struct { + value *chroma.Style +} + +func (o *withCustomStyle) SetConfig(c *renderer.Config) { + c.Options[optCustomStyle] = o.value +} + +func (o *withCustomStyle) SetHighlightingOption(c *Config) { + c.CustomStyle = o.value +} + +// WithStyle is a functional option that changes highlighting style. +func WithCustomStyle(style *chroma.Style) Option { + return &withCustomStyle{style} +} + +const optCSSWriter renderer.OptionName = "HighlightingCSSWriter" + +type withCSSWriter struct { + value io.Writer +} + +func (o *withCSSWriter) SetConfig(c *renderer.Config) { + c.Options[optCSSWriter] = o.value +} + +func (o *withCSSWriter) SetHighlightingOption(c *Config) { + c.CSSWriter = o.value +} + +// WithCSSWriter is a functional option that sets io.Writer for CSS data. +func WithCSSWriter(w io.Writer) Option { + return &withCSSWriter{w} +} + +const optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage" + +type withGuessLanguage struct { + value bool +} + +func (o *withGuessLanguage) SetConfig(c *renderer.Config) { + c.Options[optGuessLanguage] = o.value +} + +func (o *withGuessLanguage) SetHighlightingOption(c *Config) { + c.GuessLanguage = o.value +} + +// WithGuessLanguage is a functional option that toggles language guessing +// if none provided. +func WithGuessLanguage(b bool) Option { + return &withGuessLanguage{value: b} +} + +const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer" + +type withWrapperRenderer struct { + value WrapperRenderer +} + +func (o *withWrapperRenderer) SetConfig(c *renderer.Config) { + c.Options[optWrapperRenderer] = o.value +} + +func (o *withWrapperRenderer) SetHighlightingOption(c *Config) { + c.WrapperRenderer = o.value +} + +// WithWrapperRenderer is a functional option that sets WrapperRenderer that +// renders wrapper elements like div, pre, etc. +func WithWrapperRenderer(w WrapperRenderer) Option { + return &withWrapperRenderer{w} +} + +const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions" + +type withCodeBlockOptions struct { + value CodeBlockOptions +} + +func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) { + c.Options[optCodeBlockOptions] = o.value +} + +func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) { + c.CodeBlockOptions = o.value +} + +// WithCodeBlockOptions is a functional option that sets CodeBlockOptions that +// allows setting Chroma options per code block. +func WithCodeBlockOptions(c CodeBlockOptions) Option { + return &withCodeBlockOptions{value: c} +} + +const optFormatOptions renderer.OptionName = "HighlightingFormatOptions" + +type withFormatOptions struct { + value []chromahtml.Option +} + +func (o *withFormatOptions) SetConfig(c *renderer.Config) { + if _, ok := c.Options[optFormatOptions]; !ok { + c.Options[optFormatOptions] = []chromahtml.Option{} + } + c.Options[optFormatOptions] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...) +} + +func (o *withFormatOptions) SetHighlightingOption(c *Config) { + c.FormatOptions = append(c.FormatOptions, o.value...) +} + +// WithFormatOptions is a functional option that wraps chroma HTML formatter options. +func WithFormatOptions(opts ...chromahtml.Option) Option { + return &withFormatOptions{opts} +} + +// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension. +type HTMLRenderer struct { + Config +} + +// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it. +func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer { + r := &HTMLRenderer{ + Config: NewConfig(), + } + for _, opt := range opts { + opt.SetHighlightingOption(&r.Config) + } + return r +} + +// RegisterFuncs implements NodeRenderer.RegisterFuncs. +func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) +} + +func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes { + if node.Attributes() != nil { + return &immutableAttributes{node} + } + if infostr != nil { + attrStartIdx := -1 + + for idx, char := range infostr { + if char == '{' { + attrStartIdx = idx + break + } + } + if attrStartIdx > 0 { + n := ast.NewTextBlock() // dummy node for storing attributes + attrStr := infostr[attrStartIdx:] + if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { + for _, attr := range attrs { + n.SetAttribute(attr.Name, attr.Value) + } + return &immutableAttributes{n} + } + } + } + return nil +} + +func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.FencedCodeBlock) + if !entering { + return ast.WalkContinue, nil + } + language := n.Language(source) + + chromaFormatterOptions := make([]chromahtml.Option, 0, len(r.FormatOptions)) + for _, opt := range r.FormatOptions { + chromaFormatterOptions = append(chromaFormatterOptions, opt) + } + + style := r.CustomStyle + if style == nil { + style = styles.Get(r.Style) + } + nohl := false + + var info []byte + if n.Info != nil { + info = n.Info.Segment.Value(source) + } + attrs := getAttributes(n, info) + if attrs != nil { + baseLineNumber := 1 + if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok { + if linenostart, ok := linenostartAttr.(float64); ok { + baseLineNumber = int(linenostart) + chromaFormatterOptions = append( + chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber), + ) + } + } + if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr { + if lines, ok := linesAttr.([]interface{}); ok { + var hlRanges [][2]int + for _, l := range lines { + if ln, ok := l.(float64); ok { + hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1}) + } + if rng, ok := l.([]uint8); ok { + slices := strings.Split(string(rng), "-") + lhs, err := strconv.Atoi(slices[0]) + if err != nil { + continue + } + rhs := lhs + if len(slices) > 1 { + rhs, err = strconv.Atoi(slices[1]) + if err != nil { + continue + } + } + hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1}) + } + } + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges)) + } + } + if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr { + if st, ok := styleAttr.([]uint8); ok { + styleStr := string(st) + style = styles.Get(styleStr) + } + } + if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr { + nohl = true + } + + if linenosAttr, ok := attrs.Get(linenosAttrName); ok { + switch v := linenosAttr.(type) { + case bool: + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v)) + case []uint8: + if v != nil { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true)) + } + if bytes.Equal(v, linenosTableAttrValue) { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true)) + } else if bytes.Equal(v, linenosInlineAttrValue) { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false)) + } + } + } + } + + var lexer chroma.Lexer + if language != nil { + lexer = lexers.Get(string(language)) + } + if !nohl && (lexer != nil || r.GuessLanguage) { + if style == nil { + style = styles.Fallback + } + var buffer bytes.Buffer + l := n.Lines().Len() + for i := 0; i < l; i++ { + line := n.Lines().At(i) + buffer.Write(line.Value(source)) + } + + if lexer == nil { + lexer = lexers.Analyse(buffer.String()) + if lexer == nil { + lexer = lexers.Fallback + } + language = []byte(strings.ToLower(lexer.Config().Name)) + } + lexer = chroma.Coalesce(lexer) + + iterator, err := lexer.Tokenise(nil, buffer.String()) + if err == nil { + c := newCodeBlockContext(language, true, attrs) + + if r.CodeBlockOptions != nil { + chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...) + } + formatter := chromahtml.New(chromaFormatterOptions...) + if r.WrapperRenderer != nil { + r.WrapperRenderer(w, c, true) + } + _ = formatter.Format(w, style, iterator) == nil + if r.WrapperRenderer != nil { + r.WrapperRenderer(w, c, false) + } + if r.CSSWriter != nil { + _ = formatter.WriteCSS(r.CSSWriter, style) + } + return ast.WalkContinue, nil + } + } + + var c CodeBlockContext + if r.WrapperRenderer != nil { + c = newCodeBlockContext(language, false, attrs) + r.WrapperRenderer(w, c, true) + } else { + _, _ = w.WriteString("

')
+	}
+	l := n.Lines().Len()
+	for i := 0; i < l; i++ {
+		line := n.Lines().At(i)
+		r.Writer.RawWrite(w, line.Value(source))
+	}
+	if r.WrapperRenderer != nil {
+		r.WrapperRenderer(w, c, false)
+	} else {
+		_, _ = w.WriteString("
\n") + } + return ast.WalkContinue, nil +} + +type highlighting struct { + options []Option +} + +// Highlighting is a goldmark.Extender implementation. +var Highlighting = &highlighting{ + options: []Option{}, +} + +// NewHighlighting returns a new extension with given options. +func NewHighlighting(opts ...Option) goldmark.Extender { + return &highlighting{ + options: opts, + } +} + +// Extend implements goldmark.Extender. +func (e *highlighting) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewHTMLRenderer(e.options...), 200), + )) +} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting_test.go b/gno.land/pkg/gnoweb/markdown/highlighting_test.go new file mode 100644 index 00000000000..25bc4fedd61 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/highlighting_test.go @@ -0,0 +1,568 @@ +// This file was copied from https://github.com/yuin/goldmark-highlighting + +package markdown + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/testutil" + "github.com/yuin/goldmark/util" +) + +func TestHighlighting(t *testing.T) { + var css bytes.Buffer + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithStyle("monokai"), + WithCSSWriter(&css), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + ), + WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { + _, ok := c.Language() + if entering { + if !ok { + w.WriteString("
")
+							return
+						}
+						w.WriteString(`
`) + } else { + if !ok { + w.WriteString("
") + return + } + w.WriteString(`
`) + } + }), + WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { + if language, ok := c.Language(); ok { + // Turn on line numbers for Go only. + if string(language) == "go" { + return []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + } + } + } + return nil + }), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"``` go\n"+`func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
1func main() {
+2    fmt.Println("ok")
+3}
+
+`) { + t.Errorf("failed to render HTML\n%s", buffer.String()) + } + + expected := strings.TrimSpace(`/* Background */ .bg { color: #f8f8f2; background-color: #272822; } +/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; } +/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #f8f8f2; background-color: #3c3d38 } +/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #f8f8f2; background-color: #3c3d38 } +/* Error */ .chroma .err { color: #960050; background-color: #1e0010 } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #3c3d38 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #66d9ef } +/* KeywordConstant */ .chroma .kc { color: #66d9ef } +/* KeywordDeclaration */ .chroma .kd { color: #66d9ef } +/* KeywordNamespace */ .chroma .kn { color: #f92672 } +/* KeywordPseudo */ .chroma .kp { color: #66d9ef } +/* KeywordReserved */ .chroma .kr { color: #66d9ef } +/* KeywordType */ .chroma .kt { color: #66d9ef } +/* NameAttribute */ .chroma .na { color: #a6e22e } +/* NameClass */ .chroma .nc { color: #a6e22e } +/* NameConstant */ .chroma .no { color: #66d9ef } +/* NameDecorator */ .chroma .nd { color: #a6e22e } +/* NameException */ .chroma .ne { color: #a6e22e } +/* NameFunction */ .chroma .nf { color: #a6e22e } +/* NameOther */ .chroma .nx { color: #a6e22e } +/* NameTag */ .chroma .nt { color: #f92672 } +/* Literal */ .chroma .l { color: #ae81ff } +/* LiteralDate */ .chroma .ld { color: #e6db74 } +/* LiteralString */ .chroma .s { color: #e6db74 } +/* LiteralStringAffix */ .chroma .sa { color: #e6db74 } +/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } +/* LiteralStringChar */ .chroma .sc { color: #e6db74 } +/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } +/* LiteralStringDoc */ .chroma .sd { color: #e6db74 } +/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } +/* LiteralStringEscape */ .chroma .se { color: #ae81ff } +/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } +/* LiteralStringInterpol */ .chroma .si { color: #e6db74 } +/* LiteralStringOther */ .chroma .sx { color: #e6db74 } +/* LiteralStringRegex */ .chroma .sr { color: #e6db74 } +/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } +/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } +/* LiteralNumber */ .chroma .m { color: #ae81ff } +/* LiteralNumberBin */ .chroma .mb { color: #ae81ff } +/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } +/* LiteralNumberHex */ .chroma .mh { color: #ae81ff } +/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } +/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } +/* LiteralNumberOct */ .chroma .mo { color: #ae81ff } +/* Operator */ .chroma .o { color: #f92672 } +/* OperatorWord */ .chroma .ow { color: #f92672 } +/* Comment */ .chroma .c { color: #75715e } +/* CommentHashbang */ .chroma .ch { color: #75715e } +/* CommentMultiline */ .chroma .cm { color: #75715e } +/* CommentSingle */ .chroma .c1 { color: #75715e } +/* CommentSpecial */ .chroma .cs { color: #75715e } +/* CommentPreproc */ .chroma .cp { color: #75715e } +/* CommentPreprocFile */ .chroma .cpf { color: #75715e } +/* GenericDeleted */ .chroma .gd { color: #f92672 } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericInserted */ .chroma .gi { color: #a6e22e } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #75715e }`) + + gotten := strings.TrimSpace(css.String()) + + if expected != gotten { + diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) + t.Errorf("incorrect CSS.\n%s", string(diff)) + } +} + +func TestHighlighting2(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Highlighting, + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"```"+` +func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
func main() {
+    fmt.Println("ok")
+}
+
+`) { + t.Error("failed to render HTML") + } +} + +func TestHighlighting3(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Highlighting, + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= + +`+"```"+`cpp {hl_lines=[1,2]} +#include +int main() { + std::cout<< "hello" << std::endl; +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
#include <iostream>
+int main() {
+    std::cout<< "hello" << std::endl;
+}
+
+`) { + t.Errorf("failed to render HTML:\n%s", buffer.String()) + } +} + +func TestHighlightingCustom(t *testing.T) { + custom := chroma.MustNewStyle("custom", chroma.StyleEntries{ + chroma.Background: "#cccccc bg:#1d1d1d", + chroma.Comment: "#999999", + chroma.CommentSpecial: "#cd0000", + chroma.Keyword: "#cc99cd", + chroma.KeywordDeclaration: "#cc99cd", + chroma.KeywordNamespace: "#cc99cd", + chroma.KeywordType: "#cc99cd", + chroma.Operator: "#67cdcc", + chroma.OperatorWord: "#cdcd00", + chroma.NameClass: "#f08d49", + chroma.NameBuiltin: "#f08d49", + chroma.NameFunction: "#f08d49", + chroma.NameException: "bold #666699", + chroma.NameVariable: "#00cdcd", + chroma.LiteralString: "#7ec699", + chroma.LiteralNumber: "#f08d49", + chroma.LiteralStringBoolean: "#f08d49", + chroma.GenericHeading: "bold #000080", + chroma.GenericSubheading: "bold #800080", + chroma.GenericDeleted: "#e2777a", + chroma.GenericInserted: "#cc99cd", + chroma.GenericError: "#e2777a", + chroma.GenericEmph: "italic", + chroma.GenericStrong: "bold", + chroma.GenericPrompt: "bold #000080", + chroma.GenericOutput: "#888", + chroma.GenericTraceback: "#04D", + chroma.GenericUnderline: "underline", + chroma.Error: "border:#e2777a", + }) + + var css bytes.Buffer + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithStyle("monokai"), // to make sure it is overrided even if present + WithCustomStyle(custom), + WithCSSWriter(&css), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + ), + WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { + _, ok := c.Language() + if entering { + if !ok { + w.WriteString("
")
+							return
+						}
+						w.WriteString(`
`) + } else { + if !ok { + w.WriteString("
") + return + } + w.WriteString(`
`) + } + }), + WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { + if language, ok := c.Language(); ok { + // Turn on line numbers for Go only. + if string(language) == "go" { + return []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + } + } + } + return nil + }), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"``` go\n"+`func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
1func main() {
+2    fmt.Println("ok")
+3}
+
+`) { + t.Error("failed to render HTML", buffer.String()) + } + + expected := strings.TrimSpace(`/* Background */ .bg { color: #cccccc; background-color: #1d1d1d; } +/* PreWrapper */ .chroma { color: #cccccc; background-color: #1d1d1d; } +/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #cccccc; background-color: #333333 } +/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #cccccc; background-color: #333333 } +/* Error */ .chroma .err { } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #333333 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #cc99cd } +/* KeywordConstant */ .chroma .kc { color: #cc99cd } +/* KeywordDeclaration */ .chroma .kd { color: #cc99cd } +/* KeywordNamespace */ .chroma .kn { color: #cc99cd } +/* KeywordPseudo */ .chroma .kp { color: #cc99cd } +/* KeywordReserved */ .chroma .kr { color: #cc99cd } +/* KeywordType */ .chroma .kt { color: #cc99cd } +/* NameBuiltin */ .chroma .nb { color: #f08d49 } +/* NameClass */ .chroma .nc { color: #f08d49 } +/* NameException */ .chroma .ne { color: #666699; font-weight: bold } +/* NameFunction */ .chroma .nf { color: #f08d49 } +/* NameVariable */ .chroma .nv { color: #00cdcd } +/* LiteralString */ .chroma .s { color: #7ec699 } +/* LiteralStringAffix */ .chroma .sa { color: #7ec699 } +/* LiteralStringBacktick */ .chroma .sb { color: #7ec699 } +/* LiteralStringChar */ .chroma .sc { color: #7ec699 } +/* LiteralStringDelimiter */ .chroma .dl { color: #7ec699 } +/* LiteralStringDoc */ .chroma .sd { color: #7ec699 } +/* LiteralStringDouble */ .chroma .s2 { color: #7ec699 } +/* LiteralStringEscape */ .chroma .se { color: #7ec699 } +/* LiteralStringHeredoc */ .chroma .sh { color: #7ec699 } +/* LiteralStringInterpol */ .chroma .si { color: #7ec699 } +/* LiteralStringOther */ .chroma .sx { color: #7ec699 } +/* LiteralStringRegex */ .chroma .sr { color: #7ec699 } +/* LiteralStringSingle */ .chroma .s1 { color: #7ec699 } +/* LiteralStringSymbol */ .chroma .ss { color: #7ec699 } +/* LiteralNumber */ .chroma .m { color: #f08d49 } +/* LiteralNumberBin */ .chroma .mb { color: #f08d49 } +/* LiteralNumberFloat */ .chroma .mf { color: #f08d49 } +/* LiteralNumberHex */ .chroma .mh { color: #f08d49 } +/* LiteralNumberInteger */ .chroma .mi { color: #f08d49 } +/* LiteralNumberIntegerLong */ .chroma .il { color: #f08d49 } +/* LiteralNumberOct */ .chroma .mo { color: #f08d49 } +/* Operator */ .chroma .o { color: #67cdcc } +/* OperatorWord */ .chroma .ow { color: #cdcd00 } +/* Comment */ .chroma .c { color: #999999 } +/* CommentHashbang */ .chroma .ch { color: #999999 } +/* CommentMultiline */ .chroma .cm { color: #999999 } +/* CommentSingle */ .chroma .c1 { color: #999999 } +/* CommentSpecial */ .chroma .cs { color: #cd0000 } +/* CommentPreproc */ .chroma .cp { color: #999999 } +/* CommentPreprocFile */ .chroma .cpf { color: #999999 } +/* GenericDeleted */ .chroma .gd { color: #e2777a } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericError */ .chroma .gr { color: #e2777a } +/* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold } +/* GenericInserted */ .chroma .gi { color: #cc99cd } +/* GenericOutput */ .chroma .go { color: #888888 } +/* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold } +/* GenericTraceback */ .chroma .gt { color: #0044dd } +/* GenericUnderline */ .chroma .gl { text-decoration: underline }`) + + gotten := strings.TrimSpace(css.String()) + + if expected != gotten { + diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) + t.Errorf("incorrect CSS.\n%s", string(diff)) + } +} + +func TestHighlightingHlLines(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithFormatOptions( + chromahtml.WithClasses(true), + ), + ), + ), + ) + + for i, test := range []struct { + attributes string + expect []int + }{ + {`hl_lines=["2"]`, []int{2}}, + {`hl_lines=["2-3",5],linenostart=5`, []int{2, 3, 5}}, + {`hl_lines=["2-3"]`, []int{2, 3}}, + {`hl_lines=["2-3",5],linenostart="5"`, []int{2, 3}}, // linenostart must be a number. string values are ignored + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + var buffer bytes.Buffer + codeBlock := fmt.Sprintf(`bash {%s} +LINE1 +LINE2 +LINE3 +LINE4 +LINE5 +LINE6 +LINE7 +LINE8 +`, test.attributes) + + if err := markdown.Convert([]byte(` +`+"```"+codeBlock+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + for _, line := range test.expect { + expectStr := fmt.Sprintf("LINE%d\n", line) + if !strings.Contains(buffer.String(), expectStr) { + t.Fatal("got\n", buffer.String(), "\nexpected\n", expectStr) + } + } + }) + } +} + +type nopPreWrapper struct{} + +// Start is called to write a start
 element.
+func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
+
+// End is called to write the end 
element. +func (nopPreWrapper) End(code bool) string { return "" } + +func TestHighlightingLinenos(t *testing.T) { + outputLineNumbersInTable := `
+ +
+1 + +LINE1 +
+
` + + for i, test := range []struct { + attributes string + lineNumbers bool + lineNumbersInTable bool + expect string + }{ + {`linenos=true`, false, false, `1LINE1 +`}, + {`linenos=false`, false, false, `LINE1 +`}, + {``, true, false, `1LINE1 +`}, + {``, true, true, outputLineNumbersInTable}, + {`linenos=inline`, true, true, `1LINE1 +`}, + {`linenos=foo`, false, false, `1LINE1 +`}, + {`linenos=table`, false, false, outputLineNumbersInTable}, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithFormatOptions( + chromahtml.WithLineNumbers(test.lineNumbers), + chromahtml.LineNumbersInTable(test.lineNumbersInTable), + chromahtml.WithPreWrapper(nopPreWrapper{}), + chromahtml.WithClasses(true), + ), + ), + ), + ) + + var buffer bytes.Buffer + codeBlock := fmt.Sprintf(`bash {%s} +LINE1 +`, test.attributes) + + content := "```" + codeBlock + "```" + + if err := markdown.Convert([]byte(content), &buffer); err != nil { + t.Fatal(err) + } + + s := strings.TrimSpace(buffer.String()) + + if s != test.expect { + t.Fatal("got\n", s, "\nexpected\n", test.expect) + } + }) + } +} + +func TestHighlightingGuessLanguage(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithGuessLanguage(true), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), + ), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte("```"+` +LINE +`+"```"), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +
1LINE
+
+`) { + t.Errorf("render mismatch, got\n%s", buffer.String()) + } +} + +func TestCoalesceNeeded(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + // WithGuessLanguage(true), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), + ), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte("```http"+` +GET /foo HTTP/1.1 +Content-Type: application/json +User-Agent: foo + +{ + "hello": "world" +} +`+"```"), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +
1GET /foo HTTP/1.1
+2Content-Type: application/json
+3User-Agent: foo
+4
+5{
+6  "hello": "world"
+7}
+
+`) { + t.Errorf("render mismatch, got\n%s", buffer.String()) + } +} diff --git a/gno.land/pkg/gnoweb/markdown/toc.go b/gno.land/pkg/gnoweb/markdown/toc.go new file mode 100644 index 00000000000..59d4941fabf --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/toc.go @@ -0,0 +1,137 @@ +// This file is a minimal version of https://github.com/abhinav/goldmark-toc + +package markdown + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/util" +) + +const MaxDepth = 6 + +type Toc struct { + Items []*TocItem +} + +type TocItem struct { + // Title of this item in the table of contents. + // + // This may be blank for items that don't refer to a heading, and only + // have sub-items. + Title []byte + + // ID is the identifier for the heading that this item refers to. This + // is the fragment portion of the link without the "#". + // + // This may be blank if the item doesn't have an id assigned to it, or + // if it doesn't have a title. + // + // Enable AutoHeadingID in your parser if you expected these to be set + // but they weren't. + ID []byte + + // Items references children of this item. + // + // For a heading at level 3, Items, contains the headings at level 4 + // under that section. + Items []*TocItem +} + +func (i TocItem) Anchor() string { + return "#" + string(i.ID) +} + +type TocOptions struct { + MinDepth, MaxDepth int +} + +func TocInspect(n ast.Node, src []byte, opts TocOptions) (*Toc, error) { + // Appends an empty subitem to the given node + // and returns a reference to it. + appendChild := func(n *TocItem) *TocItem { + child := new(TocItem) + n.Items = append(n.Items, child) + return child + } + + // Returns the last subitem of the given node, + // creating it if necessary. + lastChild := func(n *TocItem) *TocItem { + if len(n.Items) > 0 { + return n.Items[len(n.Items)-1] + } + return appendChild(n) + } + + var root TocItem + + stack := []*TocItem{&root} // inv: len(stack) >= 1 + err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + // Skip non-heading node + heading, ok := n.(*ast.Heading) + if !ok { + return ast.WalkContinue, nil + } + + if opts.MinDepth > 0 && heading.Level < opts.MinDepth { + return ast.WalkSkipChildren, nil + } + + if opts.MaxDepth > 0 && heading.Level > opts.MaxDepth { + return ast.WalkSkipChildren, nil + } + + // The heading is deeper than the current depth. + // Append empty items to match the heading's level. + for len(stack) < heading.Level { + parent := stack[len(stack)-1] + stack = append(stack, lastChild(parent)) + } + + // The heading is shallower than the current depth. + // Move back up the stack until we reach the heading's level. + if len(stack) > heading.Level { + stack = stack[:heading.Level] + } + + parent := stack[len(stack)-1] + target := lastChild(parent) + if len(target.Title) > 0 || len(target.Items) > 0 { + target = appendChild(parent) + } + + target.Title = util.UnescapePunctuations(heading.Text(src)) + if id, ok := n.AttributeString("id"); ok { + target.ID, _ = id.([]byte) + } + + return ast.WalkSkipChildren, nil + }) + + root.Items = compactItems(root.Items) + + return &Toc{Items: root.Items}, err +} + +// compactItems removes items with no titles +// from the given list of items. +// +// Children of removed items will be promoted to the parent item. +func compactItems(items []*TocItem) []*TocItem { + result := make([]*TocItem, 0) + for _, item := range items { + if len(item.Title) == 0 { + result = append(result, compactItems(item.Items)...) + continue + } + + item.Items = compactItems(item.Items) + result = append(result, item) + } + + return result +} diff --git a/gno.land/pkg/gnoweb/public/favicon.ico b/gno.land/pkg/gnoweb/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..528c362c44a80fe29bab63303a6e7a3a05e43ce5 GIT binary patch literal 7406 zcmeHMSxi$|82(D3w6~=!EiEljDr+fFmI4a0l(Ln|(kU1&~ zpL}qMThJ$CjGE|!4<;tQ_@X`;G)9g4miVA?nR9Nrr5)NrW+szNrhjsK&VT;zKezpF zoAZBP0Ur3s$^sdMSdaqw0JPdAUkEIcwXiUUKNtl3t^{-hLsHO9E}F-h$T6_5yybao zYb$DMYEW2Mh>ssXA|xaPM~@!G{{8!rnwpBIPoHAv&Ye)J)ewut=Hxp`FQf=2~tv0uw%y#L`6kme0&@tkqCCX9cRy;MR|ESa&vQ$l$3-=j~-$B z_U%w86u5Ke4s13X78VwelaqsY@7^IHAps8`KE&MI9OQC2Mn*<({rYtT1qGqAvlFLJ zpT^g(UrAlE@aD}M%+AgtA|e8OJ|FGv?WnD-#plnTVK5l*>eVY!pD+v#4uZ$y!D_YQ z)2B~3c<>-zym*1>>1jx%Qna+Rz+$oB*s)_MC@8?QXV0)}*DgdyM?)f!ps%kFSFc`0 zb8|DQs;aPW-#*OG&qJkBp{J(@jg5_{sHni6J$vx@@nhV*dlzG4V-O03xN+kK&YU>| zv)K%z(TLrJIJ4qUi!0d;kCICbh2zI^$D%*;%@e*GHZ;o%q>8bUxo0NUExaQ^&xoH%g;MMXu( z$jHFUmoJwex_{=cfWHF&QUw$h0gAQXYIv~KVo@fhuQ6k)ubZ*in7DZ3S`wz^8wr{k z^NVnL)eK2fj>Q2d2|4xFRT2vGn5Es>J1`kjJ`>Va&dnV+v4r~k)X6&Ty>O$h%hwH} z|Fpf$W+CUKcZ*+%S9wPR#TFZ5aeiPrwa~-at4%~QJzIWul!#&UTl9==&?}e(Bz8+8 zrozg|sbndupF$R6_x%LKn<9WBanshVVKwC8#)N$5YkLwiJH9mJRHjyFW(Th?sj4CO z*op=B@NCjdmCFN7=a2rxcSpE;a#_D=Y~(ksWbEWX-}ssqcl?iH-*_~;<9}EMj;cA=1QU7M zzO8r675}Tt!|;;Vk-eNSIp784vfNv_j1OOZd!SRpC8YTL?mO=hnYol87DsB>=OYhs*P!uQuk`NBD4T@7l|#J0C5Y+chEB0qW3 z!%}nZ;Gg&_@L#9^<*8b7naJhjse#1d%!Q|(J9iGxpFd~$DfvOM+?4Xt$;nBUm%4J% z4((tNr#ycAILj$xV`DKeFaW(?@8pkDQ&TK|JaXg+bUGc&87C$tSgx3t zmj|U%$#TT*?rvPZe3|8l@87>?c_QV995=jp@uC|)q}-5lM9K>(AEaEcq@)D*@82i- zfYyx72%2n1s`YYhCz+bC?-JaqjvAQym;pk*cIEwpq zckfpIO^TJc5iAoorClArfU45_fzgxvW0F)XA!vFvP literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 b/gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..365eedc50cd0f46ea35a3176335fc67b51123fb4 GIT binary patch literal 324864 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≫bY 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≥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

6&qaNDxLwZnq2 z`OB6Fw?` zJ9UjS?9|78)&)kh&9|eFM>j?@O*YwRe?i-^Oe!r7Sh=&1XJa`&#b!NSbX#o!?TN(YW_CXsa3Z!Y5Tr~*qViCY&=Qw%f-$!2q!o;p{Jn}0R%?%1R%NyJ4jLAL!lkq2O3hLhSqjP zsa?alV*@f|sDXTFdoKm3f2fFj0=5fmf-R8GX`D-F;U1$-^~QK32Q3peDxhPR!dFp~p6rs>KQEQVM@6u;fV$*+mY30C?-THvWDC&7yW|b(yFtO zlg*eS&_(IYOpNrUUOMU_v<3h)2xj45gh9}QGL&jiKQ`d=gI?OdIx*;>tyll2MJ8^9 z)lRbr$>`B>u{?r|FLeuXB)w1wg@!_G!IY~(rsH9!PU?FQxE_iZRAN69?Ie8;1^e1A z+A6%#J_{~k+RNkZX*C+D1s|beUm;5T-xg7dJy%qWOrlnH9hADhZLomv<^c&%zCR0` zh}6goof{UwgP`8lxwUfO`BFsLd;Fn+B00$c2@j6RiG>`=jU3g)Q!A#IrUxMAvMJ8h zN%Gdp&44=#Ww6;~{{T>N^^*LsA$caRT^Pml;|WE;-WAdSGjNN7LnEWMU&&)E1rWr| zfX0WQ&U}yP=Kz`TgmA-v3A#&aoDjD18`Gc;!0C4O7c3<&#TS>7@M_kAS%X5ojPxK< zBW7Z?!6~I-rWa^`l3H6b9?J;vQgC$K2gR*kbqxd03BOD8U5 zqK<(hmlU%il$u(wMVJgy@S(8Cxx|~sm%cIQ<@mP|iT-6XlA z)WJeJanGh4p^1MB>NsYY^IkBRO9dN_3ueNqg*j`s{FAXbIy;cp0N4V;47G?thqf|G z%UVnEh_}$%5}geS1j+NBy_lRzJF04FEDVDy4@u^z3Z3g3vMVoE14byXMj5Q6gQ^3= z`2G3lv!)6w>Wrr-9P<>3qiPybXgaamn8$w;qMS0!8j#VVV2ZLyX&RVp$lkShKOQt{n)9==Z6@Q{nJZf<1sgkeC}ySH!Z4FkMiSF-_YT z{jl}{>62BCbl9F3BW22hDIpf>3L!EZl>vbkq#m>N+nGmVk>_2i=b{uhNnht^y^Q1_ zrM{Kck{3XP7D5ss_*a0N8wUKHew^yKeYJ!2fa7*~h_BfP8DRDE2j*|;l$Y9abR5BL zBPaMmKfU6=RvGvVKSuSl6I~|n`8zH-GW}RxT)V4&l>hvjDpyUsh z98bt`5lW3iuBk*Yj>Dzo@vx0C?m!4!FZJ$5@D7C0)2e&Ka2AVk7`3OXB|168slH`A z*5^oZ2{UrHIv z;BlmMfs{cReJ%^+w)BtSGd;1oW@1Xf&S63CPH^FtYp50LFNMDeA2 zR#b)WzTW7>FI!exnuGbmi3Rf-&0wOu4FOpRV$k-lDX-isIZmbxt&d8O*kzZYl=Yq4Hv zm%X-n0@DF^vlim5dcWb_93|eH*C7p@MRZc@`f__Q>T5{_c{2QAZQRc|{+PVO2& z{Z6fx_GH&Xov84n_)rzR^5+qY(uT`eD<{HT3kLd%{0T5@9)${1ToJ-9P?&BrIk7p= zejkGt?|wqxKKG#$iu?soLP!wD0O4SI8>fC z1nDVWeLFM1I0r%oikVGVYFXLV+X1Icn(XDwuJp`vC6?rTkL|d3Rg2$38NANJS*&DRkiInGa2J zBem!tW0g@_{JZ;e#Ko^-&4fVw3>z|LKvp^Y$Lql`F>z{eUTUjBqW$CU_$1)&@~Q& zY*|4?s(_qlbrQcn;PVGLj^p{~dXR-k>_!D0n2@Qhm~S<`>rTLIe70?-!LPh${_NXL z{Te5V88>J0Zi7U9CeV>B<-$U8af1` zc$zc6HM`AI75w?Z64anr^(muOM5n?}C)fm!Cz6@wcbPUFW_K-h93 zlMWZ8NIDG3x4cu-HJ+FfznR%LdVO%Ur7GaEf-F{0TRC^M<(}^!n@*=^&lQ?pCSb!! z^G2WcCvwSkZ$tL_BcfPZF;^3jmeRG*QF7~r*Mxv4Jkd~%;bQ6G(8wYOHg=V2rjbeB z38`L0F#GVS1L<1`x|w4hyOlWuhh6O}rwO-CHEr^7zC+^Zk~E8~waO5=lE%;Bun!6( z^pCgw|GbA)$%!3&5a9D4lf;Hc)>ex11^ap0GrdsBn2vMU(Qy6F=%x-(39V4K{wc%2 zG|`0=UD@>iG^QZbfUsT{g#9>~xUaq{k?nEFv*7Xjh9a0{^QacK{?`{-(}du$ed2Qv z+IQ6r)l=lK<@4FKAiVqxump4X;73fWOGgqQh}1LXx9Tot)kPE4utSg4T=!C7gsQm^ zr4=M{*vB|-H*;1$#;mf4sn)Pd zV(SI<1WW2AD8_Pqvhp5a;so3s&L!x@7x+D_2T{e|51_mn9BIb{B)kzNy^so|2Wn5s<+5buMztpKH8a@$)ArazbvAVY& z-DEVn|MD0Wu%#c1>LlXq1nxfvpI~{oWF88C$*Z3Q+jIQNTp`PUeqyEs{%7CQ6$DB~ zl$bp0!3&8rndl23NGxE+1-P(%1%7g>YP}xck9YF1!R;ftXyq{ZTjJQu*#t+~Xyy7< zrrBK^ZS>VCIZ}sBk9|3lU>I*luzfn`iBvyVJ}j*>E9b2rZt-M{s|4u5XXL{&5Oa*R zQ5sez49^RaX7;D@ThQH%Fl?Es2H-T7tm5d^6qVSVogW=ndL^jY4cHj#Jd=!nbGie- zjpqUQkGzPdh|nP6Beb>pRJV3d=@XNWF2%TvSvcL^TAPocnS9k`#jZijmB38NSMF&yQ==N! z7rHs8FFsV#?u>j;mR*55Z^xH`^&RWp`A5F)P3ykU1C{Xu2WTZ1HVN;~hH$iy()t zPNWsgo7RH;Z(id{jaEb!xhpyu+vdW;|9;PUqtPIbx?K`D)*$xnus z7QKzWK&@Xc55AGNDYdo0dY5LNt9ay8XUW5w?&$ zW$wRvc&-NrKE8~`$s_?uua0UrQb(ecebd)M-12|70vk@2_C4eP)iebKi@e)4rgE)& zq?`Zav$Fw|p2z25YFyXLqMxgCal`0|&E&kXx};2p**kQ#w>?HM@3oDLRb6Y(bl+?0O9}UQwbo~Gqo_*WZ~#OGx_)6lJ3{a0 zy)6OL?H|qvHLh(3((x!PNwWsj#DhMqVXAV<2O+{yDC8Dq$!f$Q$^|H;98LL{s5(=l zRY{`@al|v)YRNZ+mAoUWnI|GVKU&=GEHV;zdzxIRVO7?0f%LXHbzlq8KMkRhE!rXQ zcQ`Y>Dztg0zjQv=1b?59sB5~kRBDTAFihwdD)JiU@zi;WqGo-8yL4ZuZ?ueRZU}o= z6Q;;J1D>mz=QXAc3--h4*t0ShSr0a|MH69fBqup-l~Qdw!~{~>RM-Pj%iEK!=u~v8 zRO>wNG4ON`dQ#04<;FGR6Bk(2;istdy;3YPP@x|TY4Lc!pLcdn{mQg9K3AL*-b6P% zod98Q7X?LaCFO!r{3E*sN4d77;tQ37(1V-trdXcmXBuE#p=%c1@P4*dWf3@3!-Yo@Fiqf%`JU9D( zN|5=9P;n3|Sy8KbtB<39`Ze?4_CJn*wnZ3MWina}&$_kx8X5MF>rKyi6kV+H^BCXn z@0O7NvUhs$Jf7U=KDMaWeg5_vsS~M96@tc3OlnZISX2CqwrcO^0i1g36o(1(##D8*gNwxM8m8yA<00jx=OesX4Y!56_pj=Vr(JsCYcIb+Brg!aD$|#&{$`%|Y*Z>MrC}OJ>%Zax`{_s^Tnf3Cs@#sZ% z>0GZr8a0fWTbk!xaq9FTtAERYP$|bO$t7$AvUUYjG^?ooKy#*GBXI4kv!(0j5=(mH z1uguVe`YLrTQ9$YXG#Rx%BGdvRH^1y_BOdVtN>2?a5#c# zx;LHjsc_H;TS=(VFtX>9CFFU&W=A-s84C>GWph`ltPSdulTOvvKK$U~_dG11lh=9%CZi+V>o3Qh-%Za$eTcky}Y(-x%V3z^P zsqxAEPWj|~D{(H;!F6-`Tb!Om9-5N!_hEGe}T=? zpiSB|q|Xor)*0hZ{gXDA60GC-t>!$%HEF0l$S&)6L7u!%8oSjLT*We;G?wK1Ig@aL zw~MDgfaRR8gP--#z%82tqxW?PX2PnsvG8iQ3j*@OrJ0<7B?wz^VgL#5KRMKCo;V{K zZ`jqH-+E*I4rc~X*;5p?6e$RTWXPb^3x7w82FqHI^jrBP;Wlws|~Qx`xv~p>iV>+RVx#%u`Xbj zfEWhj5(Q!xP?#$41K}O#vUCcHP`N&Xy;z2{o=OeK*w=U7rn7v|Y_88uq>heW%HF(m zTD-yC^?_3U!Mt&A=Hz9dl$=-N^LA~?x$-E*4hffqkiGgpw&9`jf|cith60OA zu5s1z_&wvrDb{6T(*k@n4@D~e-5GLXyXtxz3eDuqCcstXnsfZ4qug3`3?|84@)$T6 zp#YBq?-T-_9xiM{>^572&|Blp={LSBo5>1sB96|GuDq6Bxe;rv_t^f53R_V>l(y{&C}Dn*9L zU-v?Sot1y-g=hYS9pWo%BvcME40oE5`I8@JRykCwlDDF-xek1TmjCaG-%!CfE z>F9|#uEQFE@RdFxgsehM(OH{E8;!2RT&O8rzV^zishriQ0iZkbC8{m|lc{0o)A$uy>*UD%_1Sn$WD-D?4m!oyXUb znez-Mj?5lPPj)yvylbw5AFAVj8A-jjZu-WS#3v64eEP4d=DhsCZ?|i~Q@2(jU2>ip z3qzb)X={HB^M{XZCDGSS!m`}v{qD*{(x@Cx*|=;z4PZnaDw+QMPqLkjHKik`8<**ImnWO!8!Me|?I9PBZ3 zl+ur$-S}+9`fHFxuQ)DGAJUsLokRqa9~SJwm$%Y=^?Msv#+b;`oMTkKU#{lVcSThx z3FseBDXT@1zb(h*cudoSB_r8}Fq4%F%cfl0Ov_7DMu7;r1W_280wDjaBy&E+^Sq32 zM5YCB84;-hy!XJp|kOlDD)Q6EZ7`3Tdd!2a4lWVzaJjjX#l0ks=dT}+8zN#Wj0`Q z)frZiY*z5ow;oUb<%C*I$TjubKg2!Q z8~ZdZBM22+L~z%s@E*LLz zbi}EU+Cm-{)t8=9yR?Uutcq1{z-x4^E|qTfxGc0%;BEEOLD~bW81SjMoJ~s=4Vj1>O6{!6uwQv<#HiLv_%Rf zk%=-Fl~)0?DJGIgEUF5t+iZkcdQUMv5b7F63$HCbK3d_NKrFR8Pg^NNZVZ!#O35m| zShF$3?5byRJLVl(26yf8195Ro`U6#m(BP!tZ6xy3G=2SkuhBd6 zIWS^mH`Eur=c9R{r#Xrx*N@g&&waNz(TMDExC+is%4Hz$YNAX+hT0l8`HxpC#6?9L zM&-`bI`zv=bF`0-`ps~nMhJ#Byp2^X#pEI2X79<&GpiOiQK$7M{AGvn5s}M(F8SSe z9UaP^z*>Xz+^+2$cdJm#5{DS)iPT<`;|giC&H^PeA59WbBct8+-h0H;?3^wwy%PY! z;T7uCYXh9~$rbQ7cM1QWJGxzCaNzX(&dNBkQZNzCu`-yHQXaPsKXu(j_!^uZkM`vS z<%=GN5IVXp=UWcJjyeUQuL;J$n)IlL7X$7TN#bD~b+yN@#SfmqOU7@c1F3Q0<;zZ${O^JVPbjYqsG3|-A{ zQr-l2K+W&k^_QhM0x=P9!d!iW6!^yy2-aZ=^YhG}&^r!KC?{-2rfTU*Et8knC<7%@AEiy~wJ*+f&Hz@Xf4}j7 z8@KtJW2LI{gOb_qi9ZT7m!|nhq|o@|hZr@Ej6{n7s`qv@${iKKSCL8n{bGZh_5zhP zMVF3jGn3l^{}hi2tiS7K!AtGy2Z~9y!4{6=B9>yRLAb7Zv7proBjMAstBmL(j>`{{ zU+uL6H58JRwJbSjX>XsOxx=zO(VOn1GrHc~FVLA#itZMGjdvhs-pJ}$odCrPpI9;D zl^>O>!15-LzJ{;ZEG>*Ubr_R=bR#fjRV;}AYeSJ{zKEl4a?(&RV)S0&Azf=F;|2#? zIe~vQ){><$AuOyDs!(ba9u=M!T1l#_;LU#d1#=OzALLx5XQ44_P6CiQ=umHv*h@8J z-74CJ`_G)WF_ohtmMabK2cgFEw*jD&Q_t>CpgkIm_N5n@I2WHD?9d!azzvPBZvm=% zM;p1?z+6s9lyieCUQglu>+S}*cJ#(Sbb4u~`EGv6gxP6-AQ%-`LfO3cxtOGx%xNZ; zu0vgar$ILAHcHK6Z(sf^fIJWwzFKsfd)n!%Pah}18l0D zbh+%90b3A8DYnrcY(D#Qa8{J9ooro#o}8uyGTk zJQj=84<(@VytPF*JrjtXsCgTt`4)u0MSe()2QV-nqlZIC%d2Ebf|4n$*EaufhW+Bp zqah})#@E*tPHJ=dOX!H-xReOUL}yu;FDH0g)%ox;b}2AgQ%?p=Pt1*kaa1N5AHH=_ zKWB#`SiRD4OK0H^HZuRpzAI6=PB`G5Ef*Bs|8qSc^qnavOVP6f;`H=#g%ClRKVEmt zs=4v>p+?w}3hb5gG7ro3M<;2ZmS3?ED#~4h6>e!n^s}BaXR}*)hv#& z-Iq8+&8xsTJW^BO{C35ihL9!_R-qpaXvmnA?K^dRSyQGmbx#w|%l!RX`fw)R>yq@L z)xzi1`BMeut$|$O?D2Domic{-@|GFI)7Li>Zh53V8-^_8S|rWX`KmZgp|X)wizF$F z>Q7_jpIff*cu9o?|I4}$+N-LCzvgoyOtR-roRSG}fv)k!9t7s{wkG#g@Po%#)J6?l z6)rYxomkM#+&OMLGSq2AhE5)Y)vD1%JBcE#wH)+ z8E1>xmYX}A%PeJ@c~)VwYCy_Ip`L@Py(BW<;wELH21->2|J1{kq5ZZ+;=$xnS+Y{o z>aA3-Q~D-%R)w8aD$dpY=>6O1RaD$sqZ0Q)$HJR;5s`*ZU&R(j=6o)F!Nl|Km#6RZ zAI*O!C!naCtrD_*yQwNO_Zt!0nU^~cec;0Lsi`^bIU75XIrwd-#QKqcvv)Fr(eRPT zY`s(lQAbKOQqM8e)c3#|CTsqWB_q>|m~ZMg-&=}o`cEr?F?s=|R?8zOLJv+6Y=mL7 zEH^MG)LV(8%@68iRoQtI>pJSZ5`I}~Y3Edw?|n??l8s|gh6lQ(N!7PWi)#?GqZ>FJ zxn(_^EdeZQqHJGT2|3_KvIXgTpA>y^mH5Dk2dze4R;{*HRjl-Z$+b$(ch77jOiB@` z95t?NOe%j^a8*$W^@4~QKP6JNzdb{(l54@pJ%MW-U&RTcDpuL}B zX?NcJHv;~_dG%)uPgSG=#Z0|SRJ{rA>YN_u(alJo=-yA()G$GU)N5;xsy#^EkJTG} z@%Oo$$_r$)$z%K5b>J`2jv9F7g%hH_x~dAB(i(BdzHIa0gv>&2cOac&5+9FB@!XRh z6k)3>GUB2q&7-*shc`Kn%f&&vW_|r8p0AYGXT>hmH@g0~D;<)1>SakSl}hc`--T>H zN=O-`U&?^8R7DVp5X%n)C97CZUyQ?8g&Z2uWEf4aL0=S&VcAr!_VbL*D2bkur`NwQ zQE|sbJ!61O_H~91>ZhqC%gIDA7nsc=w{FfSChSIfv~#V_I6GokIl6zdKg9y`3C*V?Gv732 zeIatMr>fe_NGFt}Yn0{m1Z-H`Iv0B1p6cw7prCG3M3RY}S=F^UuZ*A96-1Y>a8ml< zDsb(5v1?P&XxGF|l}pc7jY=^UYtdW+eEc~HRP9B9{?@Kxl)V7u`*Ck0P0qEbr+r)~ zzkd$fgKss>Ks}S%wSbTT1a0!4w=A8vR!Dwm*hC?bQ_#D;uG3$_>gDs1&`R1zJiobc zhv<>2puy%7gLKZ6sEWRXj(7VhKS{262}0xm@OXZqxK065@YOAg^$l4y5pX$(9<(6{ z@7s3Gg#LOpR}6DY>UonL3wi0tU1v0LvvFty4KxDkmtgZ!&@@4dxI@uC&wMf!;rlEr z1NLZsqe1R@@SJ(xQu}Tdqn8s#RlX0$0Ys3Xu){cr2*b_tFcKN&#I5xeKik83wYPi z=yqefJ=b0_3Aza`I#YOf8zJ5P`UubwEMZZy*Z|I>1`%n&=YbxAEdo?zN-_%RdaS-$ zztmVr%rQp8`LuG3tF+N$2~3=<^)C7PGdR2W&(EqBjpa}tHXG3aSh+na48Dy zfG-dU@u4ruZ6N8;gChg!X~n4UUICm!g|0@0 z0m8R%1xqm}lQS-ACf=X!ZP=%$gSEZRxm>SlYrSuvQXSx5J+=htZK25i3K!*V^K?sw z4=gqq6gAAkxQ{Flkxhm%@c$?7HGSd77Q`eBcaby9v0w@6(}=}L-wo04cr!A&3Mp*+ zh{p1fFw(p*0x>hKech6Bl@dG7xiHy2z2Nr;{0_(1Gdpup6Jw9#z;w=Cl}f|<))Rv; zX2vkPSPL-gct+BTE&3;O7oQI>RMSVPZ z*9&}w_rAY^quWfeqen}I!>FRZGGDPM3C4^N=Gq#s$W$7IZt%Zl@Q(|peRSNRUcMd( z9GlMGOeT>l8-X>uhjL4ThWq2>wYnA|qz8jN=BFBdoLMXkhI>qoPZePX72*SOi4VXj zxeyxLFB(Nk(|V>v9|UJCdR!951&)0}q{3%u(#&Z7c25r`Zg-TzXPH72h6Pe8MpT@} zc2v-WwzzWQ1=h4Z7o4qFGK^#q$8PO=xrPK!HXPT2KV88{gPVi6tus~_ra3_bL~M~5v)I4P z;03|+0a{(ABexz`%*OKtED>ZS*{kkkD(fOi{;|Ag`r+}*Hk%UxhY-N)GXMhg4D+$a zd_phhLYm*|=EDfCtyC7E4Kp)QtIN-@jL!>SpeR;no?EoAU)~UF|17D04mdtL4?ys3 zfVvH978wQK-NLgkQ&h?M9L1V|L>EekiY8NBJavWuUnWo-b^rvTg-kq&uV6Z6!~@yC z;;ZoJ)va`>Q&kJe>@1~ZUUZ0y+g&o9a`NXOe7t1vKfJ+zxN>~O%y^>FiT9ZMS%NW6 z%=148T4;t=1>dVTzaf~aJ#wphs)|5*(scI4qYx$9P5|a5IM5vV54NoiqQpoT$;{67 zpI4T^P+=hQ<=Y8pR>TFIhjHZ4z(ca)#mI(;XO-J-A}@4hP71!b^GQ8Y1cf2216NmU zhs_r+dOLQ3Q=ovyEUk(^H}?!(n``H%$~O*W4yf%|AFauW9R&)K4vu}CINo~g7IA&2 z#5>dJeCeeinwu$1C&4^DEk&sgX23c6sm z1fdADW;n6E<7LVvaiiL<1>`i4js;N;VeLa0C16J<3 z>sR*7_KhA{9THG+I__Ws9-7CAfHO5V9JlQHq>hZP>fL$C3C$x?;WkAn_o}XjseZvZ z8Q68qza^wM0bw^3duOc8>7w^K?T+*+>dL0P|4XAMvDCC#tB}TIPKgSn?0P0~a*r>< z!a4!6z?c;j^l+}eaeEecYC-D3rCtUcayoV{BWwQm&;yk=Om)5pvqE1 z*0-vp6r`MlISZQWWcF|J5-`rEy8l>E&DH-KDmr+t{|X#4RfLPXulM6P7?`!FxG4$f z_!Vl4TUfwi8tb?jhcRO*Rj9`(g@rm2RVqP3_)$i$d)i`Gw{s+k4ook<7iaD+laV@Z z+KDY-_}Pms{Ni=dfmq`th1G`WCqjd_GpwJ7h&8>h2Cfv>F*!I+Ew8tAn{gP&Ny2jj z&k&C9?$O}!nk*?6C@eYl=X2ykf482MSb{InNGTW1j^3O##xuZcATF|uT8BlQ4ZYMJ zmiK2KUBq_Cw~}(P9QY(pEF;i;BV*Z7T)1y++TA<*yRFgS2bMVeK?FQTv$&`PK7aT+ zWfCH4-Bmuk4Da}{yw_wZ@7pl!Y$8Uq8umm#BW0EjP*{}1{}hX*+ItLAs{y-6aO$E7 zeb&haKrOzXf0PB%fGezN&#jme`R*yJUP3mxG>bWe;-TB+V?P=oD&YKD!0#j7D%D{a zpu9c6Obh}H{53fQiw1VuNOtFH-KRHXqr<@C3P<18m8JH zLV_69D+#ZLYhhSnr$}4vLGBT$*1%nVpfuqY3uVMV1aZXSE&OoD4N|{1Kt{ELv6Tm8 zTVg8>RbDE!VCLKtKds|={9D6!qBninavsG$4968DcL#C-x;GubU zht1t+Sovaus`j6-2#|00t(I3@5E3+t6}~MAc;Lc)cr}l?@S$!tDH!)07|uc30sUug zy=pSRgKP>>;&yZRq08|2!iY*EwsOMSR>fBg4$1 z0)@$X-I}TU;yburZ+6yoMLGYW{h2APzAd^ETs)(ShU%T{z70W+8oAcLHNbYBoyINe`Vf@hYZsf9x!1*#OB@Ipv{%Y+jCc z?J!j^nQ--VR2xytVk%iMRc6chrQ}IEDnv(a#Nqu9%{0y!5Hbqn;ezJv43ASfvY$jm z>Dza?;{b&lX~5WBQc48>a|Rt1+>_z&_E-&OyQmy2gaQ3&3d#n9O1)4DGE@iTt9cxZ znGSVQM#6HdpYM0d1tLc+|Euo+jwmsh9#TsH-8YpI0<80CQsB~ASj{(Fl^{d0Zu6lQ z{#{Zv!HJbxf>XoPBLSgBq2Ph>LA*^5&V>*gkd#WTle{|`9ikDnlDQxPrCvJEDjA+Yb)%zjxQVDgqMO-o&8C$m_!Vh@>9$gw8&;ik;^Yo0E zeLYQ3bL_M`nVtB7+k*9Slo#Oh2`;)uf84Wa04W!;2Oj}+*@MU=;9QhesDb_=lVU0F zn-N~!MRUkrS_4!0SWzN6NRwM4`ci=wO#z??fPp~dO>>3Mtv(q61*0*4`~OX!5nicQ;OcuM->bw&8EifZh%x z>>*?et_>3ffl-dsfU!j?amS7=7aJzPkKv7t|HHA|h9htHIC1njGuiDie)zx)<{zdh zKoBks!3H@S{?WLi*UkE!pCyrAHLsq@ZwEfQ9Jc+(ueavd%xDMWSh&)H8*%f6gNXVf z$~gKqmJI(X!rT4GLD9yjEQ~;`iTn~kG0qB$z%=TRC~b3;Ll8$PeOW{ie*7V>AU3Ny7Xt5+pWiQEO5@Mt z7tr~mRfbQxb-{G%FfTh?h@EKU{e@M0l9l{kqDfc>R}S2fMuEXkY#UTqQ(Y~~^QjWj zu>yI``)k#HSfTzPo1bud^8Di-yB4zS6G(i7={V9L zIxEFOTgqyEtapFHBiuF za%LDrFltp@{$LN$Sg1Db}){z+c6xv=F( zUe3yr1O%QE9`%DpC6?g<%cxh^(EY?wPKXBkTmb$I#*Hyce-;r=WD&)cIZavxu6r(X z1}3y0-$>3vD7yFIvUt(mfR915iC7mJknIR=dm%@_Pm_G3T18QYSg5l8Yui(K*kEJCdE&u-7PykA|(&v z8#vuQkqY6){CgSROz6DACO-esl@zcbF)=y(rQhMLnPBLAnz_RWi%mPaL^4F1o}{BM zw5Z655SV({$Pl6rGU1`ON)*w1y-=0^v67qO`oow;jtIdUf$A2T-cl?e5vY7HanP6AbS*T5`uR3vIL@?l*NY$DuXEx!E%H+nYjI1Q-k{hf3@( z274(oJTd@!eQ4Vuo4woF`G)_2ghM69r(@4QUd+DO!pYBS&dn!Tgyn3j-*21+5Rt76 za8MF2(@=TYwG!%4fUF<{OlojD%wXY8ys;wW#HLd_hX~J;$0_^s-N^^UQr%bZqooEa zTu>C;`4DtBvfxae2hM*t=~vWK{DE}5A0%OsdM&-q+E?t5IE?IdNYWJ`y4eL$AgsVF zp9T~*ly8PWJLo)sqQ4l5*E9&A1|LZ1;D<@xYt>BIe*>RwU))D6^SvB4+J*GW=KcUXP}I|@qTrzlwmtX~&Z-;PGKlM@|A#2h7#0`8vJ zlnJ~TUx4}D1lqWW$1ZyfN;0ioDit1kb9JG-Jkz5@ihahuebcbrRq|4u-@tr@mS9kd@!Cfo?@Xjp8AdrdOi)&Z?i%q z^BU_?d;Q*NP=Cs9wz3V>Z8AC&kMST#O!r~&R1B6_AM$k~7@-6%OCzip9*oW0vctxP z6V`_6_{=_nSu|x!TxU{O-<5D5N~2}fU4Z<&rzG=&v`Gff69i72mIOs7Y3R}A`HOo6MlquLQS03l z)0~U;Ibt0qoPDyTWt2Rx2bWn*2;J&tmfotD|7JW#&x^51g2JGn%Y!1Rpm`ngt?A&d zTz^qrjc{=DxDv>!?SJaM4Gx}OPV2rlm8=H*O`sEJYFomIQ(~av&i>vr!?lIS`A5Gd zhi?9AJFrp9t_Zo>ye`c&iv3^T(;bgo#}L!O7%f$FyA$xNI(`0`=oiknc!XP4hqqS+Ze2<$>>5 z;`@9VNkT|PT$j>QC_t#JHo~IpxQKwM7zs;;hI2S)6D4R2v7p+`=*Iko5RmMSCi)GduP>@8oX4&^r78ka zp~AySrtG|!MRE=TV}#3G28y6a8&%H%D4&B=qI@XAp;QTlS#p7VAJ#-gCV&P4U_%dL zh@q5eF^n|OSgOCMbX~;1(3xiG{@AoeWdP`&grDClW)1250C?pWqX!<&Jj%o{>=qat zJyh`ixaZ$RU}f4InBS{s&4d#ppquyInMh+B$_k1>2MWg$Gm*iLDsGMZA_-h8nnk?D zGT%r~caX>=Bc~4|Vmx8!ETI&R%fR%5Y_{bPX}A)@=()-561C_3k=&%Zg}y7wUDj)@ zF~;&>Ho41=uaPiYFD<$veDu>vph#iT?Y z{U zk)$DTgW(vyaRO^FiYhc_a<1s>UQns>g(H4nxnl-6e1+L}SjC9<-AjWg)lH|J^o?G9 zgTQo!5EgU#XOdjBW6pFD!7lUgJOn{V##>A1CTjx zNREsHbYz|DYU!DsYelbK4Yu*!)1#>*@a=wn`$8%=C|xN*DGic5IDisV=$#hO9$hOB z(S9IeZKDlk5_E4#MShY;-xV2aSdn#^qvVa{Beq$dL^L<5C(2j zG~{WH%B-+@?I6fTn!!#NyrEqBxO^yjoLda2(=J9@ZsLpRpPjA4np)QPjb{c8Vj1>t zo;iKLCoy`FKQ*hAItoTWzFJTcx^jk!E3wiwE)!1zz0J$LWE3;h0PtAB0j5F9|B8-q zDGji0Fk{(D&Z15y80Dj0g5bQ%$G2faMNs#+kQm3AIE*Y>QYda8#L=n5)336`?hRun zAA5fz5sRt4-fdEPFxzs`XFNi9 zaf>={4hk1Vg{E9pVZ6>pI!taV!w4GZ5mlYzWacbgbCpV)#-gkpc_O}gV+{cYi~{$f zmbybYg|HPSQ0Hh4Peb{pfz9NDab0hSjAd8mmo%RuWIbwcNT?e%oxI)F`65@}Uot4g zM7NE#k8MieBOR@K%Lu`#Lw>Xj>fw=d*jTmCIK=tmMFZrUf<-&=U~jKLj{UD#Ah;v4 zbpAFr6_t=UKhJw0zUoh6avmZ@Q;mYz0VsxZ&jMkquydmxetVRvr6SJ1K!6|vSOB1C zc7(&&fH9=eD#y&=hR$r>K%OT7Rj<|8n!Ql4Bc>N3({|@;?&B$wHs|I~o7WB>^rdfU z$%TS>5pNq~T*zt~jOYrfSR)59lM}zMc~Z~&QH=6eS}`w)ZVH`bxl4ymk|t%NR_9?FE-G4CpNi|5b3UMLOhnj-hE-gEKafBR_z^JQcGmx8Jxuc z<~y|HC2-)YtTK$9?v%qN)lUaj{(kC0WC13oN#2Vg3A8*Q;W85|2ff%Ah1)Z&B)yBd z{b<3@Is$R4_6hoi`_ZC3JX=nDsfQ;P1x*$B@f$JoMZ?>=>tLFHM0JY+H`@jWPWIj( zaYVNdIxikL0%p$pG?p2kd#gFmj9BbYV)qS2Bxu3*>;1)5R24)h zND5#yqzdI3K>+z6X+Xe=xQ-!=qZ*Avfa;R3ur016mI*pR+=Zg~&(ebU5nJSvq|zWr zjZp@Pnw7+__0+#XdLM)ebeBzYt0wXDqqbDwH6_fAe^}@KHXO$O+_17T$_&)jKgF7c&yk3r;cMlEiWR(^JzyPX+zLg;6j8>6VEE! zxQg>gbHtXUoLmPs1uW$Niro;Q!>+wCyM`fl%VSQdzT>Vm=#l3otZ)i@!6Kob(1MnX zcXbP+>fFH6N)#DknQ9b%p!apWqqO+0zw3NlK5V}-w@j_{v1*UM;qCpz$YplwJH>9Bx$JFdp15Pw#jCX{ zf7BKxnyK_|Vi&hWa_}REUh?r^PPJ8SyVM0Q;$HnUMcv;hsg02H!T+h(m{XxuanzSY z5Ot8~_-#bxQ{Av*x|XoUfB&S5n|NJBMu^E7Fw-&-fzP?MCiDUV+=fD2k2Ng0c}^w9 zNskOVg#JeF!Cs(I{>r-mqJBtWxaUyEva9| z!iM#hY5h$!+>Wg8*%SC@hXv8~uDWMW;Y7Q_=z3S(v!|em3xQOsJ`NBJ?`S*ua1hI+ zIrMRx^gA0CSdR(@(3=6Q$1~#n;eBsBlC!djzprZDo#=w&47zNkIn2RYBnFKZN&gP; z@MRz+^co*ULGaVKWAsB8qT|@X@NbE3k~0 zKrdL8UsxE?<%&f0wg}n7TNV=`VA|Z!q8Jg3HznFY+=n)8DP8-@dK&m6I8~4gH{p?B zAsBFN3aI4{Gh?xu_@q}`<&MFQ=@T%$I5{3cLiq?lKEkkk$^S>w=5)>yVZdP)-*u^L%kYmlterGhvb4&_;><`JXNdi{tKcxTt z1n>Ul(2}eWAxbsNLd&j`D;7*hP`?+9j+|-F?GcQhLTXl*JPvqWZ1cDM~j?;*j7s1L4wTB%98`hDBYYPB@bPLcO)P^@f0epl$zVD%bU_rIF1s&b zW(ghs_N8_}Sf;FIDQC{b<~Vsuo-#?68Pz}e`8jaaR2)m3jb2aXryyO-~)as z_WOsHVOfl}?f5U@y3-FrNMta0Z%n|S_{BvZ$i$OBql|g^ST!?LD@hUPuku0Z3cJd| zyzVDx3W_Y_l(J}#m5$T8I>5sF8SGDANJ_rAamn_j0{HbnYnFbj#LVgXFwE# zP8R&=>ZtUXRE}sGQtcer9GBg>qo0pOs(Nb5|C9jcOv3yQnVp8SEr|+G?&8rc20lRo z6)`twD}XQv3tYy6Ad$W@q%7?o+PJ3mC4IIrQ(@9WzBTM|MAY?eoN*yiy#AliS7id8k9~_79iy1%! zb(T-ZzG&uxH}Eg4hyBZgWd<+NCQyJ8B;Vj*hOub^vktc=zfQJ1PiMb2B4o?HYwO); z1?W2dPKvT?KnZXyv(P08ejInp>4r($2oi7)7SMxyEmnTs`0rQim{S>Pcg*O&PK=K~ zThY<5JzjwS+fZ9OwIP^?C;vrIM|5_R4VL6s966Yk9SB96kFS|i+6XGFt7 z=kv1*twLu*179z;0VlT<==is%$hyoP&^#!ag8yaPp?uf^3l;x*%imTBK8*X1n!7 zepppbvuLpDEPYv3%>ZFswcUfu3EGIRiss~iK9?UKU92o7JDt=-8^?^^00+<^T1rps zVLr!uTzrXo!jr^#j=ZifNiwb%X(zvrBKya4L<51p#z6o;ZRwBeZxK5BeWORrP1gSz z6r`NH8%^*c)kX0a?j~p_`qXrR1K_{x<2~)M1xow*nBN2+R;zGne-4d!1*b9dCboo- zwD(b?q+&g~Axj^~)a9}2q&L0MsA_x;KWy}U(@Pfs0OUC z_25J2b1r!1V*v~I?^5UFP4Il5DAD}BaO!%j5%A{^zlG?GvW^BCR{!MnI3MFK#$_o) zXm!W?N>%@NUp+feGTNkPb&1jyFDlCw5fiRzH zn7ZB|Z~t67cV3y%g4gyCKfoX16dpoOMFQwgMN!u6Ptd%Vd?el$Nq+vAOi^BrW9;a2 zZ79q@GL^~q(xOQ2dA^#>(nj{*J{!@i#lWZIofo3!^46Iduf-1egl&1Ltr|IuJ`pBT zwIZfKpL^)&Ve#mve!UPS%#%#H?<0iQp5k#;1e_ev`MI9K>o;}!Hk~1;**T_GG#)1W z@9Ua&c=J+5rwxnF!$3YVM&cjOUoJ8k)ZETE3KNgG?WTThxWyVbR#m#V!~l;vYWZzG zQ4jyj%Fy9j?T`4nzNUjK#CC|d`#9$i&Yt#gnv8?)Pj*akUOx?Fw;E1}pjIUo9YdJE zsf*7ij-A^y{E(t0z;97+-D6T8eW@Ntz~}fMCik_Hc0R+c+-T=t-SEqy_%WtZcc`Rc z@YAO_xcv9yAb+u6$ZqF|#f-Aigdl$PII2iPMO}Yv!x&0YeOO(LZCPh#qt*C)7nsb) zCF8YOE<7siMZHe#PDEwvWO69T+uPzVUCX0KX4ASjHudkH_yxxHtm^CTw~vdE%i9xA z{=QU)+yx#s59)Wm>I&J3V@`L41Y4H@wX2gw-P@n(7pZ4m$kI`e&lgOXPn6Dt52PtW zQMq=L`lo{K;-Y*s$^+35Q#nVqiEh?988qkd6;jvCp$2CFBq8*ER~T8xidgroqZmjH zx==^p;YW!5)bnkwI#kk2?s+qb_kVY%R+S)?bHW-{7-h0Z0Tp(cX)^0@Fdew&9olBK z@jYE^<#g;zFA<>Xq{SwXj<+L6Ff*aQU*^T%8EEp9S?lEdBAPSpJQh3lGj8g#XazC! z!NLjqOpig2lGS5S_WHtcXlM>6rc) zqMVeJ=2KYqXw{N?4ZC?52@Rr6wAdY<4{@7ie!|P!Ka<@3v|2C)j;{LkPg?yXQ+Vxs zEKYF|W3QP8tB7kJ#;QyO`N*Y%au_ETyf>$?0oWC3h>_cJI;!mx?ESlTd8LRfohx?8 z2M(Wdlb#U*q711j({$3kd?2KNJs_XFk!L&=PQ}IZkfHn(bgg?r3>Da=fMn_ecw#q| zHr7N?0%s;ymHR%$3h$-3cqkAUyKXeNEMJ8vG2~O2C_}&>#K80*K9-PBa?KcyP`B&A zb%M^Qs4(aP2?xSkh&)xW5@>c?7$ZEwK}@)A8>_~u1@NDD)@!?BN~`PbssoAI6iqmC z7}T^`!2`}RHx+qxl6Y+!*` zV_({EyCELfU_^^a)&Jm8*G^%}M^Pbi9i`Aep(##Xg?)=mV{47dnv_?qzSHpHHjj@cb6sQTyT@5E}*5txFs}y27S`KVB zjmY^zZT1gKe zZi(-OYTMr2phR5k*_(iMDUt61#CH3Z3n|Gh55T|%9HIF!s6A&j?4bRUid*D5VXZJ+I6kUkJ4U;?hNa#q8jVJF) z90&D>8>qs*bcey?#GI8KGUCF5HPjgSl4VhFTb5Q^9RV@#GolU`n+lXQ2Q8_?Kv}&L zq|2*NeG~y2Ijh+f)kdwg)pcsXP);J@qcV{Fa|+gBUtZFE-;7L2rbI_ALgW?%V+$(C zp|Ve!QgHTUgO0cP^deWH)?S&#QpgH#(MD_V1dx$gklAq`Z*>!6es(y-eCvH8_?_dS5aI~0b>;oo9{ z4xi+SN7@(Z=5FNDYs9s=^Bl^M0(v28*qxMw_3ihr7 z;{g^I6D7iQIIp#W6WEIVn1l&}OXHI}AGsU1ZfbaYl9|uBZ6Li;ZV+Cmom|~C;U#OJ zE^~J87JIc3_)aKigA0Um1Bbm@@qgWu%8aqeC6z*PrLWuT?#_qh*C@UtsPCsSA5xP-TSm>IsNrnPIaV{_k_zlqg& zw8C&Z6Nu?_IYWUPJWj9f8mq`NvvQ~$F&}jRVgIx7OHah%CDj^E#(Ktb(uNBDQDl!b z7Z!9q9a|iL1p0TfzfldfA@_1Pox9x~3Qc5ZC8*}#s7%@_PTjxch4AMQsSb|aF$p(j z#fIc-3ja1ysW2RaP=GGXVbg(yk_4vd${7qKlZ>KQSrg}W&Z4>$=c3e1hUF^E6(~^A zU1Q=N%)Y?-!R5c*P`wTgn2Ltx^+d4k?*`uzc?0xJ#Ml8k zGDG&OpjUvu4J9pbQMKS6@Ccs3Sy&J!9kbDS9Y92B6!uEWShYBC&_}MTON+Ft&yxz_2FqOd81GKT<(~d3_whVY=(v{MAjiqww#eyhgn_4@-G1aVC@mhHMf4Fo<=1pvpgs z5*ZbU&?v4|8j%pi^a^3pA96Jmp7f5x5_xzURjk5kf0icAXUF{`!TW>r4yN_R&c)?L zhiWkvU9P8BYSW=bzu8t+11d(@(Npu+3ySrs?f(@4)t}CnOdfsfxX|GHZF6?~)0w!> zy7l}b)?}4O9R>fW=OKhWqEgEh>!UkirglVTr(Z}PMf_=9_cIaLy&k0NQQ9q{g2<~U zS$4^J@%d*}FBXLazsRMh`*WFP-A_lE60mV<8$(KQ_o6>t5YXVJX#j-yy}?s%&#(M6 zrp?&oSq=!?e5tQ^Y;&QW0SVv=m}_;gazs41uxp@c;!UE*!KlS^wQ=~mdT%X_7*xXMGLb@DQQMLYJ}M!wt=JB!zM_ru65IT`Wxl@!V ztXYQq7da#Ki40F!`**&%T$wMtP&>V%ijusrrgy{#+%m*;>A?4Vss}|J_ldnjk~EUO zb|Uls$8rkstzX`=#&A#i^d|c|qk8-iVr5K{{Cu+mV8vfDM2>!Z#vrV-*j_Hy);NB& zARNW3i+$kwgY@h*ZT-tGu(6+%Sa^O*$<|%KoSUWESNnTn$@d$N$5tDR^Kk;WxJ8KT zv#Hq`S&sYP|3)3?8X7cIbX;VCvF#U}QR4Frw&-i#DBkIdM31uSw$w$qy-^Kkw8@p_ zwQlf&h@}>v&IK)MJ#tbK>Qn2lcGPB~az#pviS&=W>ifoccFuEa58i9K zXQf57&1M{XfA(KFH+iHe&-*(~Xye*0)b%;4K?a7se_1zlfzlI&DiVs%*(KBwOK2T)75~r!p<96Z+ zmje)-H#M=6ZP-YmP*H8*`bwjQmV+(u?cH`4F*Z1l-LcOwki&=0obL5#goAeBl)Gc} z@3#TR5Y#DxgvYwb$46peDUGJaF3Kk2xO#GlU#pokx!#~y;@9RrWJD^e41iDsVYpRg z`rlQjK{`2MzY<>~uVsS<&|%;;yH7#?F)7h)4tzWXztHBIcNH&i>m)ALgP&Jxn=iER zgRY>Itutv=M9!@i3JRfy20){pyZIZ#;DplAxq}(LxYH>~IDXy%(5O_R8Zis=BmgIy zTH;DPgc%r@lx9Men1Zl&hw~M-Li@)wb%+ukXbX_JK+RX(diRSfGc?^b__?T-S?l!x zQ$Vc0CqqB4`&y{x%d;e%u7 zvK;heASW$dO$}s#96_qxAL@lF{C{ji*EKBK2&o5l>N}fbB8w=KxtezB@z=Vpd;E#v z9;0_68-{CTyR4zY;`SflbXd8VB?F@d04*X2Na0zKbX%T(^Yko8t*92Hk*u+I>MHJaKq4Btym<3lv??ey2UuIh}D3k=L*R%ECMtD8Wm0`o{MG# z;Xx?w$gHv9jtm=`V!e}>mE61}f%cLy*5=Wv1Y1}{nR2OyrLSQid@VU%^SDl!^R(c2 zqjMnj>79`TG@LYVdV|N%Dyn~#f2~n!ainv{ga=e(ZcTb((fN=j`@Ow)4`#4d*&>L6 zBPJPEM}A~Tp@V7w08*g}G$TDM8{{GgAgd&9?mdJ7Aufmli!cT;1W*`I>bvlozOQ zWu;o;?PbOtSn=9qhS}PXyi`lPZF2w-cD~y#cgRV#`0Dnz*S(DibDZ4|dalD+3{_6Z zVj0&*?()T3U>coL)%m6B=9;ei3hxv1d4bISs@=Uu(L0g`9~*(HnU?;mi0x<%SJk!V zn8~!2MW5uYdeuqA7NId(p&Q~u;P@VOXMYLatl>nALE`CCE$qdQDI?#WBpH|`tUpx| z!H*a7VudC^HvE3x6nnv%4dlr~p+F4f;sC1o9JY$CWqHbp?;Nc+9q?0tc#=~0hEZ6? zVu+P77VRkQ$xS-1nhxQ?t?j~_-TIO$EL6~@!-9Cb;#{)*e<06b=Hpu$tWb9zQi7Ne zIDO5$C&kl=adXq2x9omoGOz#~s5VAQmiiUC?mCG*m`*{kA)^gvxuAJFYZq}eo+N`Y zL1O+K2u6f;l+-%d8tX8KI@IN$zM%j|FME?;oM`?NuTfn;zH+GP>_Euv01WClQ%5+t zx&hX%CW(CO#X2M-f7dEO^5Rk@gqC7jZ1yED&vb?4lgLJj8|c8=8zx|A!R9fTj(cgJnHdbm!(}<%nBHosxlb*%5SJ^k6LW z@&r9^(q&Z1C{HYJH_oh!s?e>Ho3jlLfDrsHHXq4{v+-~!98aB|p=mLTJ3{GOs+qr* z7z${tdpNM}sVr^?sX4qvfqJ0B;TNRU+P(!44Bz4|E?I0?hF{Lx06nPCXQC^h?GpI+ zq68tm_Ad^k2_H$mMHOWK8QDyBxY4mK8F&S^f^$0Wa(S}Y)vwnyW~Pd6B$)nw<0VZ{ zz>ljqFL&?=-{Nu1?wiEO?O(Uef}iZ-@e2d7mewdfXV>_)KD9x2l$cswrr%ug<1x1% zpQ+zbVqW|4|Fxq4Kg3skVW*g1Zx-4(UN{iF8)_$CU=*Z1L27>QrjL)+ZCuO;x;V8aL2)@u) zQ587v#1&#~*61}yIqI2F8&tk1iHm3*z zItPF!A}~sZz&RFJ$7~8;K^QyQ5Ek!T#Qi~;elN3boZZ<{Ce%j(Tv>-(GxZQZY19(> z2lxj>0^V1+4@E=tYUql^9X4&bt9+fI)|uwF?p?&yGW6l7mk>?z%`S8mQ1@Y86-L8x zjzwoLnI@(AKL0Zy`3nU_NL7cK00a>PQD`)W)!U)yA)1U;HER4~kwwx;yeFEnpizn_ zbLbKejK_sD*qMlgIv^J}#azl_2v;OLa`vrl37;)Z zVcW;#r*WmgYSo+fDIGNo9+~Bq^0=zahZ_5@Q7X?&lSo%6q8g;+4Kgf4p~(B9_L0Fl zL8}FhdVRH^9G3+l#V4x8s_sys9+BtXs?@g|&X|G}A}A^8c_(e^E5X#&^C$ER;O}J> zvM6!PYr2!tdj5o1Jg%n!o^MJ9 zr|4MEC9^p#BPl=!5yYs6)NV*ilMc94aNIGi&!p)irI2dYI7Yc(PYzAUtTSoC8#VIk z8=o8E6TO?&kgHZIdRNj-JkSisv14kK$K#zLwZHFL9qS=HCQ~P58i6holtxWl2IRsT zfn|^q%>m9$r0~Zo2RG)+hVz?aklc~M=S!;JCO{J2CHSx~WLuo@xsLKI=FeG6BNBXQf0Kio zny}Cl8qRUBQQ56{&5TRJeDQCt3D}zQmC=P~2HL$7%U;K1(!nC`dH+0>Np%$w{3zk3 zuClR&^2+$mvW=a2P3Yh@mTKKmQDIR2n4Sbl4@65{0~lsJ?uqeyki<0$#6}>(1-ec} z**elOi5B{hL>OTP`yO+7%%Vo$SK|W}*- z`9mS%dQBlYDBW5!VZtO5?BB2vz~KW45#nXb897>J)LJ;jsr_Jg&0z@Y?q(dK`U^iU zH!j5f3=}Y8aG)`HU)@p8YL~vhAN*hAI+bF}e9gjBK0xoV^*{su?v51BsB7!$&_7~$ zy)0n@u0VL*7^Hwh!)Qtql+%=@UwSKS7u9I(Pj`Ry52PxGxZQ9CPVKu12}Lulv}X1h4D* zEd}7JXiZ0mAt`G%ZrBJ{v>lkKceB(ZF} zIfPD9a;pB^(kF-7Cr)3KePO;Cn23E9$IP4vx+sPhEu`RFKVOb}${R2M;??qTMFt#byy1Nnn4fSMv9n;7NmZ5+CV`#Ni> zBtRvyL?p{9w8DHTVA_b?B~D-| zR74T0mP0=r8mJZf_Eos1I*Ubo*&I|jR-1rs?iz3>~f=*59*1qA)35L8{KXO!0lZ82^nd->H%3cf;k`qb2e@t{g!BOqI=L z!dhU)ve(w%=sJAS1_gLBxNXBI=rzd?Uu*489^ExJuW2k)w2Gp#c(xl}~T zn2=bteS`7<2LPUI_xLEPq1Tqy6&t|&g=TajL^CYTRV`Lpj^=B|Gg2y<v?S%ZS>^?JX)jH}ku;+HC!k zB$HaA960)?Xjg7~*&RqQieb+Avs%s^=5o%)M|Do2jGFNY91W&{Jg)WHCUQ4-;&uBH z`w)Uh4xDe zE!&rcf{ETd=1CBKyYCD=(|nKj>3p92ywY`LUhfs})5R~*@2In}f(bXJA0a~rPM%rH zOs_=T1Nd&WdMFJn4Sej`R??=0w{Nf_4(5}N%HSjlH+il8fX1^S7ab3)kFp~_lx=#U z{TjvqiXPjR1~X{Pnm z9gbiY#?f$^N@gL`*NKy7jo85}P9?UvoIAtOl# zpyYa^LL}v~My~jJ@b2X4X?k*s!N*WddkjJ}c0Pj#6O`gTGt(oj1YgU0C;1jj92XYw z%MSibx-5RbGHFsR18-iDgKh=BppO@!ALT*3=n;-vge^M{5=iD3aQL)UeuLtkkm*+G z)&mq1#T@~5OSOR%=5k>kQ00#f*fdS&r}$$E*s`Y&TLZ9Jx&WylT24gSc?r}yh#@vm z*#QIsF@YNDt1JA~dPB>>7yCc8B-p;jXFCZubC1O-0y#GM=h4ZkD$z*inou zt@BVr6fHC8(Y)dQUT*$6Wf`VUTVj)77_mHLXH4WuJj`Y@MxYWMGNV~x1>a_oIM@B; z7*}((G^CD&jrK`|(qUByOxFk_GbYcqOE;D?an?VBW&)&URZ4x37c5$*a4gRxdyK)- z9m_%@0VcYT)zQ@NTYRCvXXg4$e|OlSQ)XZ)hA@YoM@^Majzf$Y@H#++l7uI>hZMU)4hSm&nCBai z0&WzO*)jtpE*1XhWC|=m-&d7`VqOPe?5Fm6z|w{XbLT{`{&QG)iwCcVDVHIMSrLGz z5v)jM*P{o)!$lM7W&C5t^HX-cDk*Kf8}0o-#J?WelmGNcm&OIsN32!3d#>| zGhu)Er+u~|miuE@wTFiE8-)t_@VM>3|I?xA5&Ni~{n3s#EO=Wux)wDuz@O&gq)`=W zstV$150Ha{FUYZo;^e1qOaW=~>MLQg!KzZpTgwAKDH$(D9%%)U3XbHtB9WilCn?xA zGDTxq(`YY`f(qOqxd8{YxoHgcqmI+8U00j4*gp%qcM;8xW^%J}hEKv{I07KbwXrCAK>-hk`W z`s`8HY7ex?L~((y+_a9R?b1_|vAf7>P@l27B4gnp_cV1@KpT5^n_3QM?R~f+uPL?+ zCsuk@mxA8J;y5EHh7Y)l_pdWfmw=roySG^gU@;;%e1lRoQOi)2?Y3(xDXKXn#P(%Q zC-H~X5Lri(UMzcX2`izZ2t|}CGeOaAFM~jdj?BSwm(<82428&kI8o8T-g=LST+u){ zAIj_(#{u6D2+&qtD%RxE<)tK>`f@R;yuuU9$>mW@@&c7OoA;J7KXqPP!aH8EK{H28EMx#B~Z#M&kwvO^xu(RoDO z&gvOBB&G9=!e~aQi2Av``s{y9D6IMBK1h|+d@Lj_zt2YWgT;kHjb?q;`wgIy{wElD zST4%vVnR~DPt}jsHBsBQv@-@-Kf*u4DWD5Ul*&YYVy(gfP3{n#8l&fNS4a5l!Kf(C zX_CYpR$>hLUd}WCHJ}Q zXa}!rEJbL+Xx5QuFX=l1oP{pKIy0&^x|Q4H>2Yt;0<8`3ra?ro)V(1a2QQ*SM598V z_Zbhdl!vrYX2dg(XNi@(umz-1W5nS+C}>5w!x;F-FKb;K(SLRwetuVbK6Qs{^ZNH( z?kpFAmgpZTKI3p1A>NeSDIPIR?(}k^ba{q4bI{4gub?irNzCi`DAxb{Xi})=Yq)~E zo7}<%TJbXeN!OY%xDe5D8^6y^jNw0G9Did-V)DG;xk2; zbEy`$BVE&Qy)BPrV|u$M(mS&dj>e)>18s`KJo;un{+R*AjoX>wsP1-w$9I|rUU9#E zezRHnXLab?9|XNpVWw^gg*ZNy=3Qx)!diE|ttH9rcxQg97au=yOFrBNo#RS{w=Lq( zkL^bn^1H)M5!axbuEUdWcg|>IBq&7iyD|2EW=ot3^frA6==3m(7(~$Jf$l5(MV^1q zC6rM7G!JjS>vFH!8>t(>pSrqn4xejlmfW8q-}4#1s?92@!yYi|#9P~Un-(e7MR4Zo z#TN~(;_!Mm>QeoUiRc_xtxo$S`?)(&qqm`|5uMtl!ApMisbF9QJEbu`o)&}tk0=wx zhZaR`&b%C6H-&yaF}Y~HVhNtZfM>KJn$CC4%^1vdZQfN9iGA14cRozf5|bZ%a*pL{ z2m9anfN=3xxEmmlkpu9-PxpPcix(>YVMW~Z3GbNt&SPcPR`}-BEL=G5vmBY=b6&`@ z0R$w%a6yFF1f|qsD@loPME`b7!&_0+6=>=2$qke)dd#SK1JQ3%Q?5eaLra%K8f_0p zYnF1RB;3>;d2eXp;f$MXKR_DW?QJxtqAkR-G$Bjc;W#9AY6*{xSzVY+pTSQhwvT za=V8w2*j8ny<4FAAtPe+RXPq8Po09a-wQb|fCO_Esu?UyIEK|t6s}k@-oQI&m}k;y zj}86;SF=EGt~*|9{9Cnq`_mpah> zxVOR_4D#;h3#h9yP6dIr?kYu%^s&Ou19dr>TP8CPTCrt0MQQYwUUjsc6xoi9^*|6l ze;M0)a1~_=8)X8T4v#8AQa+{uhn>IVFixTVzu*|21PT{F2XDs4=jyW(^>_%_o0-cN z#Q%Z+XJ+E$xoX{H%vN-hU80;^GpE-Y#76me;uy#7GVdq*o4|1`wy^zqUzeK-Xxlz* zdf(GOs9o|Rk0viF=|1flDjDd%qAI99Jz{gHvEtr3TJUIpfeKpEWBEUONkXaE~G3c*gIO40tHYVz@GhhGhRvpFJ zq@(W9Zm!@{VtzOm2|`U&EUC>|G^{3atB>u zV@9WEl*XB_?`==%wARJG`_kTA)FWH?;#z(zVJ1;b!bL7YI924g%Sw^~qACkAoeRO! zvV~Ec4o-4ZC`??YudPFpI)oteys2&wiHnd&E8>Hw!<>HcqaE9>H8|%zX!;Vd*fF^T zldF)&zV<`7RGLv|C(886E8cDzmUo!u?ulgv(8XQZ#Ren=*TiB)6&{Ytyl=xnSlVzo zjus>)R(F4qgHqanm6z8iuWe$H=k7R{?ovL!kZJ0j&(;~*iijE#cLb$K$5_E-`JGyk zHN*O4i_7D@-vywTg_mZ*xQL$?KuzbLnDs;zk4Y73M-2H<4lmxf8BxeDcq-ns1>|df=x0k#&-fW#KRod9SaPoN{?1sc~36?=mw%?KX zpXKz&b6Lsr(2uEu78}aQSaaqvPXfdTS;Yu8;PFG88id-B%! zABwsy?ce?cqS$5nDjiS7gB%2c9ckQAP3#|)x}FCu=0d6Vdw=Z#rsgisB+a4{g~1n$i3^o9=_NhP4B9k{GkB(kh?a?G=b(l6mLDff2FJ zDqW%{_j;)}H96%K@X4H4p7Y9brSBSfFwoE=tWf}BLjZ9b3zX3OKL>x!cz~(9<1S>7 z=aC8o0d2sjl{LoqSsf~Pu({hSfbPVa$|qd_ssR?S{B5Wc>tOo0*b6##>C_ZKIJP7@ zIUgKqIG@KZK)DhD>j&6G>g>!aVWu2rK9q-^qCYi9gV$_FWU zDK6_+jyeJEm|ov5$ICupPpcP~r2#dp<~$*zNRW=oBfg(_I~>K@jO>`Y6NUmJ0~-` zHX>qmJI)%bMQp(5#0zm*NM%caHwOY-HGj2rN2&I-RzPB&=Q!3E|5sFR0eO_vfHgQ# z8W&kxY(<%fpn_MqhEk6Sj^1K2HghWGxT0pGRY&J|6LcfMrzlvl9Mfn+^*W7Rkz!ze zJSOs^1Fn&oF8{egZmP4-+$-l{n5Re;6$Oc8x1R9|W?L&L`~*_~Z}g(z%4{fNDmCWH zcdTMvdIX-8N2IB$%@iwphCxd=_<}D>`ZM~v4xSeo0=_RF>mVw-kOs77qz=EqM5J@n zW&m~L;uUV7(uwA5{18FV4WImX+hwcJ_LnHaItv^=Cf%*8o4RAv1 zKq5UdD%WoGIpIeG%l+X!!?OW0I24xz1V&H=g23>~D!+rIBQz(lD9VZ-=&aAVZ%WH| zdl=T|B*iAi$pRFX0K$wQ93KD^(EtDdO+-M3KbGa@UY-XVdp?O!({19dsmr-|l;i!U zQ#g3MPz5iI>Oc_^1y3{}x^)ODBTQFTdUk>_a-CGvU=k53p}^0#X*p4y=AFnfj+)&e z#9(XX81cS7y!JH_XcMhNrxjdc+IO?Cc)sL9@zHCIzMDTc%n(OPq}8u{>M5ZAJx#cQCDSW zqvu~>cYMW(iL1g>sZ_GYBF^{7hAKL)60p2*IgmJU^20j^e`xTmg1MKZu|Nemi?ke2==|uY75SBVKqsejOS$D2dg2U@;6T z4Edls4|7b%^wNC6CKR8kpQ)u9y--o-G+u%62Jx_(w zXI<6=U!v22&sz9PE{nRPbL$~xBTa1; zGO(1>Ov6ts%9kl$>w&*rx0139sq&SqPb-!z+o*8?ND}T$^}HktNsI1=fDkgzq0<8q z;+4#1=KBigdFS0WNc8m|73_MWWWV1ZXc>h{_}BQSJ3Y*X3w1eU)G4$SUs?+u!d_x$ zOAFfFs1MOLGL&+#W>*|y>jf=lKP4_1RG2nB>HUFs@Wo_^D1vF_DcLT_IO^9WmIG+1 zfIyS^ZAn_G5L~LM`J@8YL40#!ims`wTmBU8kB&{cgIAUD)@tUWOOocoB<+JfI0QIK zTQC`@QtY~kNv^fZ4ma_3-Ya3=DAbD)ihX(BAU4P6a9cb2=#IV!Cx<%WKJEA8( zdMtL3h>6tzOQ?+(iCE$5g^F!!`mWlQC-=Ranv;F|m(0_;8kFtaP61Phf*L1_IOVv& z>VO;TMTMnoxXA)j%a+e(D8++%MBs!TZ!IZ4E+>Byp4=h7;5tun)whTJkuM<{-_Oz1 zelg6_CTp-)m>_JhU)X#3maAeR0M|oIe1(0X>X2m$8R0OQAz_qx1yG4`)~rHQLZFb* z!l9(dy_kyKEqs0ex(4UQfeh1Hi{yC$l1`e|lw^sFJsH*j8pe|_>^HL@I$E2)e3yuU zvD%(RKZ$lb#BE%K3z&sjMRaAYMp9aHDZw9l_O!U=35Tx7E|MpQzl7gUHVRtoguhyU zaT9rqxo3oks14KQX-lB7fyF*uPXJzqpZvLsOzvb~gtg`;4Zq8g%SOGk(`EPHT>=Af ztdegddnjiMi2sX3sSpb6N`F((Nn}h%X*VW5ciIKgJ>u6ImzAY2?{@?{rhm`oi%|Im z+Gyjz=Z)hOnyYYr=Oa*{7+8ZF@kP2W>(a-jY?}wSLBz)4^M%yufgVU@s&~Yrv((`+ z=hHF(4mvcoZnr7_d0{4N0>PN(#uBB&-nNq~%;=SRe|>a;LzcA~qeUWP)kY3&K&!@l zyzDU6N|){u=`J{cIE2p(2E|03O?K5;yVpNelW4xp)IXZ^IYoc({^qCZvkMA}J^S1r z52m%$LhC%Dc8s=5md`*T869fh7G!D>$IOSM8UX^)#`jLR2@#DiZY>X{i(BxHaHYya z1`om&G;n;mqy3H5<@n6-?9;84k7tB?7!%j|(qJQ{4ao!&S+U+!{PIkd@x6Xfr%;D1K8N%gGAMYAB z@>msJdpvfwvi9DlU8owsWl@LmTO58xGklA~uV6-RY4;0cLK7I+VIKs_9AvV3!=e{K z1(|%jBo2JX2s0a@S3`fi>?WpZ8=M3Vym`w`oSq@*Z<+ble!jWRaKn7tS!zlQv;A-f zE^!?iQFs)9bf^~-HX&5{kZ;@Di9sl#S`Sc3cgR(?pv^PL~CG9OgkOj2tNPp3&?na5t;?8uo z^;sGPgb&;PdDRUY+d8k8kn!!lKLrg;56xG-xx^(OxsiX~UV3K9SVx7c*ury8Hr(Nn zmt+mmo^bC(Eh&(G5LBU#5QG=D^(!eEr5b~A;g3UyXStS(%@h=atK~htt$rNtsa;nU zYp*{MeE*yzX}Pwxz_Gd!Ihe_)%b??_-m*7Zyxzn^h{l`J1sU9UOMJ+E@ti3dBX(vp z@(+<1vz7b(HQxVx&c%W#h*^Y*v{EbnpYEuL(kXIZiY6aw{D0Xot?2 zHz@&cU9G{FV>$wryR$CYTy~Kj5DwRv|HS_D?7Z*o9FnrYCBoqoT$hI+e15Xs3MDi9UB>wN+37R5Q2>qp_0BI&Mz9!nU8d zTENWB)q9gC?9j}p!7RVdzkdSdtSB*iY>Fi#eH6<3OxLc$+x0>Mpn(~ zhNa0z`1c{vcGT4BY|U+4afz@gjb1Nv`|qv$gHVz4X}FS)5kb3XK_zk7 zN6D=)Khb-j@2@QP3cHRD-SPJ_d0LmpH!xoXgPl)I`0*tIMGI#A#0GyIm54ajY2!A; zWYA?zML|@x+w>9xS^ra`>$&8d;YH1`myzPwnH5s>21g==%V33bt<*Km7txm5GGKJ3WtY_X zO<$*z{$j2LWHB)_PbGP;aNUKw`;3dMqfLh zE3g{d9KnbYM*=1{4aE9%q!f1FR%n5@elC+2-+kwMR<&#D&_6sfy*YSfcFeVE76BI(hEbTA#Hqd**`mdc433HYlA%l^HYU3GF3gGycAVk zE+%(F8X{bCL6OSa1WSj$R33N2J-eH;)>HEk%*8JTn%V(^sliZl(|I4R~>ko5Er41{Io33^rpNW~8gl z>~z3C8k~zsu^;9Nm?z7pVBM5*w(9h@BF}}VrfE9-hcAcI^F=OYHr|F>#@8IUEH*VP zO{6KA=aIF|o?DxFenFD?OJxqdao?IDj*AF=

ycBey{rl;kDc{Ai+(=LzE1TWNYW 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

Ub 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

Oc`#< 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

xBkwJt@8E9&+>-mZSm0{=UvpbD1bp|p`r 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 LF<>E&RPFo=(js6m diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff deleted file mode 100644 index 5beafc7c57e08bb4fd345a80fe126a5163025633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71476 zcmZsCb8sim7wspuy|HcEwr$%sHnyFOZES4Yw(V@3ynKJ}{rS4;OjqAI=ib{hT~jl+ zYux3W!}-zv4~XVAo@M|5jsyU3G7A9m z$`pXSgtIU;H2H~Z^TT2L4{zk8%oab!AKQ;V0KpGPU;&^k7Pc-PKe=-J#4rK?5Q*xx zW4~?ejemHk6hH3qen66o{;pzc=<$;?(BS{m5`Znj5!e~pngW18F@NkjKR#BuP%Nk% z?44ZzKwvIEzL)_3;<+QC(lZAq(;qF0z>oH49)J?&DU_i9U1wl`;1hr2fj$Dy69DGF z=>PzOw@7sV^>e|aejinzOAtqp&H!Kl)DQmGJb;W1ObiTWUPlIcdo%9Mr{V@KmH3C9t?oRV_^8iFi;2wXF?PB2pTUi0v7`h{q|qO{9n5F^bo{MetqfllY+Yqk>Z#$i1thx1C|D3ReVVO^{8 z^oW1Xa2&IAcqAys)`dYkPmgQsdNXb;f*n_IAIjAn3Rf+yn0}pk^h}sUeTa@)A~@)5 z5pS%Hb6!Ee`bAFev8tW>N0WPI)3%D$6ZBBUO^~t-d$&i}R>hpO2in!l`RUf|qPMrj zaBDg*j>kSbWX0R9u~=!0 zIVs=kuyLQ1KXM%{f{&bWP|?M{LSY&p=a^X93sY$fQT6HF+!^bz{G<9VnXB-3PN3@_ zC1+ZwLx8MLVzABxY_|X_erb-=$QS3j@HvLDo$}*SeZY(}Yx6PB1g9&f=97NLzU%hv zGiCcK=F5g&ej9v)TOZ!z>+)^2%cCSqr1?nEq8_j?s4<8%43=%O_Z@v1(b7Z&&U zLcZ7?F)&k;)*wB#I~HxuiRach+@|{2qro~h#5&o>PL4`yGZ{TzPDDLzr= z93#frET!2;Wl24E-rI&!J8<{5oOwgnISm_2H*YO7r4;q-jiXe)Hf;WT!d9h{2Rw zl$>4?*l>M2!uwcW3zGQ43nsRnd|0WT;2CUZhI1^MqI53Ix@-xClv6Hqx|v6>qrUU+ zk&Bb_Ma8eJrgonK6Lkr{}_;V7*QsV)s!@890CvO z(>{R`8DHm%Nv$=~8o-1uL5i+qDT`)u=Eu3_-;|8_iHe>}la~m6^Oi{2ilTparQJgQ zt_xM*QTEflC)ij^=FPoKhB=fuNFv=?@HA5vrF)y-nLdyieAbVJVq1{oP2%Z=_A1+j zWEy@HUrs9hE$5UUA(om*+17)8zo;mT_BnU`C(d#G;L8iEDF1N7n_}4%EV!v5>RLh8 zcQT?GCqrxF?mGuW{hz7WqiIuZ8td>j)mqm0n6kPPz4s~2>sNVk3}0xv0e{5db|rZoI5UuHV#;cHA4K0kV56YVz@=JvyTK@NpO-XXMNm;=S z-FbfIehz}D?^~sD{@lu14bU8hZ3G6Lw2)@NZp0PS054?1k&2Q8!p3m`rjS%^29v6& zfHrg?v09?Cw3N+9jxQo*QFOjCfE^CN;PBXde}cr4s%TNtKYJ~b=lbU1Isy7)OA-Xw zoeB&>_>N&m5x{cmkHP^2LAnIsb334t-@fzzVf#g%?=Y>cKn_oUx&nC=13yTJHjQb)1gsuSYPS(;FAb*~8CnP5v zCy)qyW|e@OyYO`HIwH>_vCve}XlB628^=piTJw3r5sjH_+gGA{(?UQwe@8inAQ0+JT=abMmeS12YEDAzO2xJyrBN6 zf5|0)kdK~7kNRe!v*4}=*~RwY&|+o--fxUKJJw=NwbV#NMWqLV}dS5M*XCmmM-+SAVzkSJ?-*E0BWNMXzwwhS-O{E&y-N z4CTTtIxAc0vzR615)LD(8rZc8g%{Kkny+_A}T%$!r~#=4MhnY2CGm~+}AI&H?WGaspXPOEU( zA*g1FwO!8Hwaadn$IPi<*|O_;nKyfIOsh{O+Z*L(5eF$k1U+{eP>L9=;UYpln9sbQ5I}btX?GfnCm|yL z5)TUD2YSnrE6AiY3)3|RQGOcbcNzzT8O{PL#072)8ES;d#DGK0fLzG%YsLt6*a#L~N6lfJ`ed2@#Q?aYlkmD#s<=SIJz>JfNXc}1as{gl{>b-!-FVYw zEny*P#D6PHsm;>0Rgkdt4y4OyNg5q%(txj!kNnL^HY`$F2rn~1gp_!@s3wouAEgrp zE6c>?RQAVIeO}*|igxFw8WSaV=gu7uG__%pB!1Bj0^K&Wr07?n7Unn$`H-$wU`0=2 z)9|~sLB9q8k_fVNrUe8NOX4#bW;L@`+$068a1tyxELwz{17;{ZT60K_5~-MgX)UgT zW*bX^P6&P?5~s>MwBwka7M}y@&EfQ`bn)nog5y%qc2b0l0kYOaV&CD7TB~<=^^srosGDVQc7FxhhoiFRfr?zYFAK5u3VNXZd7QFDYjh6NvouI`wX0^So3J9)W(v}U|$ox z`}Mr0DJ%oCB3tly87?bt^FTDt61RD56Nc_$Ri`78qnW#4nQ-*&s%7*VDmOthbKw$F zT)v=~YJct$eWcBWYyMi3s>+1N)sz`14!?%waXAY{YccK@gSjIXjbWfoB$kS9Wa5d$ z*q{=gaD;4RqA3_nlZHe*;f_c=fi^g*WUVc(11f35^c&%MA6`^W{KMMNt)bovUvvoOc@>6=j-c zB2o8E)<$1r5RKG>reT{0pUW#8&VY3VQ;XH^zx*tE(TN*{uT_lBvdXg4p)I!nQ5#_K z5AZG@GBxCyG*u8@mD^bXJ;aPOiQGexdW_#Wkcs1%W%tx-h6&6SCW5;g)B0^E-Lv=D zIv`N8o^ARds^XS*4Zqr`FudWGOw2c{xh@((-_X#!1yz?*OqC4FLh4UQ@yiWdpNI5! zIu*%f9eS7}M_$zUf$e8A!~JvhZ{n}ndgnR z6_%UjSu8HOY{~7Whk?y?j|*y7sDLeD%VAb<;|Ts%tgX=6*+j>K>)fkljGX$mdgJi% z(?5rnABFjB>GzA~!!VBb4}SQo(gUb)<2fQz(i^2Wm%ruBuAE2P972$r+gU1SI6gV0 zy5*H!A<)uc#dl4K5g7{R@-6ekDF=H=xZcYFdco{9xQ)2R$|!-0w`|Ec0J&rc>!WkV zykrdP<8!_S_>AL#=P`x}_TqpOI`ag)Pa56^aKiGL!v}{=S^|9faKQDM!UP9#z!{x& z0zi%#P8q@k(>P(_S+_7#U&Aq)fF4yt?Ld5|Quc<((044_IZgbdM5n*tDa`#5l9l~u zRq&Pk2Z>6*Kz|0{jT4mpfyT>wfR7f*-_|_J8D3_RtQS@Z(bCrCA$lmM zcQ8L?e~L{%@94(2+YUq&M#RL%`iFGjV({Sbp@%^+ur_kqSer3NnwU)jHV}0BV$|@g z@JMdx@MQt`!7y1sSv1rd0TY&g9YA(~ra`LUu{AjYfqew7;Njs3*qDPS#L$Lgd&A-2 zagneyly%rD0zEo7eqmy2;&3Gj6A4HW1p-XU;KIPeFu7skmf?rLtFbk_*qMNu8%tUN zfT1HwxBr_P0lJK7`gnU_VBk<>;GRCP`20>~)yP+m3DnE?TOLGsSXHPiNYv7|x`S%1 zMqDFkzzgO%UVi#N=uwFB2hd7pBL-kVB^?9H&pciGJb99#4gj~C=Aleg5l+!U)A>VO zLg<`M3#4sMB`H|_brN>S&X~@b@1DmV$9&O#ejVj4>v2|%XLZGiQ!UuF^KBFFubtnW z{#`paso&7s{VNZ19kn}!^b7h-0+=MfSTGUciooOGmSGXF3*bs)_G9YA9)>N4S;@S} zPGr1f%gBBum6GMj(xX^K>I` zr5;!tU7O=v^BnUWARRjjnuxZ}wl2RwTR~$F(Js-h-eCRfZD${ECP9KMQqw-Lb#_hN zF>@8MTKLFtclx055Pi#Y#Ce8xqIxlOueFBVqP`#Vw05C<>O93h@f?Z2iEu@JC;lS- zlJ_|BDi3-J5dsbiEhgJ|s5K9nOQ2v?l zd9R1>{^`WvuYsL}!RSzYT7FwT@y?5XA+$5BHJmnd9F#pw4^};W0rfmn1ONTn*F8m6 z*}t-83$MpYOb;2uXach*z%k&ccR)kLM#sjzipuS2kWUH+R7B%?mwGA$!BVkUbAdZNGt+eH0H<1pNr*9pSO9N`_e+qvk%Vv_N&o{QUvqQ2n8HNsj_P11ZUSb0?G zV1A9MvSF2}Y_qVl=|gwOVCZvPH)Sa$trEDxt0GsmI1;+u^rPM2)a}q35afspup0n|(AE8LBh%z`&#-I^%`wgRA~| z&V}OB(^F5bv|XM3silhr&xS{>WDjL^c~yB~#CFl}bw9XqoYCB>^wdVyLKZzcJ*$1w zYIDe6lfNeZ`VXJgYNgA{)tAe!%ks8vwsW>4>t5^If6f2KHSbvHbLV+=@9~^`B*90) z$HfxkR^vWk^wYbzpS!lZa;JM5^>0RhO zZ$IkQJ^$q!>Avq?_7nLo|9bP&{q7H#5%>U@32>w{ud-3#WaDPz>vWzvBYEGY5eDan zr4Ru^xguSml6x2D!$&CcOsP zhV22l3Eqj5AHmrB`S9a`qVLro-Wlc}=pn^KvVoEiLL^L$m!u$BM63(p1I_>-H-+Ru zHin4yYZm}HV$Y>u3Ph8YAU#AZ4VoG8CkUY;(xdp3@ezKpX0p7+n#eQ|d&Ub%L=+Jh zNhuMR=ZO$YN+=g~6oJjJr*CU~tyb30m@wbNH(+pPXNyb|t0!oWf|zh=N_7i$i~C9X z$ukzODqB%RB|obuQw5#N#uSw(!jxNBk-H{CE0LEoFM^#z%f_|j9LvEJh|V!tfHi4p zal?rlFQ_zCHgT>qUBPT+UrRifh|epZlAoHN+CCz2h1LAgFXaEz^QU%>9djAh96SA$*KaM`$cmL+Ni_se+afaojj~QHJ3}?Ry6WXNp zB;%NDFj*0Vkj z2wwKzFpNp=QSM3Vqoq48PY=Af(tX{dsr%gf?c2Y%h99~<`hKANzw<_VX7u-KK|%v) zcftPzNDv@#K&t>;1-Ka?XMyklKvo9Ot?--R#{s3iH@(oeK&zm#0+^@3;6aQATpq|( zKpTB}4wyI~83d44eR;i{w`ebt280gCyP&9n8h?;K@^)i)+jbH9GItvl^ew1eP>FtT z_Y)YlGN`4~Od{i9pu<##$o4<<^X*mK7Ft9>k_m?D?v*#r`T`a!m_k8hP7q9Kz?+u(5@sA*ZHJRX!T?akP8OSKn9ZSBT&B`_#WniDVM0 zC=|1mAE_vy^No}k3pEs~D4$YVq}Y%6Oskm0H7jfs{!y)sOCCkvM>wgrl{YB1l>w_l zsFYU#FIlPvSICR13RXZZ%~Xt3^r&Q6VK+x{DRk-QRM07`RM3}2Sml!GD(71+x>$v> zvcQ!oTKX+cTHslxvRG%rRktn6T~s_RiCHo?i8tRvJwyaW>_t#>q3Gt;=(#Jh*Cbuk zoFBVPw9_Z~B^4)Sk6|R$l7F|^yya1TgCnzl{OtC504;sG8)%ERZ~$g9+C-gh8fe`$ z6TOpY+oZ3%{`b1X zi;)E$J$K#Y+3zsVbva*W_#HnAVopO#9=<2yMU3L3wdjQ3=o~{21|Y&B1Ik3I#nd@~ z{uRV^2?RGGDs1c)*VWZ22x8Hm{>AmcIO*s5*0KV?V zDv9moG!6R1Rh-_gs_G?67YeA+pF|lh_Ev!!S>@Ii_nB$APHe2 z=86#!?gNlTnZm;*)^$>fNn3(KS}lYv!i%CtLA~KyjuFL49XV{AfD>ddf0IH^C;_w0 zo)8dCkU{*etH)4GNpF%GtJ%Ih_AT^GRB3)&*1L#k`r=ei_DBFVY2l6*-;zJL*)r6X%D_)q0{5 zW_#731UoiOC@v{)DyJbdaW4!)JW?|8SlflSTK{CCzh>i2w>ub#6e8kcaOaz^{6Q2c zl2HZ?_s3A>j0zjCKOHuRWPz{`5)^hpLZE?UbRirOI#|ILCXvK39bPJDHS_vazd{Zp z4NFf_Bh4~zEgsV%%oZ$Vb1wiQFc#3_u*fVXwFcG3223v6lE zhA8}36&jcS#sPTO5ExUvNrC|K++N0AWnSrz;Nfq=f$(@PA$Da4IX7u#H%++=N+Lq> zWzj}Whg-BWUB5jyZe8C*8wO3%h}cd{Q}A(oO4X$z<4`odcD;a_W zs??oL-L{4?n)s^B+)3FUItm0X3=Uq8m#xTu=H_1ZIrV+p%h=RkiRmgwbrMQN)xHw( zL2y9t(7#Cc?I)!ss~SoB-%@p{-zS%+ue&d&v4?FjPh6|3X|QZ-C25t3BoaQC4o$oiPYeQxWSLx=)Jgu=}+*)HRZ(B=6Pa~_5GZv<@Y0TcV2P- zhH4M)gG1nbdVDR+_1b=1WRClyfrY&JLx6h01PTWJ@_y#pAi@Cp2Rvl90&-T=ENG2T z>D-Dfe2)o;8eSdGA~-g>bi}|py5T)%p%nGm@Z)h1#eCP%^mH0yBpE}!PwVIdJklS9 zi!d58EuLS>T{Av>P!ca=L6>tg@k$)`Ee=)3AZ;9z!u1J6U(^2v-`2o^te34 zze_F<*RRN-%Y*8K3A89^twjaGj-h6h#S0`Z25P7*1s|$SJS8d_@%wm`B8r!>&n$K` z`&5EiAY>>tUBw~>TAtQ`xXDAIH8;0#Fc@Xl3{(NWI+rY*tWNMF$z(rbONs6Gnl$Ad zVllRui71eWlsDjzeyi{sXNx3{pF<(%xLYhM%qdCP+d%uhHI| z-FH0a{Wur}^u)b($6Ffrkrki6uA3O;;3N0x-L71@8v;YhjQfG1T+6@8Qs<0TZ%z)s zf*r?N2v851(A9K;e97V&`7@Puw zkWkeNo0O;}9dkbcprn)$Q0n3W00n~cLL4d)GYJiEPoF$Aflx3yxoWLS;E^Dcrp_Jx~?aXsBtRV0PQTb zHmVUFN=!_D3lWLBxwR)@!O?RmI8YLSM#yl00jP3QT-zFxlJ`n35Z_QtD0u!)rQ zau3?O+L}+s`nGwiCFAj)@BM>hn$I#A&gfdkU%xB}N~{A5&4|uBw>s zJ1>W`71CjuP;~mvBTVlO)o2I%5f_!x?69cD*?AER>+LUmuW>IpQes0@5KNGODag|v zBFF_uLM9p@!pg)!1jmN%#2vdyT@ju_}j-*@W;L`-jL5EIQ zkJ+p2XA+8gH}US;#QzxXI9peA0G@IDYz&5?#JQj}9j~Xx0%LaGF3+COuHiV!TUi%O z_1I>wqy}1D&b>-~A3R8bAE|`;7kqj~S_?X5ps&z&DqNrtm%$hqE2uX+Zof82CTT|! z)d$BBdI-RIds|Cv^1 z(PvR+cKho>4U<-0<5X{P%p+R+Ti9Gqt{ZWAP}{j0znd77g+~8dLK`w}R_nEtrM~m_ z1YxxVSi&X!>kOo#4rn&SF?<{@AX1sVW6W1NbV-QDbUGQ zZH}?6Mywu6v=FgR>fP z*_hFUHL~K3k}&~Kx?zyHJSF(h8;Rk|bslSb<$SpkZ-O3!vV)WHY=o#>;cQ!MhKtQCSXJG>aJH(@d|oc;35D>kc2B@dSZ?-kq^%^Bo;wO z#G0Z9FDgnD5lgTK@FDrkqBZ&1s-<#LPb8Ywzc8wb#alFMR0)PO#0<(=wh)dBkWf-Z zBIET9+-e7b#)T*vxMQxipmOL0ArgWcKD{86a!_PDB;ll3Rbtc7+26*EQc+0C{Ux6p zLvn%QtjSH8c2(U7r@%6zCCz|B`U;`})wWV+d)jVsZK5OZ)yGI)Zm3^qo5E9d624l* zrKUb`Fi;rlO+ApGhTUe+LcXx;NA3ffL%MrCEBPZEI49UPRg8+nXjY@eypRgAs)?MF zf@4T$ZE4N2&Q_ggIW}mLK@?_MJOtFz-rk{YB;{tSy`UOEY02&JA=C9Rd;Qe)Y7n_& z#|V@xx8rcUcikq(3zEJ&EI!~Y zgNT^l#I5)xus7Whh!AlWm0vGhJZ-QDW*Qneg*Kof?6haF!bHyptjF~&W`-iBt_#hL ziaip8?{cu-1DmKUaerIHQJfHETzHAj9DqoO%3#Cr?IMqNI7aeqMz;I zf%!qj7OQf4n7+@~asRi6W0{UR?Pzd3@I#bD50J?3XVPTU&`d*BU}x~#_o(e@=*97w zpF!i?Ec@TyFX}c3VWlI5jIIg1nI}KyWNR)N$}qS!T{br#WEv$U0;ZHrFOl$P0yNMY z!AL}~7SmEOlhW~a%vzN`bwbAUMb>l8BAt@(NX)j^HPz2Ba_S_oo}%85lJc$6F!fwL zMG_u)Bk54<%9Te>XOt((hC>Yf&dIgb`W_lsy==exH?AP*;pa`Sin9q^^SB$l7Q@Yk zllqAJHdFX$=1em-n`VDn_pS9K+}HHOCp)Kl+`QoUx~yqc0;U2Q^>)aRm+7e8jI=)l zAwkiC_4$3M3>skK3tMn#Ls3RMvq{g7)Pi;dOWvCpe%#xBveVYGPWaUag3&U2-{56%HCABqQTYAr4rM+!PY}A7C>smA*5fPd0{!ZSFlmOv@nbZmX7l=7qv!l5Z88$w) zA=M#9?&kT|$1?e*eAU3xMQ)KXADy1VP9(b`EM4S-Txe2E8)PO0AwW6Ei=#Zwg5fkT zc$z{bKG5Un?C&3PyS+OO3s32MTWrAh;d z7zS?-NbI{&vdmHO?$@;$&^Vm7R-^mrBT)ya5p?>>s_j|*eR@5O+dMxbdEQ~yjTuEG zTTgGi-H+}zDbVZ&+F33Tg`kO3azoJBXrKT^X+8nNy?nz?z6=ETq>rZ@%7f7QWMETM)&LSi6NKcudm z4UK0cV@VGJBMXz#vA0r+<4Dqcq@tayGaDKL^Kd=Pb%oFIJA5%WPyQAVcxq8}OHYAI zqvRimvEzyt$aa2?uJ!Uze#Oe(ZfGenY3IxW^jo;h$*;H?Qx`cI4)#v6Gb<2so8I7Mz`)O8MN0iB#(;mq+3g~ ztk*!>t%FNQPfPq$`ei$MpfqijHEqk$K20h`{8twf zhr6Rbu~Pz>IyC3s(kE$R@)ld}md}P)DT#b`D+kNn>9yAfu%x}lHI*K}3OqLzc($hD z@bOU?#ISsAi?L3}(^vicQT5v7GOLc(x^60xk&U*miN*QiJus|l7Jj-@jm35aGt97sh;gBjLNw<*O+x}KLk@2_WZb3}vWoeKja=jRB zR-tCCoK9~bI{^0Y#ZU6l*ZH{QX8wVQafEJ>mOZ7vYdH}YoAk;=ifti(6f7 zmPriKo4cI{wRX-r6^n##*WI@eV)@xhI={)j;J^C%qu&uYf;W5B1UQD8^Kzi6(9|&i z>^SAZSjU=R@NWIg<$8L(J29G=(Igj}TlFUThrZ7_NN%~j5fmQr_wb(5>!ntZko#hG zpqRpW;}Ah{p}nANoLGrt$~%g5{_I+7AfgTeA6Ncv*{S+Pv)t$iiSScO07y1wHlXrU zU^c?K13Zcz@y^>k`-WT}i3>lqN^HU)qzn;Ez%yaArai-p1-XSh1#~tHL^3_KcR>%# zzI(Cnw;d1dmWbmgI={?UTAV{Rwb(x=SUv0CZcQK<)#0k4%rpHb?)<)Pw1ac>pBP_I zJvEi3yGT)$qX&?_iA$m;m;2Q)?1PCHaybPz^^zVblM$0j#+GgI$MyS`XxO@5Y^Yi3 zd*-(V5FpxnCfA|#exe>|AFG~hlGW9HLgeFuXro6Jqhy$j)r5TkP?LlLiIDRw{pppv z*jgrtqBo9-Y1$?e)uWYvXf#4KM{J-{g=+O=ny~mPzz7pf;-``?Ts4$=Sm5bTB?bzU znz>opcL~k_g5qKlRwEaN^ zHMdwZme%H~Zd6sKr4uoy#>7r|{C)}c5g;;$2jMR$v0(e^1AG}zftZP+skP&+gzO9! zabp0tii|Bf42toCaX|nTkKK5PXhFozvjakK2wgAh5=xfM{#a&JB%%dVkxRFki0WAS zh{*f!6!5OLCTBPOjRx)qWqgqD*X9)WI9A)!(Kw{TsD^aL#oP@|b@T;Wot=foO7CsO$U0!C|+U>T#z*KgdB4KP?9Zaf{wpjhGh7(tCkU$HgFq$r1 zhXqXPjL0I0VBInz0vBE8Iv^_rUQc%cO3KLl#;`zsiQ!I!>i?`1a5M38tKqBJb#UW| zAIP~PKl2t5aJg3M&o)&m^2%l`F?Zagy>F@~uKH0pYKl7Q%x~fc|Y?m(}^UPYH`0;{d8#y<)rzVjlOPRCh zvBSdn8$Bmc+*HGUAMtWYd(KpV*y@VgHa?8=<3qb|Gi&wK42WrNMHw0B2sYZ@I`j#A zWVUQyyxV$ZYdA09@r@lVweAWxm6yOPXB}o5cEk(6wmyBJBuODCINkPK8V7oBZEXf)_cbH8rWYyGG z=3zUC?h6UxD|XTVKkvNF+n5a`@K$axF<^B+I5YDud7Du&o>l3ottw&)cgsYk+ENV} z#;)0AdENKWn-UW{wL*gg?h*j{b)6v%m}s5E8Gr!U$(@e8BtodGn7=I9!a~n~c8oQC zjThFJCoEaUXOO^DBT(^2{3iMCZOK8HR=IM^d`QDkn`$|Ee;v?e9AAeROZwX^xA`|8 zmVoQf#R62oYOAU7s^vFdh+@k;tqY7U1q$b0cvxM?dte9-fz%j5_#C(lXi1c*z2TX* zSOoAXgrr2`ECVrbHnb;fv5nEozsmg!mcyQYR>#&l`3U-R<$Y!!DIYQDw8+(4XM;yc zjst!)nt}H-1$eKolY3@!?doF(80Gac@7cX23J#UGH%Dxz+HIZfW-ov5ojaZdVvOK7 zc>c!mZm$AvN7L)C?e-X7(m$_F=)XN)jW;SG2alIb?aR&vIl5eJms%Tt9emWVZn!V-ZfvNfM&22Ke01e2Z#7n+ zhyK*fRE`K`6q`y@qF2kh$SZ;gsRjB=Dxtyk|4i^Qr8w7}d)m&!`pVG5iYIM=lU)(7 zu!~)=PuZseH>{64d1+bg+vS#u!*S-QWtoHO^c<=D^#<^(vaH9)_Qh*vHr$G@(+qn0 z+;_jA&l7`;yP0}y?bLXXa%d{Efc+U7bQQDV6Ij6Xl%+sQq|qe!HIf?~`0{(>!0C0~I6ah<4$_IO8$f zhgC=j7F0e!#dA!4zXEdxHq0v)FFHqWD>2C-hk2tGeEfHvxf1yMRG%f`Q?FsoUDX&4$pS4l{(cUsWSnE*n zDA|#iI#L=a4O_7)c$2lYeM<0K+QVb6NKWES9J|=2^hX<0#<&1rFN(D*Hrt^rB@)}K z2mM*Rg!&f7dXAN%84cZ=%PBZ?d!KVMBGD4^b?#t0-*Yn}F)d{1bK@JYbJKhqQ%P3P zVN+66uKYjdDaL!=B+PU>IGTE`x1Ncnld>_uqcZv)0u-w5w!l}lNX^qAfIKZRxPjMy zs9>EYC%;4xLCq(Yv{%qhIqOcr*`$3+YU_gs_V|RI-Ww?2Z=Ylt-=i&AjQvpc}){cy~f2;!^Ws% z{Zw+Nn?0g~tmG;tt<`T473{5-FB&gK{^Ff&y7-M>#s!1h`}%GSKPaPrdm>@7tFJcN zuU^NQ`u2%<-*gjp;LBNYwRo#abCDgGZ)+H8JYQ!J3W>@0v=AAW_2eW9&$k|V7$-lZ zR@;WK*meXvGDhK3pa|jE+yBaR0RsyPND@SK2@r|tS-|8f9qwOZzjvaf7q7%7Z$$s z`C&u_Yy9 zY)@KlOzP5EMr*u79slx_%S_r!Ml(InX`6AjwYxDsFEa2BZ0a}G)KYQ1v1DbI*E2k- z(sXldZq!vqw6pGe{_j|EF#ND9@9_oj_l=1uKtgH(36Aboh~$DB+eJWk6a`#CAyTZG z2PUFFa6k}PG!Q%XD~e*CiY6VbVu0cPnzf!2`fo_bmRn??Cg5XA*znh&a}reeJtAj4 zbxk3J^18VkalFZ_(Xp|t(H-Y{pOKFVc~Xrx@@2@JU3{-Oen^82F83sbHx;$QsZriD zfAS(lXoVr$n3ZKbwihb|>WL`amJg3YLMoexhfXR{v^THmsB`clQnnI7ns}!3_M{1f zs%k=xE-wQoPrr-$rIcGADNSuLEq&*Fl45mtl2ZB$+kEqky35(N<>`)=#l0v4$ETB0 zIM>tw($Y<%SupdZS}R9b_BGl3fW|ksX8|``I~IZTrHmZiL}WoY&GNQsyN%vlc;?XT zzF-iA)l7U4LNko4LiTbWLk!)~a5EjhzyLx>$l)nG{1{Lyl8_=a;J+jhe{%cP56eBk z32fWlIJ5HOm~MLI@b=ModETI|g`#>=K>m(UI4c+UM>5aUPvf0;cH~>vu!-d=eIbJE zaTZ9w-m*PEXjTtV1`q?oXB9B0(C}|YYug*gfVxCa>(wem!j|q6jN=7)~!f>pjnF=Abky-nCRvhhw`ItpcoA3$Ud&y-to543TO@9tVLLck*) z>?N#h%5N6@{O$X)w4AUxVGmO*z=&Ir?kC`lG2CPl2!ssze*j59w!fGn696-8su&y^ z5gHNB4|tg(43`~DawQI#{)hgM91yoP7Z?5cA!Yfk1*BB@^KGg3!tPt{$B((I?>>uP zy8JdifA`bw&HOZ!5E4!crB~}gou8-&orHm(De+MJ=6lN7pTeJ&|9uNT0k^wV1#+h06`B}lbb#otR7*sN3_B-9I~ zOLeEtYw1qSn(h>;@Tr)EJqG+6_r1|%AnMeq+ezq|BNV()xkKoeTquLtq!R&f1|Y!A z9_1fF0w%&VS7#2W9mCKOY6y+-uwpr?EuV!fP@$HIo({YGq5y+lr4`HzDO%-rxn-ESgL3}8N?=stBSnIM22=nfbPT!zGSX9$ zlMDca}VU;K_p6=E3DfPo}mu*A}IUo234O=Q^LOM05-xSql(zlkYR ztsdP(0^z|z!VOYh_ojyI&Ze5Ah^)M#xHM{vlMl+ao{bI5?r=tC<`u=KRplk>@UN?` znQWKH(4`zDve3n54V8J0%}6iNZO|4m z=bFOU6M|8#7*i313Ugi~y$L-2Z$%za4i@I0oQF%;>G5r=D2So|;OT{Qu`i!XLTS+j z+>0%Pq20CUFXW0AWJ67^`Zy3t5MYFxKdOf$i23SFOTeh@09Qs1A_~|BD9w_RY>cJE zOhh)Y`RXFo5HmFj?P`r=jH;1r*?6?kI-k+Cxh_U82DEHHKW&{)t6tk`B_F4iY}qqf zqWi_)3@eU4x-4^g{`geTkAE3lJH4mr8hS}UdW3{i)T)@tBFT^>3$i54=|J$)V}b-S ziDX$7oH1OX9thD7G9#%fJ~S#cG{T>`&auU7ro@D=ZWazm7m07b=(z}QC-~WN@c$SW z492R|f@EqiJr{d#azM|4C#DAu4`m3KNO)dbQN?s)wk`!6M!f0g4SO8;)JX_()VG=5 zI}O6*16y4fm#sL;)U3`_jB~qIIg}4iGnqeR@mInVLJ3MicFKb%#TY_^1p-O2L3#m` zWSzt|XLEE2hQ<^bqXthTQl!_rLlbp8$7nbLL@$AFa7o$PBpQnq8mzQ{m*^u~!>hpKPuXgpkeZ0=S= zh@ZbRf8;jzox14C<#nwGrk#4(P%%^2dT_?somY&ZuwdQoYwM0RUO2LU-_DGDfV}KI zCw4ZRXz97L=K8%jvhw;B)fl^0-&9|*dpZ5(TzwN;#r+70DcrBc=E~Cp=l~PIQ!)?< z1d#}nejW=Q_eN#molJ`6p#XeZSp)!`S6Pb%V*dv=ybgZ&1NZMCH%d7!rNDJj%1tHn zCCY7)FqgpoTcZ5(4HAGKa_8+N^20lnm!N*f<2&*Hu>HME`5P(WR9Qx;u4E?4vpuZY z_%K~xvLp%=fP^xF$s9hynd8T@TjpzHkP zY9L1xKmebOibcTd5oU{I+{@k8imkQPl@%q9)QBu@8xJCE;#4!im6;QK{L@9l(s)sU zKAY(xptH?Ci1PRk3PO=+uIS7uPM363cgzpXJh96qepXCvTwPVPVZ2%=MrM}R)F!mw zv7u5YSjyUS>=sGPZ=2q@vux)4dWUpTh<8mm?Hk6b^io86X-$1n+nsZjipkPyZk-L# z=MSvQw=|p0)kP*rip;94t%Dcr1NDxm;+xx--#(myaq-%7t8w*gqXi(ksIM%oGT$5@ zkrMZY#^psRSs`$t{QTP?k&oDbLL*7YB1HFwnAc45isQmGpNW|BVQ$q56Pv zp%a-=8R|oQu5P0aix`@{4wHVW!=$!r1%xC4O(R*>4I`bdfvINo!$_~M)6?bE-C0}B zVi71lb9zcxdWt_|RXlvLNApezmxUj8vUe$%m+odk_PDYz7a~HkgKpSdF<>_kos`+Q z+__x`NI% zdvEV(-n*hLFHELStM?x7nET8o8@#+>FM)#2S;s)>nyy^Hd)9%oHN{?+I+RpXoC*+; zYD*mHf8;`d){XA z*MOfVzovU84LQ(DEf*}E%jPj437U=cUr_?DylV9tXZ0ZIM2sZ5fiU)JE@jzcn~__M z9EZ#7W6PB#kf*Ql8RmZ4Dx8j1h9nXBet?#OGn-XZMmj>d+3Ai92W@g%YM41K)sIb? z#UV*7#*a>#*9I)f)+;7pbAg?nMxMx75-5@EE%gQ6r3rNVueh%479HL4=?Nu0A@}Ml z2G^HVPB`PGeEL#(qIfnre1CkU-6D{Xgf#qGeqLc|Vo8^+e#8+4MaKNpNWcZf{uMNk z86drJd4=+J&S1{Y;_UTQ9hZ|5of7G{xQIT7wvi9W32cQeiw%oE1{t=Xql-UAVJzxl z@y9gk;aO0BAV8mk?E)f6NJNz$=`;!<0JQR>t^!6^BU?ql)9 z0^31%9tp_B_fQAZh$AOzMV+oT2k7+WAnS0m1T6Ggz;m>Ey-s8v^nM@<`(`Izo5N(>L@(e){T}FJ`uvB@trGw8xFFY+JJ-Dl0Ws5Q7skqgUg| zmfQESRZqt6erpxo0c+IXv;7b6@50#R8d|&4Nq0#XChdoxp;0~80Yqih;OxWr~hVjnDzG(VrEQg(6LYP&pvY*Mo;hCAgan_>!1>8{1cJlh2b&l%EPmR$slP z8Zpjve9)SfrgGKZcnx*9Y^u)~BRlXVlHEMzDCjDoYc;EJ!d}o@mbe=S#b?AC^UVf~ z1B~ghaRt_B;f12_rLCMzYvV9ql}=cGnOTMH1=s3H(bguf$y z6pZp*+1?mn8L&JjTnPuH^ls1-9^uXL!14t6h61zy;_ty^v~sTM;cDfUXxLiy(58iN z8%ZV^IIT><+0$_E<)Bjtpab7Sz9)VjC9#+QEkaa4NI;`n$R*J5(6Drmu&9QIG6k1U zI18=&r2DgxP#&3=l_bda-0F^?d*q_twUveA^=Wi`s@hC%z;*bZEuas}$g{`i*#TNB zpmM4%6(GK(za+n}*=0PA0M_7J$*;s$P${Z)RV7?ICR9R#B#e98SI)?sX9LL8n=UUW zE7NLD4fBSC(&nmKd{ZT1+I`uFk0`Q5MEQ6E_I4VJbCLuSoRDrPXbrhnvUknc^Cw%f z2#(8gB$Sq6TsA!CuunB*o~|#7vu7s?Lby5KI3O$s2#U*0j4?+CWapLiIE)Zm)LoKV zoMj4(Tvq8EE;9loln#_S@*yZbDbpB)|>4sgY1hxs18 z`5WaMC&e!*|DvUmGQo8TF=*MPs1VGQGM=L$S`sk;*DBx*BV3is zi&@x>$Mr6p>T|6;f`Mo*>NdhW5me3L&P_tdV1RM=rhcKzPyNf%5g)gBL*% zO5L|5H8ob*l0ax{CrOvz-csENkese$h(BOGmk|;g#b;3v--l|9c)6-cQeBtRk{KtT z9PVN9Y$CxYhZ>ikTa&N%8$I#)wGipXIv{mBA6>rwnd@rc!%hUSh*#mCk`@%qJ*OCf z9#F9(=&+>lF^}5pDRLDk%Y@E`1|)~W`Aga=_q^#_Z8o`kx{xf zd_lnC+bkCe^F>1#0fupn;1QN*#Fxx{G)7u8&%lJfYUW^T<6KLM9H@6qA82jd*kY0% zB!MYq8JTqjiBcd5G?iv#v7f@0=G{a27#9xhtl!#v{ZJnLIk>YPTk7n|0Eza-wC(14 z_A`P0q$lu6beepR%wsETU0ltgyx&2QpG)pW7TET09R$Oh zP>c|L4rZq`{xdzCp9j$KI7W0g{i<7h4pZ3A>-e`@@$XCSreB?eI?q?nBhYyccKrJb zp08-l!ie}1`>I$*xwM952=TApSiB4WR{ct*UlID%Z`oG}qkd&8*(h`%D=K0Z$}GlT zNkBvpiLV!w>MMs3y5fzHaupmyh{rW>HK4E{PYoyx^8^$IaeAw2uT_^RPmp1_U1R2R zFn&{t&8p6^bk^p}Qb<-q{^a5Ci+*iePxbZOIGUxC%LmumaZ_LGfw`HS7@NashT{BcOg~bUG6*j|hma`I zb2u1lXB%LTtA{_!*2U={8hhz_GK_==!rl<03ax#lE;l~;O16#O|QDN7D0p z!XiWg8~_FY8DKwLuUy^_0?p2(+z9eM<=@I-=!Q;c->f_b?T3`-m8UmBr*Z)yviSDm zgJQ7w2V_QtsD^1^;z7VA#Tn^Ifs)rN&_k~Y;qHCjxKL5N;$UiiG8 z1mjh)qm62y1Kuo4vipI!-fM(9*j@&6{`$>Z>{_@(Yvo1d`?)JuuPS)7{LThaR#@YW zdB6qR%NM?rmahdpU{uQY$^{gH;!&Y1pKV!DK;jG%5MTkO)ew9&ZVRE%_|UkBFbW^ zeP(LS1M3{1e38DWasLFH8)s|FChF2KoBguI-x9YJL+7i@{tZSAy5VqrzGUKg8JW=kSs+4PMascp>-3HH2AWtoi*lO)IIm)jCW5^v2) zOArPBM7yyzEqS(+9?2mOiGz@{5FX-;Bx>`Vk16`BdlP%4)opqh%rA39g z*;aFMQe13gxW69NKurj9_G4qC(kM;(Q`OTRHw%v+AfsnCA4fnrnSlZu(QvP5s7vpt zD+xE2*R*9;O*rF-)UxBA{_0pl!qZKuMP`HajO-ZP-jG~fY74V%Z|qoKZII21Awbeu z+bT=Ob4+nHjm;eejroak0Mv#Nzqow&jwq`C4Jowe4>sgT#6LbeJk(^jB-yQz0J)uO zD@Jb|%z(S%%JLEcVzWyVb@dxNG9Vx(JwCq_uyf_1hTjbo&9vn~TKp}Bq;Q#h8L}D% z((Cspx{8t)z8nx~3dMB_*=tZF*=# zfG`0(?GvWM)F5eTjHQ66F!hvHoewD=E;99|)xq_ekVAN03+)6aPjqHR>4FE3zq4oh z{h-cjz#W7pksyk~fEq?Gh#i(xi^&_X<4E@9 z(=4RWmuVA<7aU3CUHAJ?nrldl3*pMDTg$w^4b<@qFMAQ}xgG~9LoRcmawDHd2#&L` zHuf*>6zYY~A`{9-PDbMc{jh+F9U19@C?ps$R%!euL@?Rx4eV_2O)`>|6Q21I&j)Tk z^LT-HlhsMI0J!r5Bk{BVJiZ`a_m`5%^pywiG~423$>RiLAF<9Jyu(_Qq?6`F&_{++ zf3O;l2TZ2`y0BoSF}spG$7migIM#6XJ?r#_?G+xk7;K0*_jBvihFz7ZEzR`___0Kf zON>6Hv9T_}J#GZE2K>>55}|xGuG{ zW5vGKzB}!c^DuC6z4P9&ruA+4(O9}90`-V&w)NjwFr2u)z4yAld_QccW{&CE7*|xg z{~=#;KVMzP9Uyn?EeGng%w2x^s@?ytZ^5`}+ldCY)8Y{lF4I#g92potqLVQ46EXNB zmVYL35Ih1$L_~xkf~wbjk|zf0;~^!fm%LAQl!YpxYo>`6(p3Z~ej-ydHGS?#m*FuI=e~Eg!j$ zxHfvd8CgFgC~be8>pzUB!{c=jiv$os5a)QLs%Ne=m%xON6w=qe{v^zcDZ;Eq=hD;8 zmTV&*kqh3rxG;|!=3nK*;>=nA!WXzAB{AQ42|FlNwX~SxO&;aTs9tpfCS|q zLp<(y&7BE%Y?&urJ&*Ue|9Kx?m-BcwzDM;26*?q@LQ%6z9~eL|x9R+U=NKAEC(*m; z)uuR-Sz0rXgXkpW8kMijdAGo%a$f>W_cCWu_csncaoD|!4t2rZPu>mhAwX41lW>Ce zJC^zP$3%n^k4LW_5Rhh3z+UdOWvSpEi(*3!W{<<9kD~|Arm03X3n!HS_}K@q?~E^w zi#6m&r`^W5lx;teWdee}yQm|hBGTjkh6y%N+MSM>wFhHOI*pgF{c*5do>8x+c04^q$TM zOdYyUb}zVF_m35Vzy4mGE~%(4m7t=D#!QSGsusS@eM1Wu{~!iY&4Ur;q6N-(&nNMG zBS!YGp4%>>Jo!2i6MZK2T8hrv7C#+k>O3{FT#X0F@z`j7v?4%T3H>1SvT^ImuraM}=9{lZ&#ylq6|F ztr%suQ9nq|tZ~(vU{uyt?X6mSOLLfiV{rY}CpJ~>alr#dWnT}p8#`)zzu`20_Nb=1L(1ld(Hr7=brdeD00`2o+8%si)n44+9V$!U9ySzM)1bF?Dse zl$sGiLDWC(UZD%+p>{Ni8eFwHkWnHKY)QfbA7RSTWjOfzJuVsDoLD2& z#E1I}Mk4xAtvY`SAn*qW3Ls&bX-NfUV|ajw_jEscV-Lnom!lcL;_Mxp&Y6drsPbx8 zVKQKA!ruHW3}sbibr>@0MwYKGz){ZHT6%inL|aXLt?`{GXI+iM1dw2>X{a?4sV}aj zIu}DiVO32fw*z$}Bpj7;P#E(m(*qzsz}yQ90Y)&b=`|@pSgP5h*Yw5^c^ojH`?a9< z>mFNWX-rMY%&DL2vHQI)ET4XSR}Dab?lyl6+1(qfF5k_lFQ(Mw60I$R&g;O^@C+>%d?LzB3ULsY-mBoBJ1kCzgwskr~yXK9^q%5J&-m%j+48m*H`f z*UYP8WQBk?U9)iTh3srYwuK)lS`G=-{_l_^rklNHii7HFXOmcUSDv|u+K2BC+`Ea! zqkS~1ge-oltP;!WbZI5^7P#RviNjwU{qNZ&Mya&;zVH-EMkOfMmE{Kl3I`%!RUFYI z5g&a;dS<49_d5Ke-Thea`!%ZgF?=N7VmC+0X9KsbNr=$>zxd(f$|Bk7sJGw)k0;gL z-}7la@_F>X_*e0LWMaN<06eIWF$|)BG4>>N8S&6pgeZ{79GMp731?6{ba3Mr1#t1RLGD$bm?hvCZQ~Z_#7XreaK6vE#=}ix> zO!pIYISmtAZfIV0W;`_@9Dh$2;+Sb2e!QtNZG3iJbM=~@%1HN*M16!~P-w?PcfPhN zyY7aaE1T0WX+7}t^vt5reDN8JHg7WyeCX*vQRuZ3c&M-r)wQi~clI&@Qs5E;L0=z=_ zDQOU2LxHMyE+;q4e@Hc=`5GKnsD=dYxfv?96u2-{Ua=2goiIvHioZqS$b1cJR3&Lm zvuH+lo5xDa-A1G+U#mOTLuzSTCXG#s1)NgSN`Dm?r%7H$lr=G&V85v3NJDyJgczOK zT0#>_*IP529f|P;wV7$!Kn6X1W|>t>sY& z&|idKk|(L&FCMkHfs9LZ8j9N~gJ?G(h}jR+rO~Xh{SJr$b`M z?5(mUnG*rmWO3g=;XPqe{4?|Y)BMH;`SSG1>cX7*+26L`E050`zNh@+y!dD3?+~c` z9ntw#4&#!=r|3+ZkW&p{QZY32;9enSCmas)F)-xU|m%@Cpne`pwUUR~FkP1Jh}`f;98I3Tz`L<=*H1 zcAaEU9$s8@2MWJje231vj2>8sN9^mr2;W=GlQts*^Qfdpbms+sUa*z zUlqdTO-#>%JkOZSKjtF8;|otG<_=XtS!$sZCk+{N?xI_4{A{uJB9nQ~m{h6nNn&*Q-S7`#^TZ9kDH>6!xkEHAwNfP+h`{U9sWj7*ltMD^nP zBs1+9PZ0x?A~5bp0F3iUtHqMa)W>ox|6cT^Q067ABPm}$e?<8bRB)nIna>;g2dGe6 zOlA9o5b0Ir->Ye>X)$*Mpw8!7A3V>Ci?|4{B9Wv8OK6)NA>jF`9@g@@d0fqL8q+nT zdy>D%1~n^^aErU|y-RK~DMo+zTlxiFe2so<=DIwI`9w&75$B20_(46i6bE(lF((uJ z{C(fPd=CWRI}WpioTr2sM6?WRgk=I_XFpqEBA`jHAe*Ntuk$fW17_u8##Qcy|C(`) zIF)iEyz!U+OWw6RPd)N~%De`P?;s?EdGj|RfHdy^p3avuA0JPK9sHX6o!9U;&i>#z z;qJ>T*bpItQt7w%tNzVA_oNya!t#d+gGdn4v>*mH;Ou{Vd>l+erv5p}5#z$vU*I1b z-6jXQ-nek+)G1PVn)sb&tDuP2pk~KH0Nm>B+r$t-j#hIj(!U4i-*Yeg;^G2XudW3^ zf||QBfO%#~Jd>L@bLG<3QFm~L3xEd-0c#@=gZoc-!MKoM#O2DZ(d1d9I}L{MSKVJe zeFit-h8s`2zm5p%L`XQu^KYf1Tomt$&CEzj3>4Yv#77kI*2Wq6e)PR~hX+M)=lSp; zA~NzsDhY|5JhsfF_wT#ug>`8cty9OArRe>8Z+>B2#zpr#vYgRXdvnX?t~~slET?x> z-`ujPGgtUo;KZ?~hu6Mx+o1lWU79NMpFs}tGD9Up>uSQy3#d3Tw?V2I*FQpwrtARhUAMNkO0;nQ`q%{l(7 zF3tvb?0ITk<$UGT4ec@hE9YlrA*H_j>3R53NbSV-8oa`N_v+_v>t}DBWrzEBm z$3|7%b8MJtoCa>HoNTdMVs!ozlP>$6)oc1)xe2-e$q$08b&dq3G0t6Bn=1PU_|qJG zyH73aJ2>G?mh}EXr9;=XgJ@J5Ukj%0lv}ntx^vg8o@&b_nC3xWx5d$&xpvd0$)=Fk zPZ8(wdmiZQXutQi>xT0&E*ZQ2=v|HN!x!$q(MC=oj8;+!)GTx%3#vqKc)ZGj%^;CN zKvIYw21J^fhE9Dg_`Y8Hh9yf&%|^Wq0MZxHkwyADvl~m)TNP5;K;#x~XKa zz92P9qPCjvg=ARc01NnNNC-e(U0pL~S0HWLQ4&>k%fT%Jbl@9X_PL)=?e1tUHUW;Z zI+Ac?HHEU&3{EZ2MtD(C_6a?7r6tgL*vsO#C6WuGWB@j>9(89b)9_0ok*2v~Kn8?3 zndkggK086OF&jL7t;{FFGEH(!QX-&=GFyH|T4H5VWlVHrI7)y7z09RCEj2SY^)a6a zLigH76Hu{ds{{Y6=jc>{ET&a-W!svvLj_&;;b&L09WNi4|4-o1p@)M@rd#tRG0N7O zUp&)Zq`xim#>Se>1Jq&{lGA2CX%yeY#mkTO*@s)oqhne}S36cbzOxE53wELS?6Q+< z%iJd*v3yxsZjaLpV5uFoFT7FV0Lbp#P?bN?ZX>{QqoMnh3E?Y3De_}D?>J@6A(}!%3uS=KLer*W zja?@j3o8%Qm>SA6Bq_DD);N62V5%Pp8b0xbts5Vn&G5s1wX1LI3ow_bNA&B0>40|c zA1(36vY~Xm+A-ci!$T$EkDXguOn?hb`(mvSmg=yWOR|#uEo*Lhd?sqc8^?PH>A(GR zvr&DgclOW-+oI|=x2^ins#=VlYmYWZlz)9Jzr*?(GhG~ zaxjsN&Xw49-)kofGH%OXJ6bIZk?D@|ikQ6BWA*;?{xf$x-J3T%y{yV4kgWQl{N&;c z1I9TmQ)@RCHJ{wk3eQ1q^FUhGilG{MDAn~@{v8WH4X`$prxcoFf+dK^ZZ3lHgIy*J zK?(VFqvgeYuB?!dOnbG}In|gS9{||gzM~KFTxnrIsd7zGL0OU?z@2Qx9iV&mA$m^b zp(Iy)RAdN24gB230FcH_PI)LVH;1sk%D|+|D1+)+Wc51V^WL)XU*VxBrJ!>6>Q;aM z;*PIo_c&>f(#ji6BVlQ#Sb;>QW`;f9xUL~VC*{@+jE-l*guBToUf8FM+Sl*CZJ={D zs?gt<+u+C@Xs?MK4A0ChjEhc=3KUaA$_KZ%R%}_fe6-gL<);xKyD}*x@=O`M^IR>b z>AbWoBsQjhSTBTQAWV?el$87+1}m8n}E>XeybkPsg~aZsEH^DU*vIp{vhEqnzSd#zM}? z2?!GbPkM=2o@Ou0N3B+a)sRgq8sAQ^;SL3sXuyP zV|O;jh5Z}%9(F-r-LV53d-(IFy*E1ld~0fNUuUHiw>g#GpiLX0gdquCs1-&EDP zq9jC~RGtt@-IBE?KQ1Qhuqhei;_*F=xDCPLmlyv?enWj9)6gYPqOD*SB5%c<#3cY~ zF#5bn00Hq#B7XdYC%`D+oVR5KEdu9!*$E$UEr2{9fcQ_w;4*4u-k@k&z;yrl0<*-u zkeZQd4-w@U>b?M0kLQVlC-gCddwkecfzV3id;b>~$4lW!IVqtjp#jvkRytIj9q@TE z(2#2MkM<7^iU}&~tF#8p6$#;ojILrU1_HN>IBv@Ntm*|L)88+?YoA-Qc+c{pPq z+xa&r+lAfqv~Fjb?RG%K$!9DhJ(b`FP80FC#weoJ*5>5Q0)d@97N*z`IMp-A-n5w4 zA_^KIEQm4)14V0!Q#!lbU0ITB$ggc}PdRDa+yB^+aetkqc}4NctR817r50t4DZ}CE zDKUgni>&aXk@H(C^m5(WTiUWZ%TtKpShnfrQV5RAbY|q%?ca2gU? z_DxnzHRJ}y`Wv$w>^c2y)v?PWGIDGQF-egDVqjGD%9CsQ4^7n?!iwtpZ4GNX3KIh% zA`AgqzxW4XA6>mhw8q2T`LVzU&k{Cz9|WF%g%?MQ1S7;9KGc;h09)EDXkkf_Af)@Q ze44W@G?d%4WZB4c8B#=4l+OlGJ$RCZUR8pRER2)$XPAf7pOpzt8Dkmcc4epeV}a-Z3AovN(%Uo} zRK808tl4SdscbcZ#w``8e7;6yG{>mOfV!=Z&n>&ZWNQ9NoYpwG^3prRUgDC|=0|E?8lkIU7vavLsJVB=e9D>U7 zY3Hq$Oh#LhD(8al8N6$iLwJHDR16h&?HwuclXUU*>l;(sTdMP82`TDYUg%0FFEELM zxzrUk``E@Je=%_A_Sc0D>{xz$1WE=gQUKS^-P(}Rbltc!NDeWi6x8QdF7K+0+8bof zElW+dMCpaF-UH99%Ut*NsR4v}*6kOCG8Bz+cou1Is?khF_`Uor^@qsdBmqQj2@};8 z+eEl3+CjKa`D06oKcTjS@y&;tHk}+*})ND9b&uGL? zDfhUN?u#gv#r(>aV8hQ<5KG8W%WB_AYtInMQL>hKJJS;;NGPgPHH4@se@~nnaefp3p3cTQbSCPXRKM9L+CJLd5PXaV#gO=c)WE>PhmiW;RS;f z8q#_ArJcR=ed`WZ`2}{(cZ)Ivr<7zpMkUV9$iB@-n)d?U1A7!@@8);!Ssti3;Hy#B zJ^t1N{8}+s1_t|E%0kIgjNhYcVIv*+M&`|50v_!TVTbITEMY=2TQG2!f8a+fLKLzw zEi=X%ZKQI58j_-hD=>8n_E{|nSn@0v?glQ{*<8U3@ujwOKkToAz{CV|ahfsAkK|s; z#X2HHSdz_D=MkbKp*U_nBnbR;a#Sk4yR|l_u`o%B%y)Izs`_n_L@$L6)(IufeFtx- z3uzf$-dlo+sl2VdAsVujZ-u#9I$Y^M>G$%f9>W0;_G9OP0|#6Mdk*cMEWyAmN9pb5 zCcw^_-FvrX9tG(-gwX~%5;uwOpgiPc`M2@_gu?7NBye^M1S|la^HHH<>}4#=N^+@n zT4aouCqoS1AdV|yoQK?SwwJ1*L81#)qc=BN+R|c!+jc!VJ9+0sy1)H}Vks!M*IB!~ zAyc0)erkE1jCG~s``SA<*2d_{QmT__kotz*2r1Eo3lV@3oC6k>AJ>}?6D`)c! zRE{P$7>a6o?MX#(=7^|_%1CAH^z$p8@y|B-NBaB48bQXfVSW>?wz~;v71K59SX8n)s|S; z8gFl}k6u^^bG6bHo>E*_SI+491B-v>+F}#>zJ~=*!T?AVK%8dE9o-<(>7qP-Y|Nj_ zKp3MP@L%DQI9$+wg-hUY8J`NDEA^Tnl4S6}Ar8i~@CHzht}!$^nMQ_5+`E@ADJ`f{ zlOEY2cuEQBi+9A&+%aa=>FYM1oeZoPsxV1XP)-l!IpVLs_@45Nk^EDPR*JCzP`~q` z@o9Fg(s7*GI8{=zqCFR5g56zWG?Rfh(e-r)l`D%-m8+6TlmxS^JF+N=vcv+UMOnCV z-BY}lRyc{v^+l-28XC#2|I$lF?NVXw(kCrnw9ge_so}%7%&+a*JMJ>c`uu>r#)+LL z`c~h!(uObM=#sI9;Ty+`0|Ib{(fz$32?r$EyGvTP^p}RgXIDOTT{Xc4y&Df6Yk-p0 zI}guwWntWK{Sz~;js1lfckDac)^d6B2aq>ve4F+x*Q1U zK^Blai7*xibs&mSEfROlU|Zin`B*DL)*@?BMjF$nCR;-@lKmtW8LTE0NDem<`j5#$ z!>J`lIDKD9R7XFudb*{5Lq(V@#^qM^t#T^AeeZK)dV{&7e^-y0CcP`oPKgN((39=( z*z)$0*8Erzi~1-_f_Q;a0HJ&L>Biy>%lj)$AV$0YP;%}wkKfRRPeNw%uqPl@zX;p5vP)L>xuH|?ygL{cjdw0U)NvUn9;wjK1SA6th!_DPh0=C^`iSCq)ug&blDRm0b=ZV zqy%qw--YWp-rlzC?6x}Wxz%f7(ft>Ig0{&5OMi1AVYKEYg!rv3Mkd{2AOOwYq%I<; z@>vu&e;8OaLs(IyDMXfd-sY<{KYZ`rt(bdb@?%DiQe9@te2b({Yc4Yu#qODZPq|>d2{9Jc$y1 z&mCsm@&@_y?_uE=^KRua=e(0el79FbG3LWg0go9{l2812%zV?5Po!#Kj=|^vWX{Az0ZNLlPnD|%i zj*)V`98)n`MtfWZq9r5f6$9M}VeW6^wAOWX<>NIL%=Yh#w9j+=9wdJA6b3|4 z7hNj++@pI}#Du^g;(M3o0E^~{BG^jt*c=U&A`^*iz>H1w$GL|p-Tlrp#g(PYTsg_H z0f+GR_pVb+;pt_)6)^vf@_XeTSWZVpfz;*OZ@B4(3f%PJZ}9(&y#C5FEkxMQEV366 z5)|5^NEX=^uH!d{$RJ`sAfhRqgju`v1oI|&vRLu^to;~mXL7L#OhBmOoh@asLAVM3 zt!V3WTT7I;egij8e5I&n>+{=6A@2LiJvV#{9|89ke3bsZhrj0D4ER>}Ci<7)EU68mzq4ZE;q5N*qF|1TjT25n$;g)N>#RgZ z2x#5!i<#nuYv=2v4ZxWDiYBP}IwdV%r`NC96UJe~#mVr&58Y|W9TQs{Y}0ubQ;RvH zzBDhgQu)#A_|NlX+hu3jWStoxJoJcP2t;R;CQk^32vCV~UO07y#(>c6p)mvi#33Y# z2JvbdW8*ZYX#N-Q?_bQnciXB5D;C~VNz5M<5ymOrUm_&j$HtWHVgkVJ{EVK$40nX3 zo%0!2szcYt^=ki7;Xe2LhKA#<%WrIt_Gg3?66#!ScQ)-|V~a;(jQ|Zh9~gyq6i4C8 zo+3<=BJnE-P$)veo4jw#*FYo$Aj%{34Ix1=Fqw~*`Sb2^%0~Ec!-L`A3-5jP7tgc5 zg@vXCwR7x`9li{=a-QQ3)`fr^EPwd#PmO(=R8XLVxq1YM`q*P`00%KVe4cKF9o?YH1ZvwKHRt;o~q z`)_(~xkCBL@4?oI{WWvF)Uq5=*jq}e^y5Iv)|?)neP~k|V^*G^L_4!>Ej7H=rDH^p zX;D#cZVW?-d_0g0l8B@k0r*UdqQ-B9hleMIC-5^qNyk={)+!q;J>6G!5AJP`m-W?i z_f9Flp^-@cn0L=FoS5jS7^*bU*^Dc!+h;A`rhd*8UD9&+h`dkt=He%HgsyN_T-*kLhPg0*W#5I54RAq;=!-4RW5G+_59-MwR7S2o7B?zLNYI*75QySq9AW3#inuPagMjc)4ht)m~A zD!cl7QW#(JzX%EUiqE286vy&mir2i~k@7bxAAwMeAu57G2d6RGWXNQ@)&v%dCtVPq zeKDMwQtP)}xY18Pede_l@SNNIEnQ;mR5{?WU)SteQG>_b=kdv+S9WiB zV3iH%*(cEqRTd052`AOeB8-;PmAOF*MOmmC6}yTuKw$J5BfLojzzB+0A$cj5hEjkj=d&|q4)}LRV z=1)NHpIJS+>)wgGW;*+eb6ZNYLIQIuS~AK<8}btZ z0e+~w1fglw`I%*=5V#8hQYvkErAgck95o>%j0>epJveE1ct@D`rKEXq*1AW|UwVZM zk@E}e3%vNK@&%!m=SVlQ^&tX5gmLAZ6OV3@u#TvzDFfp&Y0ih}(2!tug@B)4hr%F? zahVR4H&?^XRTjNEugPJF4VC-w!{=|DeyG@OE4_Sjo*=Xu|DQfcVyVpxfU2C1qI?=XuJZQPQ0>dxo3!&m^z!!NwRroPjPdSL zS<0yEHDygNs}7j=o4)nY_S~uA&QiKiGpoANIz|MN+d8#wV`0aA*SF&5aDLlpR@Tbl z8d>8+?YA#F)0?nY+%!a~3XG#FTy$AtvFy*{ZdKHf!{Ruq&`IldrzuCR#VYGfvx2^*iPMOj81|eIR;}hF#%x)icHunEuCSyJ%o~=N<5!-@AVR zd%g`{Rcu13VuyF#e{z41t-^0mDl#mTeng6h{&Z4=%_Bt&zjEmTWv|^oF1u=MUtejI zrMRmkH9AP@;JrDoyjy$XmR%!ucqqGRZ23$Ee4XW1M@SfBJ!AejI+2f)z{iPTtPT|I zl}MRDvpQ6S^$hD_?pLn>fIxr0%x_ZO+C8#s{*_<32Wc0;d5$W#p44ZU|LqYqFdGqL2K~zy?S7y~Y*dcn%bF^T6VAJ-8S9M?P9=)z1QjY_h zcRswT^ZEm(NhQvg7(FfMba^+Q_PAL6|>=yko zvRk;1?Dp^Z2jIO2E&~n@Hk>lf;ezy5SYWe9XZMm~~avqLa2$Zh6YZS_qHyy*(7L3D# zG0RgGi4gOs@Rio0Kee=W_<=7yF#o_ibfqkGy`uJq{;@kgSdaYZNNp8LuaefH|KFvx zUA(5dZ1g|_Bl0G!#iy<_0uKqhUHvQoJfqH`Ma(yMY}J<{5u zuvsYO(pvmg(pvOKrM39*xKEPS>~`KTXc%nR(v>EP)vJ$oC_nq$(>R@O|Ap78`UW=m z3;Os%Grk{iZtvEnt^-54Ae6G1aetjQ4X^A=aTX+Egi-P0B4M|1T|-if{sc)))Y|03 zXu%{DHXWQS5~cR*&UCpKU!I?=0!Ma#eV!ym<<_L(a~P+0ZSP!mus;*jrWkS?@-wQl zV=(vsd|xmMohTbMy6Wh&j6j}4f+R|UI7go(L6#AqSacN6*2CY>P*KYg0uzl`Y{}VZyH%D>VTREL#K3`Bi1GEg8yM|80T^f50vDnP&+iZ-sDXh_G{ zJ}|d)XHit?zU$|Db1|7b@kA>q>2xuCBc^R&ppni;dhN2|zNE;Q_JP4B`j}C-Y@|OC z;l-aIB=ieqUTIDSmLf(yAt(7#kbI9xO_~ehw9DW*`89b?mFCPrMCgBIzFB!)`P|oG zqQ6s;x_95xq5Mk>&#W}VP^I#}S<4y=*xrGMm0@@*a`RSG0Ie+2)InCx$1mLH?L#InEX-FMhdR?dR z*;5xTEWPWQ?Q}tuL_wNk{geUCA&C)3na!!nb!&T`E@D8L8R=*m2Z6vH{dKS`lnYMN>b)u_J7msEa|AIt^D*p}XMUTmzbU)N+3 z#ER)d?aE(<{xVGI2-LXhGph0uL_vzqGU4-ZMw!MWzi&%j`@Z3Pz<%z3QDX5GU1I`Bua?3bZQ<0CQJ$prkgGqF&EtxI*>#_AfYMmnU<)U6sv#s#?Q%^q0smi^o zJitz8mNifkF&liYO;Fo+6_=(NCBxyd2Vjx1Xv*U>G);NvWuer=qbc{ojE7GvqNmee z#S18h@g;gLrOA>&FplzEb~*x}IwMe!95WpRi<$E!Ds2fOOKb;~SmjAQ>7j(bzNXLb z-qPBOMsOAN#<6 z3ZL1pFX-9Q+O#GrGQW)WVWvDvI&g6>h9Fal*^wG4?tf_y>qBa4eqtD9sLR~AZue*r z#?AG~cHW249Zmvn6zwcY2W)cGBogFC#;Rfqm`keSxR=2?%8C75isrk`!7@$>;wA|M z0tf`UCP5E68FcKD6P1D0_8i}Ie9!sXiPaQ?Kty%VkqG6TdZp>WL?LC+cAn`}ie7%% z_)_V`QYcZ{T!6Cs>T)G1GN(FCC{)(^FtkG9X=qh0x+|1hF+J@X^D?S(VlcPJ&~bmA z`nFV|TRePCKEebMrbAKvTLeIQ5M?BKc9lPBUABMM`tBTz%@tjv(|KSX?k>UDQr$P$m$dxUy)kY5%NlYpDd-r>fR>Zn8wsW! zpF4Ki?{N>%t*6?V4+)*9487wWNxh%!r@s>V~f_}`$J@Kk<2kAVNl@=D{WLxM`j7p8Lc;+EllK8c8TFjN~ z(e5lcodr3W+@buOh!9}RHK!I?Vx-5V;-Rgz&hgre2#}9=GM&c308@VTz}nizl~qRa zBnVMimb8jI>gyvx2KiP{ro8e2z$FiaglwLY-eQQWtYUG;)7 z=PKD+ppFzGA*!C%Cu6C<6tC4CuIc6IFB>77Yx|D(SHJlAH}3DE2EyClhM{|#Kl7V! z`op)CO#b&RC5t-zyhX}r@3&&a+9I&F2&JFcmgrAvi}mWZ0By^7ees#X##>*z^Hv*O zDeJ!g<@X+aN?H7h8O)&k0L;n{*&k&Zj^ZtJY~F@JHW$A~NO+fyObRkH?RqQ_fyZ!4 zT>PD;>zwP^>EOsB;T#S^X3nF9hnXWo!@~XO)R0!^-1r2taaVo#Cr5 z_zc*FcQXf0|hOq3hoXArGSz!T4Z4)9&>mAH7~V29EN zPo=k%rVSy=Y{_sbZj_PcJ!>zg!^Pf{{c?2Z z-5VZtKe`2WZp7bGvfumSfHSl=zeY%Smd-~e%K4byL|^GmPIhL7r#BIiT5q)FV^w=&xpr@Ebkv#kV%}YO z1cFl%Lc;W|r#gf5lBG>Z#xfOC5n_4!r@e6|QNI4SfLfXx8|rGQt)wtHAt=COC-L+3figQp zANMDdRlS!&R@4 zjFTnuYJ-&3I`Z8a(Je3ZE`p zqne3EAGkCX&4bcMbI}k~OZh+pi=ENAi zQhv)fo`ZsAv)GtDP?r%HbV86I%$O3oaM2T!Zc*BVKk^-5N4==tRg;*2Wn2hI&IAEv z01<>{BuVO45VA z%(%)r^P=chYL_ET1&!mC6}?5VdP)37;MX5ks)Dney^fgt=#WrTXmXAz+AlNtK4mSI zMG~AGmz0|v87Pxw^brRq#?eDKiax?`^EYo=-I+_Uy?@Qzs&x2y&c;n^y7QO=(CWFh znabH?c=B?5_E>wqzjy+(^pGXanAJjVQ%Ff|Lzx9|a&djVGt&LP!KHPLWodv@iW}-( zk!%DLlvd$SLYF#%TWD@&7DrYHAkh&7&j@;EJDJ}jBx20XMCza}8APMYymW{YuMNl4 z>^|eIB?eH(-Rv1}>yq&XE$+=xiM?rzvt@&|q;pMu&2X_%rXwgvTT8pv)>RKVjFfVb zkjV`+W`pp9c z7#9z1+OZ{f_ctS&28LS-0B1E04|T;FBAS+swB%!))igZZ6^jJ4 zVsW#uPAWnK$=o;XNl!+?90Z~WD-e>1gk}+uWwHWcLBK10ywGG>81Z;k7(Ek`eeJLC z=4*e%Yk^H3a56{XB_gl?_z$=>ggd%v-t0F1_usRYMa|b0(EmN6Sp9VbI2d*DLXhF1OY75dVtQ8eaYj(F0*|z zf`Ky#RaGOyRsB_c-JNZ%O$}M*$n?;d)bx;GRqjv~pjvKu2luy-B9^E@JwLg$6)sai z7M>|v9iFI6V2OqeOv_G=cWrjC}?&M0|XfOEyI!=bjOf&_Uh z2}`rk+|UtX&!rxA<5Fr`u{l!QCKw8G7W}P+V?)FF_6k38J@MPWrf_s*%&sIP6!?YS z6y!fpB0eM5ti7exo!NA7x)|ezT4yIf<;>n%{Oy6;XUp&@l3ClI?anaPJF@|&7S|+# zTOmwU43J*gl!U)WZ?Vf0;q&|Hb8cFNEeujuZ^mHFNdx6XQhnA0EU#XX#Lf*HosT*3 zJyeaBp(*sJ%YUjGizJ9=Xz*NrT!sStX~Hyl4v9n{qA(W-{(6wHzs$4;e#2Dsi`VRQ ziQMX=>7kjn?IT|!Kcw^b(@pzex%Rh%uIQSYvC*1kH3Plf9qlI1lv*-^md?&@;91A#SD;9*$@xA zQ`+kaMEJ~=i8Zg@q)zN{@5Ck}jLN7?@d2~QAv@YK9}0rZ_|iyzoizpt@GMfV$DO3J zLAZJh*r6eUddyfnaMIi-0}i?(bUK|~XV1@N<_4x2RkfmWGF7%Di=WDT0`K9M!<0Wn zJ#0`ell1>LQ;ICC-M#y_|Ig+W?6IbZ7x)3pUy-Bx8pB_DeoH|GAREhdH-+b8u7x~y?d1p{@-mjfNoL_2*;`YBLnr!WA0w= z$vG77jq~-9Q`K8+V%6IQSh;pTvIa)jZ38sgm-%?H1M7-0+oTyYqzJl44S06Ge%)Pt zv4K|qyoQOL$NN{`Go2~Viy^k*#&R0bm6M>e%Aw}U!SZd&0oWLM-Ba(*e9(OQ=+2=6 zj5`iJJzcSFtQ_O?rtvlF@;}U5y>hH6hX4ZfFzr*J(1lX}Tb~j?U7r$JpJ)_C=8`@U z!&P};47g&xZg_ijgkB8nz4_%$y*G~M2~S`{L7T08XJ4USlBM*%<_)wro1)g8c)TZb z)m!XVvdUGrbwh4{bvh=En~v3FlpNmFE^zBqHA2EmVkHV^-l0^R9OChfmL-BA7y+nv zA8@5yOax}S0t3^(0o9VQzi=CS#yJb!3%|Pn=F8m*1iE_{{`Qpet;N0%;f1F*J+6En z?s{_56R`EkEl)tJGWq1j#}NX^6CM$w#RVjzqn?cu&GS;PK!^~uQUEocGKzSaA%j+q zHh-izkrqes+bOOtbs4V|AXd=F<5VdOkaiySju35x9LjuAc}jUlGNtVBzTtrk>>qaC zBARx?`)lEQ2w<}qCG!U_?2c8o2Qki%XCxLb@Q$006bV8)V!ac_GfyF8*rM zsZAva@H3);yve@^b_D=<;KMn*l_g}r|7z%nx?m6uO(G957d+unU(6&C3|PP*%$W_5$+9R)I!T7`_oBAWF#hMsz3Kjzc4T0WiUvbU4baOtH1~6wKv!s>S~;o<)x`bsYNWPHX|*<6k$qE zG{#0p(FqHucNL_ub*|pdZc=Y+2fLjvJnfNq)}dCq^zo{XKLMKQ=s)s|OC)niOLk^` zek`m5DJ&^Ex4<9hMJR*IR^avYMIw6pX94{>pV%y>b^yCNHY>8eEOc z@DHp|;qnUnmD_2R%~w8+HpPZXW+|1u<8|_PF=3MM501-NP?s5LjtePESAIXR9ANo? z`lQQjpvADF-}CJL_tIzO7wp;Rb1|0T(*PKP`U34a#AuG7C_eHA=}@&YEjLiDmVopq zh)!EHibpBPiZ;69l9Qqc!Pb=cXagnKfmFC57IU0YKtP&3`48emAl$)cP5?j{(0Y%( zK|O^G{B}tS17s5efK9xFxeACPLZV)z2LKX~5GGLyAR5@U!B)yifJ2nc^#2_qILrnS zWYKC4x3bh!hH#mX7XzPKzW03KlQVnZGydQRRbIP%%uo4JD7eU13A9xxUs;$X%6ugh zt_mHkxf4GZK3aE2X_)d1Ob8EzK$mh}3H94MQdjP`ceoaw4n5QM>cX+GGwrY9Ut#E| zQ2uw}*I0SB5^@oby+^{MET=e&?PNJ=2*!l>mSu+%0V9I7&@gXMuOKAGM}`YwLTGZN z#Evo<_+4|}`;o7NLy>#w8DI7Art&j5y=83;y~re+$(qD-)2GnM?~KesD_s$xAYe1W zL=*x5i2#5ERq<=oZdr@IqP1`Fc>Nn)iA#(hiC00e+axnhq2X!FuU?etEP0dQfkXCO z7-sUOg7EN%K%J4DsJ4=ig8c1`=3We+_>#TBVyUa zknk^Qk?CF7m3x?M@&7wEP%?n@-u3l8JHB*B54i)^Zam&G zaOc`e4DJb!UrV#{Ghsp~Me!^qJtz>92JKp6HF}NqklWYe*|o)nWXdH*`83G7q(OD= zk^C<%e{5@spT1+qncmd$73~E&U0n6b>Ywy_g6EW90jb^f#O%=BG~@8e5J$&KSM|Ei zTntJak7RKnBnH!_MWDHP0Fa>BljJDX6Z|!+1Bn1&38wt?m}Ra(%yuhqpJJu}_Qr^P z5*#Bo2+=AcybcUu+|0}Dw^rHY4Wwm%AOUFmIZ{V&p z4f5JuztZb#cUzB_^qO*94xkZ2{UCnt6`S?Ashi&oO8}L)Zm>zgEUC ze~$&R(Hx*Gij8%Z)g~b%2C&;zc@!H$%mZZ#v6TOwtUbPIXViN5y%9u>|AIPLxe5Mc zRNj8?;zs4n99>2v5g$MO;cxMc?wy`}h$-5y3Z)-YNA!QMBN;Yy!|#pCyXR@3+3n0J z3onOnpII39gqpEBK>0Dxm0)82Jw_lruuEWp%yZs5ngoH zB7Jw|5$NOKpy#`2EjlFL$NMz_wW>H$e9Dnl>^X3~kglYqKs+T%l6EL3AZ|YC>xs)w zj86@b*^#W>fDle+P!1BA&e4-3*<#Ubjf7tyXaKInVKIJJ%#2@eyvbF#wzPZCP)P#O zXX_KPs|My8f-;=BEV+0VB|*{o?ImHUVRczycG;L-(l%95*6Tj{Tf@MJnNr_9!PmKxjquU`dDK;LUY-<0l$7{e)wtvr-p%UR7$!QuI=}lCA zZC$Z?MOOiUMCKL_LQmd?4J+GOu7R$VYgcC~r*Hi8w7(uODqOu{GBa&>a-|K{23It- zvD*ey%UfG&A`wRIRJT|!bfHSrkIuRRYRfD{AcU)o)nwK(zhFoNO{qH!8dVPXcz`60 zY+ntaP1z@W%rya>?Z&L^P-7~!g&2Kii=Fsvw&W|brQIH)&Q$_tA!j~UtqQufI~tq^ z>)Wpzu1L|zazMq(JEp=cWjQI)!Gcc2{&5AZ_Sn4W#^S&r$(T{rI$m1bTb3Gz#h~PK z>0KGhdQptcF*$1j1F@)!NR3O(HARQ`;XrJRPcmUB9N2W=mYULKk00(OXK_-2v#vg| zB5iDJur32YCysn)^K9PAiM~n`#+mg)Q_Hhf{o=e{26!xYsJAuQb^o$;p~Ws&Q3^n8 zc3DM96!S(<&I|W2sh;s6%tYAbxwuNuD6Bk)WZ59AB(Te4GD>#PKt$Oln|yNcTv9H_ zy6539#$Ue!6EqjkF`7WeJt9V*0yuSfuTMUnH8cW}1ttc2>+II*R4J}Jq%7&<;L`SdD)ro##iD z5_pFXI8KOi*RdNYDFu6o`s~&LUuT?wX9wJ zLJ1L|%JQPZtPE3%F(xPw*}$fgSaM~Nr>-^d)$!4*66_8`5FzF(u331#d;es0s_bWU zc4h_$$BEz{A0L?M>fdm4Pv^##^bk>(;9egok-)^_p3>$+mFC2j&fZ_ad!d4kgrrvH zmXFjIm}B%&kk>PJ;MT@FLR)9HmO)}wWl>n|9Y=SM7P3&pE&FbCzH)QU^!Pwc8sLJ| zn~j!eKhlpkCU)f7JLV9j|YGg;r-ZhPwl-hQu z+LD6EmvPC^+I)!3pK52h^9HFq`iFEaC!jP+UPOe6h-VP6Wj-zeCn_4#nHCzunZT>X zQ10rDzrX5eXS82n$KlUk@3-vwo@~87v~u&{FSk%$cijP4_zB4mUwitq6WiZ8(I?yi z^&3yLcO2hPN5D-JlQPe5R%yL*S=cU=q6B`2tsjO4H66u_09PdK1TV)IAu^ry)ncfP zuy7=1VuCFl0xN{A>@;;sjZQF3Hv+s`09ievx7dB0l zRj;TwV^C9Az?WEy8kHvDHlF{o%vBr&L=05DxRD?tVM>-TK-5(cp9s+pjO>Df&`?hl zQ?lm4rQOTV)4hDS8Gg2TUir75eek+Yn$kAbKyM=~wHn0TN+J~#=+LaicmC&t=6&Nu ze!4qly}7g@1^>i-okwPf(+$@>Z{IA32+|@(#r)a#5bUDaOaWJ zEk9_z@AzIueQbSO4xea#WGsK}s_~{QjPuR6#90h}AASyJw#=;GR5)CBY!e-7;PjN?7l`!cWUZ$Kqry)Z(TlRY|6;hXis}VoB;d?;sDlZ!?nC zD8)E(`((CGKX&}3nX%KeMdI@$wscvUizx&{4&QVtj!MXTl^D5Z!>Q3#4{a#Mm}&&a zpm?%58)I|Rs_G*rL^#LV8>eg%+J(|j(cWmSy|JIZy>;#fXnP|S>)_GR3|T*R;-#6< z)2j-_i-NIqSt)OC=uNlWX{0jtUL{IjyWy@O)?Px}d-oVPrr21U8&_2wyPdUnaS;Js z<$n?{U<-P>2_Y-IyO@ZaAjAJG_A-^(^8ij;=HlYw2NdoZp_Ib?w$lTbu{guw*pARY z3-IFN%mrH}Tbrfo&7DEW$Zq%GDq8`oM>r#WSJEmV#1bong;3bQjA^nNTA-t#lcuWEPD?&_k;Mk>iVT=QWk2$dVo&d^+VCGK?W z#MYLzb>%BsvI%Qb+M*&%@Q9aOw8CR`j1vHmNI;zC8cUsonK&qWH9saKfCw!2!pB0f z!NZGcHS2$-lg zU?O3OV0soys$-`N7z@Lyk3x33g zJ4WOfOL5!ss)?%U`EOyOkB>Jy_>~30`k+UHaF86ER@Sw;wsB)@WUk$5W(k!8*aZ^d z`IdBNP9k-Y4<({LIwK`IEhQm{5j$tWwr%VC@-b<|5tohOWhEIHXSa+EcNk{AI0N*r z@Ve`_EGq&itB=b|4F||=omja%1D;8=U~F-A^|r?!ima@yaijnQMJI(uWkj(({0#TJ zzkpoG=_)q@0v8Af(|wEqi@??c5&*IFK!&}ki@j+R%S-d~tmb4(a;nVq)tM66?R>%> z+eYu2TW!d*8UbfCP}x784${oqGbQ`3-`JlIkkPV==i@oN_)DIBt%|LfVgRuMh!78m zuoWW;B5*$kf;?&Jc;9(UHYaBZxHUN~IXyilg^LN2x=t)sgVs%TG<-r^t|S1K@eL$7 zJEhQK4D%NP#PG+1Lj2^|%(C|B^0EV(&TM8w~Xm3jUWyCidZ(da< z5o<+zMn{#T`({QKh6G!6jVrqIY&*2y+i6?9e5l$AkWe;M(U#^&4aIO0LabEh_GO%+{m`2+PRMF-+16KccWQp!J*O-KP8gRyyN_Xt~R;5U^kbz*x24 zv2t^%5!Jkmu9S}fK(xF?^~!bm=jB+;G#y(A&m&~=P3W9VRN>G=Qq`S; z-+g1qKQ^;!>c$akXhiw43Tt$bv<+u`5o2AH!Q!yShUkUhQ1S7AI5{T0v~#w`IbaJH z#Ax?#YaV^-!96YbB-Cv*Mw;rTgcnPgP9wKv9&e{O92 zz*i6;!aZUypma7a2G6))bzCrb zV`dVVa3CO&=}$0-cuG|tv0+Kzi^LazTmeu=g#it+&<&K%Q#zSbO`dVlef+p+S@6g$ zuQ6L34N|eh&@l<}3wo5E4RTBxTNQQvxq+fx`6)AxY=-1bG}FX^aECopCV5TE20DzM zbD-e5>$eUSVJIKlQVLz}>pUV7I}!ABT?`1Nv|Z!#!Q61*>2W*~F~OX>_04T4d3Qzf zwafXY@SK=bIcqk>#xOBh&GxTl^^+oe`YU;NM5yBCQSm^!=MUk4IAd~tiXliR1O~~E z$#fAIEcTWuXYGh1LR51mX25qAer>Sjq)0-BtF660X6$q0Y|;GrRBVAAY>O&Uv)4a<;#wAX%3o;h?0F-mU5&?m?%hoUAxZoZ2HVxp1&A4aq^2>yUsS0lpm_; zxnZW#pQ^uI8xBBZZnMq4)^1;IUv_XH9g~4OUSFFz_r*K=0pincO^So+b+@)=G~BX| zNtsWR&X7X&C%7eStcjsbUHgk}1 z)ca@)i|AfSzGe%fwH79btyaS|TZlDwiH|XSQTc2vS!+~IOc4^5N3W77Ns~Kt*_16*LX$vj%CwD&B4ZtZItzCsk@{9)Z!w<3NqeIYi@v9;Rr&nA=YC!A_NPG zQasCyzc@va$*>aLhgivOK^CH=C|WYRxSW^FqDOh2i&M(iNf7!eZACbWaD@iZmIxFr z(5A#hQ*pSQ1TER|Tu33R-;U7LV; z_*O~|E`ldXlB1P6N#?}>A<2%G?2OvHSQ3)e;z&p=YRoLnYAT9{cw2o2UDwLuAIKL$ zkF6+VagMu2hd?}=PxLg^??0uMB+(N3FFNIcz?kWQ(8%I%geH16CL;$bLtSVk+Kq-? z%XV)nD|KW`e*PsoETfepUG<@ePHPNcNoYoXNJM_(Gyr0${8%11e}7IyzJBfWWO{mbdU|SlYHENko^_NRpq_hoD=+D? zMPXX$j6=B$LRe}-Ql>Ew6A(h_&n)^=d34F^FjGP@`?;6@Ow2R}slXfn(u!A62E;;R zQvveZrwX!*XFGBM%+{1N$fZ|BktZCKLt$kzwrs}=R+ig3X>->P;oUiAnh- zu~`O3UUE`FSxmOUo|~k-NzN~eA)WLU1B%gJA%Jb@3|T|wk%SDc2m-#*0IR}7`9cdf zaYeuB+3VlKFmejVDZAld1RSQYS?DxLVqY_VJrM(8-(&D1;Oj*SCyZ=`%U}Y6T|Wcm z;nUFg9GO>kM<~1CG)2U2!KS`+$#z{}P{|tUD@nplE&wh3#UgLfWZpmTWv5B4wLE=j&22HkHNTo6W$KGsY z|8m)gy>!K1x!t{-GydKOU=F&x5UE9K#_n?O-)@{uFrQ`g7a%#rLW)e=R3A1Bt9m;szQ?yr(0Hqrzk;W+g= zwxJeRqc_);Kga>VyxS%NK@@oM`blr1t6JVb?uA)_TWtmT^oIR(D-9uHNv>%AmXIK} zOU+E1WtM4J1qA5fT$|HrssFjl1&Bu7XAPe(l%aevf3 zQ2{*%o|qmuJd`0`BoTRS_KNAo9N7Q^QOeVi&t39E zV5*9scocWw57dBLtblBoVB4EFm^uz}8qd=*U=W)1Ao&4{@16AbCjOhG3sD|MvRvys3_00ZGz+RZ zm8B6%P4x_cX7=U%R~Motn($@FS0QOuj_CtR*A0}ccL-ZH@4T?i8hkPGGciv`Jz|_W z`R@LTZDlF8RrZmiD{8`Fca(A@2DanG=HpwMF@y)*lK^<+?k{g->u=k;cl7{-1>O@H z4&YpWM>k!00=kdNkd5@5%tTdafbR4_m*3E`p2o7gU_r?9++e8%fN4<^oG0d%!E5Jg z)`GJ|N*?hIVn*#~NyUIYipWw%!;H%@P#O<6f*fg0Fcp}>1s%qr=H$4nq)4$v%4nQ% zQdol^OX&^t=Q8^9W=z5?DJcb1XCSkm$tLzwImzg*kYFFHvtqFKuC}+8tnae{WYmup z#uTK*nCw+?9nlpI3m7c<(e8$l_5B5ai~3jF+e_B<+vqp+CyuV90FY{T#dStk7OOvR zQ>i(6g{}d+_!X3jijZzT-yDW;p2v_+2?<@GK11Lc7MAj!zE|fE|9Je?frMaExY8bB zUs>4Wm*2Om$2DY+6h!}JhaMaV))P}})uE!%=6s!qvEFFREKUg(B88@%=SFBm1zZ67 zpPx0}veregW7&q1jO8!fyC-v5$%>8w0hgNxS8cD(2rtSvVUk_fnTU{p0_cCfD87dD zD3P|ggw1HNJvTKbNJMl-lRZ}(gooPV^e5{RC_OPKA&jD9O4yE&Yb;>`-$ae zKa(cJWK^c76(YGwA}&Db$7B68w4kBRY!yaDQu@jrsfJ6pv^PKoNAz!-FME z#~g_4Z8ugk4NkMzX$^-riW7Xv6-t?CqmP6&Po?uiAOIw&B>m zbsd@XsdeS{{Z;wOC7T`Nu;grNiH7jx>=@;DkYp>PeC=C{|De*sGbk5cUp$9@TXHwD zz_x!Q2!?lH7ee%z%WwF%On#&A+bR4_;wu#Xjl~D>?>z8vA8_~s96pT0|Bk|QljjHY z2mNk#@!!H$@j3e4TZ_l=?>*n?JoRiq0(}nV-~FC~KaYvvsqGT2Z3F-6i;I`MUwzB- z)hbWxw675Nl6{}jmOr8lR82?Pk(C(2=8+C9%W-8DB4Fsn(jICN%P?g_S6ps*~l*LTpD37$3o|hQ+CvtadNG3PV|4|YDI1eIO z{%!G(ABIbeTU@+b`MdBsTgMb$M<_h7Flh$>)0_aQa1W7brQ@t2jH7;@RT@I81P zSG$`|pTb|o-?;I#yO!1Z6h1=!Mp6*rQWhY@!$#2{D{Bi4;mj{{V*l>Kw~ZtdA7N5w z8A76t*6c@X7J1DE#yFY?3(@{Q&cV*t7XEe1{ukpQ2O5Ug3;oowZ_C{X5tN61BJ3eQ zL+Qwg`jFoXUA1L7F#_=(1y)rpAS^sfAgUlIgTB^# zUKg;}xU1^$nraMLb$yxX0}a^#wX61)Wx5Wnsv?Mvv+9-P_NP`)JeH0m5yGNz~=cQ{xI4(?UB@QuHHVlyDPDTx_jkE zr-Qb`wi6wTMF@LO-y`oU|iaKlARDP_DLeURP zW67$7qV#CUPK?WlPA`mmGQKF?kP#njj0}jOkIG^~QCdtuq%k-ylcw>~isK^uW6}!a z5TIS?l29!;Q8*pb0L)foG7E23|IZ-XIJ5H;C3IVRkFpRCT~8=^@i<|!-udG=<-3o- zgRt&}(%t74&z~rmIKRlKQ3uisD}}RYB}zax^oonpACRdA>_PV)6#P7bS3D%oy+@^$ z14tIyg(oO{5{3V|2LGh{dJg|}75;jadYwl|D5QHxM1gcB0(69({9ps{)ce57wX! zXfxW54xyXSQFJ@H6P-r)vRsz;-hKMy9mj9K_2?}(9zL{V>z0jk>(;KCT|P58F+SGY z-PY1rS4$1#8K#6FJ&vN?p-Y(Q_@bg1D?|5lk;USTmM{^kB$=pxIW1z!XHk;a%CdT3 z9VTG0L9*Flp#QRTg;w^Pil{(g8BRwyE#N8E=^yd=9j@BWWs$PHM0PmB=^yz1{ti?X z+v`LEwGMl&cymNrQUn+xA`&r#CtAao6+2~`H6sS=bQxw-42HkSNilIjI2RWcWIN>8 z{Doq3dCdAy{DsivF(pY0Hzqt5wmgEAhn`C;3sr(bV~Chxa2c#2cZO7l+!5k_BjiNL zbTEGic7Gw{MDTa)`iNj1#vze00Xa%bS$ugl{mUqeFC({>C6rc`#+PQ&f3eA42i1j@ z_IiNoqHW+GnH+6M@C%|;AR$Z!<$t#)=R;nSll?bwz>pMgz(- zdxngirN72TtKsaJl25`TVq3=o+vo%rL`6o> zpFt$jV2ZR6TcnBo;VInae%}2V{E`$9>K~PzYB1NWa1~EBWx|(ndbBwyG}`DFZ;I5t z`|i7I1py0!)V+|ukWapTIfe$Hha@^mTrxPbY2ka7@S#?v@7CroJ^5qi)1ZOo(Z`@V>v}^C?$p#C? znN3r3yUODWHceGwTt2bA0CU0G?X|0VXi* z&8$&@T4Phgn7eZf)vr3hbNPcs^0_gZsaf5hjfC>ZysRWaw&zxN1l=PS^{%Zf9IsC! zB&({;^afnF1@vJVdG`1`J3wm%(90-O0pd&gOY-}gUB=@GVb~)4QmB;vjU<#xM=do< z@1+$$^Z~%Ob!W8cG;2)@llu*CnF3K!Y{>Y>1(v^1jeDuTHP|( z3|@C&huUKy`|P=&UI537Ka2>?N>3ozn4T3BZ0c#qCODQJ9)7|vUwi>Ty8a**f?^C& z(E%7na9~PVPEuZSgjkL+`a11vqjVRo&yUupmwo#x(v>PSzHswvUxJ3alw$`^eopx1 zefNR13<$!op0@OMpD(e8iidl?B_{mBoqv4crdPj02EP246XNfb59z1RDsF_~v%Wh`jCb~d#jAo8LZICO96K+#&~4wilSi%hH& zjv*QOv75jAK)}$9uz)wIVUAM-G(!Ske2{!4+j+F&fA4Gf?(@nsiUkhLfof^T=eK z&U3G)mf)6HeJ%#J8oW-~lkdU}^|x-QmPop@D?5|g&9K;e@PV%9tN)WT*6xs{7+Yh; z;%_qtnr*U_T;6W|wQvk@O4F9!!j6jU&>&k)Pxi>cZZqI8D&3Ba6SiuXAXjb5DGE|#fAn`uz+Mq8-Kk zop9G(!}o4)57g-mwOjh=;F4u`zByNQ-B*rxl7B2b2xY799CEcCSz9e2#03pulD5R3 z$D$G>h(wq~BJyZoHI?g9D;8zu!aXLXd)?ot#z%|`pZhYYh;#2Q#kn|0x%rfE?2Pgp z#lD@{$0&9^zd_OCcsr>@?_RyCRjl*wZx6ikWl|aE-s{9!@OLGUAw136JG1y_p@GhO zCiCS*m`DUmbJ~1Mh->V8<~=k#J%!IX_5E8i=gfq2MIN>Dd!)&aGgoih=>>(X5a+Ol$h&3S-6n3_l$50_^ve~*RFoy@DU2D7|^Z>{N_`^w!z1Q$)* z*atIP2&vnAN9#b{(akM{BxU^hc7Ce_LP8}QX*L!j5P@v+-YLlqc}N@MkK87?`RM^A zs{iSCpB{qW4LuEiQ-XwJZZ{?f&}xdaS2*gyiIC{YfhGXP+~=!;_|6+2TbUaLp^zlS zD_=YTrSS_tkAwU>l&=X#&o9iMJ4aerTW1&lBxKUI8h8!_gwZCR%EOyRst#IHBg`R! zj$c0N5gj%4AldlRje+eu?in1sdCZ1COCk%}>_vk$Svuj^!tVx-&z4{)o;f-Iw%$q; zfTen{fX9QdJR7ve0CpRtpM+H-4xmBe8Jw{2&@f)1tcHDXUj28^xaQxTA3qLn{L%dt zxc;kHx=Xo@4$seTgwyW7xDQgyQwWh?Qp}!ve=)+$ECfT|)fYl^hD35hZ|>Xh$W!nB z2@hf0<;~=R`&PVbp^?|ni;&R6_6O^=j4{H{On|i!)Lw^%@nKFj0YeG2=Qls7T)68q z?>+=wcRoeq%8t@uJwmp!N!mzhVgcP7G02AEU9nnn2SU8NA(K)-hE8L@h?-aRDgr2& zf(MSiy1jMBaHYvl*71k7o%BH`KQBU1d|_Kr&)z}1zg`SVpuhX}F0=V#^V+YT8z(ro zYt3~xRM)Nf`uPch^SajVzOhD^F+A8=X~v{w$6XDz8N)-}PAev@yG}MDj2aj1LI-bm zE{jeg{LUa|rE0iF7t+(y(|8voNjAtla8^@c%f30xJl!adxbwg(hkl$`p19@FmFWSx zknvMr+PUS?*);#>b%FNTp8lJrDg*pN%6!r+bPTf2_0 ztHSeOs_4tl80##SNMyRBwAA1_u(CoRS&bvJYi+&v>}bN5AiaLPD08Bx4Dt(VvW))W zMO~Xa2ljNO0?r#c)CYyF1u+1j8MXPz1?ISj5IMZ0Z%cdmcq6|%Id}M`VJPjgMFVrQ zMF_Z5{{-Wu_+BR%5D=e(hHoY9X58D9nP*ldP zu)kaYFMQQ~@QL5P3|V7yq~r2tPF0RmJPV9&u|D{Th%ih7!Hi;I;JCsF?8Wz zBRP=rl+t^TMVY_X@Sp%yC!wD>D9tKcc#F3FU1CSD_$P|}es+$rvq3Krj-7xna!L;Q zNP_`5a#{%`Y0 z`HJAw-{FaL>cKZhFC34=m))nLNqhLIQ-{dhnf(aEkA$BJ!7@gnD3i(ynF*S_!1qCv zjmZnDXk>CEh#t`>SaNhPFDw{u$$_J|x~#k>ee&GpOiAfp85W$JK*8?Iah1-$DbifGm z7|S3!%r^YLmmX5q#pAqq^5m9>FM7^D!B4oK>pKk$Qk7jbTB%8|kW8LfcrgZkl-ku` zlZ2?8DvM*JAwS?!K;PbT1NHkRiX^dYU^ZVlhG&kzT?th!1GY4Ft1gZ&=`Do8qZ4@; zXZP%At=_q2)T;cB&A~CcpTcSFm9%ziB$J33n)RR8C`IhAYzm5Si)9=-k<5#W4zj~Q{v_4A|W9{vEr7QDsJ#KhF`H3M0pnT;m?ZbD@Ixx=d+tCVr zBN(i8BSm>#WhsEI@d%?A7ym?lC)|k=Q311%C5LmFS?0PTpXTsn6u`6_J{RPZ=R^I+ z&mWpjEIr=Uv8yvxl3RA39UMJ2WYPOYZhPs2yOj5OZl>)D%4jVu8mY^Y#PFO(;m&(= zM{io@sKVe}byLgW$#vxzx9xdgp!fKnpBw}C_i#_GDR0$48Q`?K5nFaom6ffW-ykGh zFC6o%a5iF$-(z%p*4^+9zT}=Tk{Kc9@^2X3Cg>y`-!Qt)Ovr&+sAZ$Ryf7mnz+>6a z))woM#o{^Bh`I#5-3#~HIaKS05J;>QomuTeY#5CAv&kZoZjKeB-4dGHMWTKn}-MX z_ori^3$U|vq|p(PzPqD)cV7m^>785L=d$baVgcfc+VZk%bD{xaZ7sOc>dcCX&9BQ; z|3w&mb@4ysvT(mPgYluv#>uvoha1vt)?8o9V_LMXf_dcr{Zk1gM>;xpb(~9UI*=1)3`0GuO<^=5EuVOMu-VTa*An<3)U#+HFM6Y!b77j zw{vrA({y>H7_7_f+T7YWQyL|mAviqCnqF#&6e0+Y%t}iyF-M3{S9d+j5?nH}qxu$4 z!eE*xxGFTEh8g-(itAGzr;bPTCpAo`Ab@&=@OMb$5}GM9n%RA3_J;3J#H*Tar z@Ks9fBcxkY?NFqq9b$?CuXe~F%M_-xZ&jgfsy!c0;M($v{*dAa-`FTVOMUdBD52{t z!Q~EslCmGdo17#2ovfp^qY={th876>ZGc2vojHHfZGl|85opWHSDNb*<=AoqMIG${WBC&ibg(+AByB ziswE9mV~%aP7zr>b~~REZPnTcohUDfplu-$fx6vK38LuN zx&P@^U(9Qd7l=MKCpFQL9!umhl6VlW-D1js^t#bP=gMa0Y1upHDxv8dE0pzFsmTfD zdGUsHdqN5Gk7xd{mYOJMbL`5J7*Z{TLxBoCY?bp-To4 zV=YOGQA{9Fc+H!iQWJX$2LKf3QeG*OerJ><=i|->D#L?D* zIGY1w$G}=g!DMSTfw-*FgajHb zQPJ6{F}NkAI6F1iI8s_USQ-ZqWA84tROVZQV|#3+%Sz~**rHBlmC+g#7?h9|XH1I; z3{J>|%{lo&acMDz)abzAxHPuYEsG!UF;nM0F*cadQo6q-iTllHwA5sEI2bL}x)=p( zw?3F@?#?UjFJUuJ*I)fqd9)z|RL@e5lN7pB$SnEdZ z&eiQ%07c6-JBwC7u%Q(8E<$=zWn5`ad~8-}LU9KID8xs|OC;rrFDjq*``vdL@fzj@ zKyabp6k4T?6vBYg>4}#b8>YjEXpXM5wUcf2 z70GO+@d#;;`-CL%L)@z!5IJ2?i38h31yP1%uz2BU3k2SOa+O!1u;Vsr2gUyV`_6V- zn$58pA__~y{Z5;8Y;>T?Oh{^JONLbAxsNU!KVCb6FrU^p5x>47D5X=o0Q=@jVEUz2XHd zp>P)O!8429!JlKem&_=A?iP;mj2*eIEIp6)Wd{^t2b7j{nIByU8Dm0&JqbG`>MZ4x zX4!fr%`%L(Ui>m#Xg|8LM3VXrJ=}j8z&6!cBuTOM)@34h%6hd$_%Q9 zp)V+LhooSd3*0~2KO`t7xV*R25;&^t{xXhSrwdQaO$jxH1{v~&2FPw1w<(LT=+0GA zBJ;BnF(&w&r~{nWF_i}m?$475*lVmTO2t6nE>VXN=2>V5sb8UmTi9)BghKOy_=NC& zs}EbbZ2iRr*gt;%T(MrtZJ8{#Rj2BCOn+!7c{VIm6om*H)8FCv{paegpLFSEW5sCs zFYr$Hjp8$&V1CzZw=2fbIJlzl79PwW6H{7InQ%iPl~KuQ97p+_iZN$02Gw^>5-)gk z>V90)>l=&9(*J3RT-8uS?T%pp6ApTkXXd>fC;Z5LAMF1ho-pxtrq()&}K6AJQm%=MKl^BBKthx8_$i9{i5<*BK$M#hmaWWM(2dYxeL#J_?L4C z(|FL+LZ|RYRD-(d_%ziOr(^CabcN%w8Q8ke>;c+FSN%2k4Mql;F-ZBhwX$qE67~w5&XMc68+C zK9gU$4gx^=Y{^J`jw^S##U>FsCI?C>pG?U}5O4mqscIH?5y; z$%e$Z8!RRO$LQYU_qFHrJapHd;e3p1H=kLi9LpWe4%r7~IsXC(%WR<6&*m`QM>dT& zy^s3C=b}pHf0q{{dN_UBp-+qul~pd5vmnBKv9I=y|7!Q(iRA@4DW`4LX>phWiQIPm zgJb%|=G;=$P4Z8<|IyV$aOK*QLv?e5 z)rOe5&VlUFdpA@N>{@%g7xD+|(%E$|6ZZUp>NI>ZwPCilV$_)o*qX#MFsD%8+iBVl z8}nh5=-k^(;T}!Bnyblk&R@}^IY2dce&^!DAx+mkx1(nBiotZ{p8h=@DYCxj@Y6HS z9i_DygZa)iU4^n7QZ!t>Jq~Iz);qg5wq*E`*Vnyzw4LD8%Dw_+gMZW9@t)-mZmPgw zj6TUSMrO9Hs&KAs$-;0BHst36uAIGrMMqE5_=8yC9Bt>ME5Kq7A%JZ_5N`HdRTqZ< zBg_N75e9+Nbk7y~6@=xzy{ecNv``oxuUsdx7^)m|WNKOplRC{Bi|gB8a`nK#lc^oq zwmxT?Bqck0vPWaUkkpyw9Cn&@`!4PatXY5G%90hsy;kKF=i0V(L9E|=W}tL!(R@st z$=R1*RAUJSI1Z-7cz_(bINibcuM_9!VBom?YOC^`E~{l#?cnXp?EpptJ?$_`5OKbeGX=?7pdx+XGaZXa-6GB1`q&=fV4c1*gODf3yM0Z~x}O{LtX2ov(a& z>gn$c9^U%WEz80}0wX(bTYpdAck$h)@LnJrUijOMYi|G7{7QVc+YQit?A3MaCtf%{ zEPxX8|5t4VG{)@j5~3}goX6m~QpZFWVUi9cQN8w`D#Kg6M(&0d0k~p=Eks$8H(i0d z3WR2jVCPjNH4KI5c>oX(3)KSV)Rky*a4$*xcja$o$!!CZ_L1uwqxGBSH%kFoZDlz< zEj4jB&wuABdd4LI}_o z5E4G1a}dmA0U5kTFNuc>)6p;2;*iz6wz@cert)F;g!$XZi@&_}!mZymey2ofi4$Vt zl-8185y!%ph~pI0o;|C)f&jhAV`W@){I>ImHN?Z$RPBNlF&1crpB5^m2T_y;SzN8t zH(1K(vhcC|uYiND5V}^9LOsK34)N!aW%ANPoQL;m5zexW*ngfO`qM+)vCbaqyqh25 za>lof-%M2g5Ch>kmvZ3t7-cT;7kFsFbF4-L95zK^s91iu`SPikZeyWhx4lGnC12TC zHeq&4KLQz5Fa83CXaod?7h#C`5UVho*Qhzldrb%83-F zOs(r8^s9LKRhUUitAfR!QaQobrsBrlKD%7Z0IE2k?0 z#qjbNt-M|e*2*71_n<^xo~V^4ia*n8pN-{T;}{?8&06WX*ePB*rIq$d-}aWmmvu9& zl+5tbt#oH*37xdG&a*QcS#Q_UZ!*RA=-UQQZy(fJtCrftpQ@#-w>P0EULK{DXG*g6 z^}Tyzd1>sGQZ|xm>1-4)#h%g;gvk5iS7_-io|?ZxXej{kmI@={mprA2)%+|%>U*>J zSN2{V-^Y-^zZ10YYQ-;WSZAHQ6!TKP7j1m>JJr#@iH|<0qd!4;V;gUAm#0nlX>+fY z{zNThZ91UMy;d$s+Sm8)RL4>)E$4N2sdXRsop}}e&$O>+Lwslc7+u}H;#Xm=GwQ~0L zu;=SU?Q8YKI4XUpohs|m05q#~q}gY`b&4-)YkIv=$45n{VHl8GGz@3aIGk27oc6(B zl=K>gv!;HIp`R90K2zxsFwb6{jA~s~$sh>^AlM%e3E;O{A&L2dVxC)}MHaCtC~z*V z30e_KN~9krgoZ|23_9k&Z(xF}mzA(^MJ4gLRWq;ZZ-`0N#a2Ob#ugYNEqz&CBN%AP zzw`_x^WrP`tDBsAMmW32`p;(uLK1i(yJUT=Mg0S1*D#S1j%E*5WNagwy{ z!9pjhL%pci)m0mdu}m9|S;I*)5(Of75|Zg;qMP<6?`+^VvwFB8-<~wow>H=JHuO^e z%Zx0VqgZFWW?w9JuPs=#89wUj;Um7QaxFP@jSid3TdV@c4UbON?i|b)g#7juo3AVX zTA*Ys?#j>XsVnfC*Du?DcXRix%M10rdI-cy^u!~@)_WYg*SG7$sJz;%2PmhjuGnD6 ziFUSP$m-nGSiiQb*c41>x!oQI5Rp-p4c$9Ct$_1JZx~SYj`jA?qj0?=TPaKv-{*BI%vrwALEYyJ$6Du!)~zmvwLND5KUU|<&iiN579i;KP-7vJSpb(gpC zT@KnV_nZZ-;%Bw9V7)=l_o&{Msqt(r8iuo|kMDBOcDeVg@zXGzO&s7D27E9WH4OKo zs->Kmb{_pe!*IX9c3SZq?j6E;e(O<{t1<)#4h4Z_A=gn@9hyY4%&$D+A`u=GnefKs zJ)Q-qNnj$9!D0vv^~!L`vgsOo%j^68s{8pNd?Bv?8e1nmw<6j7n465mxd*#&J3jAT zxbN!Ckk;rdiV`oJRrsw2Yn4eMlhO$ra(6os2~IJ%F}N_Puy)EM=1Uj2Q64|ba z9%c?*WmQ~Q@v*(a|M{r7QJY(%Hn;bq;Hy@vbMbfLYPpQUWO-IA{P8NO6qF{RP5do| zxm#V)|Fd{mT}PKym`bHmSl~AfWUG5?pYmIEZ+Y`@R4XoFMnV*(O@+x-ey76xPK8;9 z5c#3ZU~)YC#^N4usO@q1tjBw}MBc9PE%&J~_j&Os#pH*)Jvmx?-c##(&x=PXA@B3{ zw5u?8E5A@-esQHeLbDH^S@iwIyVbtmeWg9(xFjGio>}O8SDSK& z9IR%|u}32}X>CMjw9m>FoAX^3H^}Fz*E3hk zednry&cj|lSB2_4tl;y2edb||!b}RC90P}0e0J$vX)USLT5=z%fKIg~oxUxRbXrU9 z`-m8hi6O=D_>K`>uVYo zvxdb3vtuczp!H^4>&<dhKFmjD%jieN*jye!4`PNGZHzN*NttN9B<1Nyhujd>1=RZ%nHI+ zie2qh179iNvr?!9LyVK2N8UEg!jlw^;l7XIR<)0Hn)dMl^f%~Jar7}9nx^Hls%c>` zEc%k+@V|mX`jduZJ=z47Dvrt}I3CutHR}WZsm}F3m*_#A8kVzY7aUcw99@FtpBk34 z5!JjM)joaMr(rmUHo|FDlX}`0gJjn*obzvI7;w86gV@MN)S*&|-}vfcH4N`@4r@S# z*^e~c5dySI*(1zT9xEHs%LZaYFe0G<-29u&b5jzGFhO(tBE527L5Ag~R14W#1QQ+@ ztmdZ7M%iYgC64E&Bthhw0?yL7tocU+D#xyCipeoYFO_NvEF(uptbR{C^{^afAFgTG zI#3+&6wPX8?KrV(BqlU1rzpXfZ*h!R^!+zHHbj27U@qG-R)Mi&Y?q7tfW^8{jBdaE_ zs|nSwonIs2_*&=8-9EG7RSM5 zv>2_W8iq;@!+of8DgDtf{6oWVU#!G2NL)+$9P^Cg9MWD-8zK5CYvNz2w0n;0@(-!~ zc#!Mz2}E%mZ!bP52Fqi#e7{=$CAf>?AQZ=4YAgB|chXk;n8MuXIk`t8IEDz0;rpJ< z1lR)SIE{Q)d3bS$sw>*TFnl@~H4;;!;_&XDex}m6pQ%`Q-V1S&LfofXE$p0+vD-4$ zQk%HIN;Rv6jhEZBawMf{U*Ege!Al)lX|8e_~4$^ zzCK6Ccpop{r{~mgZLb?p8;KXI6s^AfcOhq`mm?o7qxR; zEBzTSbfRxV9d&uccJ2{9Zb1>Wa|Hr+Mv9sEM}XKHjP#NWvzsY32WJ zRr%4ij0~mC)Q? z+P5EQrT4q9DwQl+>3vEuFD>S!JaY>#J?AO?A}htLRNH;u(RSbZ%NG8v#b@VQwbHYA z3oqT`^X&&(>HUA!TE*{g?cjM@OzajoAH9!+WSt~WY8vkUvtC=v>2lM@sjDt8&Gqsz zsvS=X7_HY9tzY;3#dkb;J{LYy4|-;5m)2{o)T))9jrES0Rw{m=mEJEc`BuqfnPPY; zmq6&7Z>ti>w^cgKbF%Q-A5wSbgWAsYQnUdnRHJwgpr=qWFHhFWC&iaEinl&X({yU| z(qAh*TeysuF5@%0nsbXs>5P`R>{s-U2kIc$0d1Q_J5qtF4{N$%g5>INQ*|_ zf2j56{v==O&vM3mn~&uqszz?vlCi8;3R#91TK=dyA4SSf)cN>{Iv+e&3*Xg`saiD0 z5=uIy*jX+YwVdIwDL+$j{LBZ3FU%}zQMRiv+pi6S?q59XcBmRFRa?i;s~dS+o=~a! zkCwEhS1Dk*TUfc;mU-nzYCS*l!LeJia2US!C)BmCojL2o?`UVv`eK!KYo*1KRVzK~ z(rCA*R1D&npHwBU&mi9ui{~Wat^bUNJ{I_%mL|o{GM2DXwe6vN9I-ZzDhA1-VYu%F zUrNVwi*Wo;d+=}bj*V6-rD~=3uJc_9V@eClOTtUJ++T;ZZ!Fq3_g(fKtMf{p#!q@$ zJfi$S9qk|Z@|+yQGad|B!=RnQ+1e@m0J^C0A6hw|jSHUHDAZ`T_O(?jJzK6(BCT{* z!_7RN_px%k&s#3~NfxdALCU>oXFSU~q8#N}N3zkV%P#~F_K3;q&9IV)S+=OFrAIC( z$Ed{~IeA=z!Ypu86_q0+Epf6g=F`f`>kjx8b*wBmWF|)~m6bD7x^EiGxkg@-+HDgJ zks+3BTbwWLu=Sh8%2l`ae>B$#9TA?Rgs+h2RBDl{QV~SK3ck-zR#FJz~28B`RcP+c8-7Z@=Ue-drQi9Q4Py}jyFHTgZHl- zZ#u=>$KRK!_1(|k2QqIR{{96O{*V0q`&4o);O%~m_v)$dzsz!^*MVj2rpUV;Q+8FtB2GXoJ{5ey@x4@sX*)zi;yvCn%k*1gD&v{7m zO%&gz&-whhi)9GovG`~RpT!y%;OrQV6pG(oF^5OiH`UZ9fxLMjmVP51T z`DBQ|=%VVy)k$03A?>E6uXsu$d_Cgq%A}CUzx}+YG*0sX^UM+Xv9wy7qbL3UqRNeb zQS07G>(1b__>N~5FMVVdSwNg{vrvj9bPQ?M0-=*WckyRQnGouE`4;~CG#eG6j6N^( zl+)+aynIMoH&No3wKmO$YQA{h_VChoJf(luO1*XS-gbHVD-2+>s+EVa@&Q$v9lX1$%`bh zV%TY4-CrU}*=^(5&9|+0O7g^si$fES?yko#ko1P-uHyAW<*=h-z?l_hY+inNVEpuI zJI1wJ?;k5&-kOae**RQV+Fh0rVr*M|WN7Nt@&XK1n;)2>A$XYp@#Vu;?aWWPGn{mU zj`DA^_%~`CAC}+;S9Z4?#O$b==Xaet%5^FZSbSg1ss#@(iUz`1>qC$K&`Sa7) zq)J8f`5f1re_ySm;VR9!FXg$Ka$QY%7|#27`SaJLMBERI!<(;3nL6q7LH;~{6=l-A zmH8g6L7Gnse{SH<4(3P8^@e}&@E&^2kKyVazW4#%;c@)A8&PnN2b$1{a+#k45=6GT z`7QE*)_d1-VvF>Vi#8MDV+;X)l8AC4S7ujjY8NSLjziWZQGz%!f1;z<|4IGOp>rLZ z&P}EIiTa#|@ol%X_ujhPE|_|q)P8&&2Gv#!F46aYRU3)wr+oT>EkY6rlyq1^dN!JHmOg)9 z($i;wKkr-oyZkHh0g@iCQ>9DD zV{c9qHge8@bGXcRU+j>pu>~S|{f`Tqp;uiYy=wjJKFr&sSPB!R!bGDizDBY<-l|Hj za0j=ce2=vNzV_3T>P($fF?1*;LaESBA%5?H`1d&#;+z(pRH8*Eafm;C6k;od(4r(5 z1o%Q+gI7fNQJVP`PDx(67Vo-vfj%qz|4UguQ$cUM#Cr5OM3KJlL} zBJ2|*QB;c)Z_{3+S(n(!!Pdh6Pds|t=(=Ae(J_GDEX9x@&D2GUY) zXZyVeLrgTsz+y<$ohbcD+llLubE%DA+iMSLdu=@|@nF>UbCiZ<7F8~_uWMMotYPu# z$)}9o`s9d|af)FD<6OX#iKCW68`uhf2!y=lay+&`xLU&ykF_G$6Kkb?!=>#APYu5= zV)jH_#A|40JUowh`-O_3+~aM6uXvkuQWzs|<7E#9H)|uPZRA`d_ZR)4I@+^K{YABN zq6JHwz+*R}GGH4JA^Dm1DX z8hu)kFWsYIIP<>5F-Te)cIb$P;WV;Ck&2l~FaLu>XjFo?V}&k9!*Ytw zw_U|z_i0C_^hXWLsXzXcLujj?&w&<&jI5q0WKieAHyT-;7cKgc!LsN_Q0GVECOj=` zmw%$Q>sx27Z$_#2!&}Fmg|x=4ELZ~*rj6F<%1zz`l*KD9$hiV zP^@7n)3s%|Rp7t4A?L_4V-Aa8yen42R}j zk;HK%d2xuB`0lf+yU*g01hn19afth+&u|=k_x&}Rk7=||D}JnDID=xLQN_^c(~4~A zUJb*UU#at|wPC%EXc$hTEGSYj6#2BFM0!KRaQXw^eWQ)2E=R+13MGJD#bRGFqJPq` zoO=IXDwcmS2z5TSh)jhf%B#u53lF?CF^ZOxxi&&3u;lzf6mLXTE0R#%k?Fk z58>t1Yn6?GkN4_?a-U%1Yqm>;d1Ube73PB_(da*5c+|H33RbE+Xyp>`=YuL926Nh% z@-rOb7x}#Bt832z>(%|d{t6E9I!^h!JZtaw8s%48v046`hT#m#gi#g4=#p04qhUDn zQDwdWd?f9dH<0e)VKLC!j0JQ0&%w2M8>js(=hE~^RZ{-q zM?KfjC~fKcT$(d?z~8Zy)5}9;5GjId3(fzF&{7_z8cO!K?lG57es^pni#$?;QJ9g}GzV z*B?#ABmR}o;AzcE-d7)@;t{uS-PRe8Zfnn#FHPE))r;R)s=3g<{0je)&)XSI@1S9L zi(ydP_tmSY7{|b=*clb(&ZSgL!_%eVc>uk=^yJc7@}Y`{!5mvc>1LI);$Ox0H5_cs z`}*Olb0KaK?o`Kt!L0MxpC}dmE1$bE_1kkkxS4FAXWC0mHh4muSe#aIPcN}Qy{X}N zOn4W%)IN0iN-2CkhgFzIm(Hh#r&Gi80D1!^R6G;Dc=$RUR$_*#L|cs zp{HCyF`y^epwkNgIzlbeG1M|`mia~fL_||ak}4^6(sOlkLkrUBU|78l!syAfl>YI+ z6pVdwB}q^^l0<2f&j%k1J4lV(`MFl=5EGtlQX|^fMYvkNI+j&mO}u0Srl{sV)M|Dx zG2_+mJQVJ{XS9ADy;xq7u+CE6v+6)w*HK&EKaBs3n~#0<^6$mKxPoTe;NgOS_%&_4 z*Z14>!Z$AGY^tflIKO+%_Up?cYi`=JwlfRk@@f!hR;GpXY0LUr%Tj?XtAqd4bzsuC z^*GPek)`~Rk13DDMpk$ij^GB6P*wCH4=u(GURr#}LyOClb6iJbfgkWw;A=P($$XY% z)uMPtcv2`;HO{mg{@}4`(DxVlYRpjI|B%0T(Dx%8{x9nL_dGTW`d*90VDHgQ{QX(_ z{xcl@Z|eKw9$gWA?<+-q4X3D5TJlqT zzw*5(C~P`7StQasB+hiX7mwU#yftdx0O`uHDsW`?*XK!6RBlZgF&)G>o!(8b>|j4j zAbM)wKBYss6%4rz`5D#OF_^VDi)t5);)PFcanyflu}L;8ZLtlAL2L1;-)8=E?Cb`I zPCt74ODmPi*S==_s%hTzzVhPa2ems!%Jp(g#b}w(`2mpPRcEHxK0H?pLg%S3eaZbp z_qTCc>$pmwGN4 z=R3%d(LGWK2!5(0!%t7an`fwdB?4wG5o2i|B9Pr>lRyAy)`KhoJ7cEQEWVREHP3kX<@m#{-Zv>k&l=p{@ru#A5pJnf_;iqZyY5|Ue|h$r z*?VTK|IBYbJhO*wWxrx;hBX`0N~_qM*?+j%CSVsh7bs$IGkT48bI`S{4fzSaVRHHQOduuxz62HJ}X{4~oENKvN{knj+6r zoZnTS?pHc%nkAp#E3}kE#=s;J&|jK6Hy28QeU%X}3xHFKIqGG=#D|UzfXxfwQ29&0H>oy_ovcVMGze~ z$V>CmI|@NiOLcd~^)M0AZ}nTv5{sXv?0wp{o7<{c)|7N|?DoNP53b4th@P^OIlK7=(g@#}nB+Sl5xl9QFsVt9M;Dj!2!cY20m8a09L6 z)vB1|8|0>&k1^YEKrt=&#WQHEz;HtcZ# zKKTco8~QHsJl^TP%$EXyy=Kd$#wt=hXUl-}a|(T64F}%kpDLaZVhgJV%|Kl3cd8im z(g#G=t1CmmyXi2QYVQW@Ze2(ybbWutes^Grh7Unk`c5kJWOYkkVNWbftMw?VddUmj z&nmowZZ_8Gmjd(Y{y-VFO%GJOGjn%$44CK`%nHXTjk$?p%#16t-edoIOvs#TEA*aw zhhzmPU&sc3lzZKkTC<^G6KlQw$Zx^aJC;sd`qVm6(f9eC4Qsd#7G50mq5haY%AE~9GAM^`bpLBr3-8V{Pzabr zyNr0fx##Z=n#|tKQ+1b_%#vzM<0@#Us3}FNy%SwJrIbEcQnqwQDF)3|WI|Cs$Vn=Z zrHDTpv8GDfYMA4bsmye3%&WWsT(-@mC6qqkz84H0qCrc98P~J4-lQ{ zfCZec_h(IByseVnHBv^s)ne#9oJHko(e>UAvI{p~KnFqXqRq*KAh=!R_yRTvW*0k7 z-U&j^z5o@n2aC>TfPR33WoO^kf0quH%=u)E$E`TZ9DW-*!Q~g%F8o8>yM1cwS+nZ; zC*>_eVUofPg&qmRZS9)he%zE<&}q)VY#7gC_DFOZlUfS* zd?f9hS{gU0opweoHF>^(Ca9L4Od`=vsl7IuN74Ay&_*OYjYADJG+#p#R>KUDwrM9@ zQ)K4jY3EzhWJrB9w${|+&*qzi=bZL^r`8M!OQ4ap{L1?RMzf-0wGN5}|K$f4EXy8l!UyrYKy9xZDzJiaa8N z>m4iQ{t>tBz2sp!m1(9=W+VRh`L_Pbjk4c=9b=M6<224t)ZBbMMM^1lu(*=qrxZO{ zAW6|yN;E8K+7}7M%#rqJ%ttBe^T{;cqcnBW5RLsPwQatVCUlhEMp~z_9=#5m&!q_* zp~FaXH0~qR@qAl!l#ce-YvB2Gmy6e=M{mMLZ;9No>g=|-c!LBzZTZ+N6%FOLgqWcS zP)E(=g{vBAf>}vR@(9I64LLG?qQS3T!qX_mz6SF^O~;Roa(Nt&i0Z~9?qp=R>YO{ z4d$sJl$e-=gnplPrdakJ%i{Z(6)BS<>*5g^YT3$FJT#_14KM+5>G8@G^@oBcY30Y3jb}&VjETw2Y z_}ZTWP&69EurFvG#HppnFF4_}Z=w4Z{BT+oX<^+EKdg=DcrVEvzSU%RBf!AYE_aaj*dZb7Mh2_(4v(>w;ylVGicF zn|&+4Sxlx+a;voY`Z0Q(%o%Jln_5p62{vae7cw*L5kvH3UYaRG-(+^0acgQNS%_xV zidrYL(jJGV=8^?y5QfCa+%yx0aL{QGQ$i7Vndl-?Bt2OQo2(E~vg%s05Gf(SPuo5= zA4S9YZ6W4(0-V#@|EGXa4!_x8qG0=%)c=CYF|mf0nAr98##N_M%*NUPy|E{dAfXth~wDLef2 zE#kFC(>($`AWT(*dn}UbpsLMHXs7z9Y9)^qP~B8@lL;hhkgBH97>fEtRmX^crv|9r z8yc&jda3FS5w@w%TQp?G;;Bw8+A@Sbs$Yv%Z&3A-4|0jFR7<2`?)dQ% z`$2wdq0BGIgVNUGW5W0jC(>kYw0=hfX+DSF+hL|ZV!UUw!%H_+#}DnW(~a9kD|dwG zW^MTO9aj3|u+iKd0XifMKexk8H#r_{vt4k)G>nn`?&78^T*k%CUq}>SG*el$FY_he6SEyO_L_SLS}lsOO=Km+Mp2S zsR4%R)4z95(~G?7QtH`|xP)Rh`WtB~h#Vh%p1Y5vZvCBgph|g+{QAIK-ri=+RN}%b za#E`e>=D^!q5^9X?Q|_`Xz^q5OXBxDLR$Z#q#3?H-0Q1fpk-tWGQEGWr`86aU~`;T zglF+nDu3%)y>*W{y$odSIpw|gt8CA6kPGdPooGlW-g2Wm6#=@@lEi#6xb~}}G$-E- z8KK~fN;LcE;ZF!4yt9h2s`{Xiv0C3RoNBied1dNF7b`Q6%^Apr*R2#_aX1s>cq%`4 zi2ELpL-dcE7kJj>9BCuu9ruAFQ;dV4t^-G>Wgb29_a z+ykC00G{RnEr;6)zR=8gA!y#u+yZ2Z1fFUKo_wtic%dm6&WL_CuK6q@G*J6k%pf># zJR|9I18kTSuC^3=LNMxzU<{97^lib|vl&qm8S;qLX0>Ej3mK~Be-0){4?B!uyrlZo zxULd3S7n5?2osh1nBBXZDX-SS!D_`uE~U?lMS7y=cPNS1TAOJ7!~PDrp*P*1hl`bNPGo^JkmYfdl)UxC85w(B2V_$`9m0=@7@n zwerfZ)z?YF&NulbvF(JDiFt~m8IJ7J@YpI(_n`^STo4-GT( zk!=R;T2B@7aJLEX77?%BxLgrB=F!Mm9u`Lxn*l97Y?t*t>MrY*NAY2GuiqKwm z+m($O_wpaCHb^7Q6kYiReoCRdCg|RMQWE4XnoV(PUxLW3@$t*K+2-znId03j4rIxK zZL<&R)}=pdjTV=0WNqyfv^deU;OE#M$uyQ^+B>*>l6(8~Qps3y#dMU-S9GPw3jBSx zuQggpgy^uUdDA+&6;Td%s*|&j+*|B({!7QdJto%GsArZ>rK(WbGOHEh%Q<%8)7_fT z_hVe;RZ!0n4{1sHuKqCE8|eaNmsmgR>rNlP*$YiZxb%HXcT+hH%ha71T-oVxXqG=& z2KYEstQak9xo|&I*fNV~YG369ctc#NcJgP-tkDhZ)$X6H1qUP)ge@pM2Q0yImx9Em z4n@#)`blS)6RVr>M~)5t@&nm|!Ca#2a-u^Ma)FE+*v_6mtAEskpGD=)iPszXa=C>U z%&%3i$0~c{XUSd|`-*JKxaP#Nvh-2LY^P1Y#}O2D!bIEm6?Fnt=36cQ$9GP+I%`hO zCs|FmC$3(ff?c<+r7Qv7UpOc?h6BAJk|H{;1FAC%w=$>k*|>*;(|GV&s$&cVQs{&U$(K&q|vn3TOJsdCSlrdN_U*~ zhWtz_l{^Pj_c=`yVKu^div{r^-!zO|?;6g9Cw;PZ5T?TZP-sX?3Yr6v^qeBv4qB5> znA}q8UBrQ`y-M#mJexUSallME?Pjq-n__JNLY^F~0;{L1`wE=f!=6q+YF>W(diz-w zPqFCho?U{xXkW*D#nd9<9%IqeL_=vzUO9fGl41T&+6KvoU|0gKe2S-lVId3JO(MLj zc4~IXqPh$l>h`5d&3PLZz?65*{6EiH_@0<5#ijUY8zsceCMEJq%Mz=7dUSEB`+2MNpm7UC~I-r{(!rjtay^u>^5A<(jfN)B}4_G2lSsv8xrq zrEsJ}qoM`RgW$NuF>YPnsZu={{3Q10YsvgtCpfM`kQ{Mv`Az)w7TRS#qxlV?$-g`D_4IJWy^i|FRk&Qmrj*!CbrUIDCmNqrUA^~xTZwxw| zy4~k?N;k1eFrl5#K!_Kx6s;EbOP>LsOVoCzKWPOMzWg_Qz_dT``NOdP;m=l)>c9EJ z;2p2eepBN3(vD^uZtnJfMmdx4g^}Wv!52mr(mz@LFKyy}pZ))PXde}_BL3i$#ql4! z>N@@*s3Y6euwLetVOILT8CL*gApuXIfTv%pul`}6V1y$Bd4aj~k{B8l)SRvDd~PR$ z!QqqF6MY#IJjWr#>l+tJIoGk{b|xQS^jS{(`2P8;hTXBD?FWu*`C(!#v0XgTw;3oD zjB4NB(2(iOj&S#4-nWUzeCQ%YjvO-^HqLY~F`ZyKaeU0g9dyFk;faI8cgGh@*IBq? zWHq|G-*>;~5)&0fg+Dp%d^+VL^doaBX9A~&-+2<><=_~e_ve{9MbG##F);<5$nMi0 zVs{WewUHS-7kU5r3048nj~InG?Ko6Hfp&-G)u=)QpB@O4f7rTCd`~=o*veOqwJO?Y zyQ|-(nQNb8aD?JG;~*{XAlnc)cf@QF3E3a`nHPB@381*h70+H4A0YY+_#}fz_oDOL zv)417-Y`_;oOAlZgzHPFnFTZvF8}RG{A!W>T!7}4=F#80)-~Hg{C?}C&xl3yt63DbZpp# zh`+gns(3#EggX##tI8ETrxX@XO(_ztuMA&U)q2zfKl3xdqU-Ufezun0iPOW7M?Xib QFP0|&_qk`@GEO`H0%fES*#H0l diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff deleted file mode 100644 index 68e61b1298dc17fefa60f27b7026a2086e44ce26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67428 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�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 diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff deleted file mode 100644 index 2a50c6d2fd438d7af1dac13fc59715e890f971d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72748 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%0DWw(Y! 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 diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff deleted file mode 100644 index e65d5310e4526fd16c11e31206634226e6d03ea8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65392 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 lY4M?6^ETWw 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 diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff deleted file mode 100644 index 6ca15f36f5c48d9f456be7e99d0515e95e212aea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67976 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%0ArtI)I|QQP4R9kw-lOUH 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 diff --git a/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png b/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png deleted file mode 100644 index dcc70338eaaf61c743c1039bab5d4d6f7dc12cb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1502 zcmV<41tI#0P)qr?$jSU;qFB06^sI)&tBXJphH_bMbR++qP}nwr$(CZQHhO+qSOG+88C% zPVzmm-@493b^8tZodGo?Ktf>#-uChLy!+Ei*u0bqc7}pv{u@mtt`d+;-B(o98gj{a zgUV__D&e+K*`>IU$v?xW>~S?nrNVO+&9xnnNrE%hYYsTydd+C!vR-rQYu0NHvch`J zXyLJ5bINPhYYw>Bdd;ZgvR-rCv;4D1@-_cGjtZ9l57`uaN(B#cLNXz?Qqj5~kV^|M z6}js{D)BB*g;Oyg5$7nXGQbXbWPLzY?xck@{4J(RGyjDwO1z{>&+MpAGx-6s+Y7aHo8VoY1=b;AfYLGyf1Jq<^aQP=%%Bc>uhZ=1Q3el>$snMIV5G=?R zYPLE6#47%Znmx}4q5fM;9cKLvkupA_4);<*Ao~dF(hmZ~xI|q}M}jzwsE@ZMgbB2T zI<58xQL;ayPLI+-l%dqEHv|cPlDZuV2{B5zsN1Uo5W+E;28^&lgh=OT!0|Aa+a2Vq zd=Oz24H*CdLLR0ey92{~nV->+N2y@G)-WsFjyI;8*9v!(5r3^Aq32e#!oUO8=%Fa+RMwP6=~W^6{gaePY_7fj!>fJN(E~ z9OmlC4_?nrZihyWw2R;HgJwd$c$aJWgAM^2T<3fJVXm%xXKO0xW6Q9M@3e%uVs3W5 zoE{OnMVjyxgY$B%q$PH1McK`8%OD#U*#BOQt&ZxRh&n$|xV(9!5+Pdlg1_#@Fpfl~q9T*PcrN|ovn2>i zVrRS2&SA7v+s+FQUw`=U=F!Dn3(^?vxM)AO#}SIuv7E@_CX7_I^Klo=OfoZeKiyM< zkyhtPWe+6~g2b_+NVPX%xU${vN+u4H@#DS<3|IF}r1FR3aBoLWB<*zuLy~Ks6^)~) z^m0QALsCzTq_W%K+=B1oU-l}E0kH+wv&L~)?)Ku?(950}|I+sba4dPt_>bC~(XaMI zyKw^6d8Quy>b{Eq*qs#IT9akJa|!&D+4+Iugrv`V%kfYA1pDRI!KuV8W53+VDj>D` zaZ#LX_-^Bo^qY3V$Um63T_Z4vHvM~U5I}V4ol^BG5aH%nfk*Ll# zxD>x(oHN(^G*)LhXN660D|O#ENA|1?j;-hx=S10ca4mL*bEC@%I9K(|I0yEo4k1|Z z**I(Zz6_yQ@!dE}YF~$tteEXz+qbm{P1{q7GbX)Uj1X0x=@n;4xr;RjReb$>aYpoc za{^(jzm^xLO~qSHh=AmdZ^mh|pL^4YhPL}9aT>%YixC-RhdYf^vhEWVh>+Ow7ZfL_ zH>=}_ma4P8;v|&2REx-oZ}=ciNME)l5Je5w3*xv`ywi+`O6~e?9EY{T$ zh))+I(#no>8Aq_5lNE@#*os#Yhv@B^IHIrmTvqI-{N*~#3yF;%CH^J*vMq`EqVaMk z&DeFl1@lOvb?2#5yB8$jd7kHap67X<=Xsvzd0sI70Q0SE5-Sa1#{d8T07*qoM6N<$ Eg2^c8-v9sr diff --git a/gno.land/pkg/gnoweb/static/img/favicon-16x16.png b/gno.land/pkg/gnoweb/static/img/favicon-16x16.png deleted file mode 100644 index ee407d9a7eaae40fc4b82aba910697a0f7add1e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAialK%Ln;`P6{LQA@%`fXO#0sM z`r~Y!=QqCj|KE3x2h+Rx|Nj4f()Pgk>~DUS;+YSaCN1YXGpF*w6qXo=6H{kToq9Wz zL+`-z#90#(UL9l?pRjsee%7k>)7rTUj+>jBn5Y&guMzyPe(hIrmom-O>*TaI?qgvH WGf_0_Tz9k&&ac>+9EFKx#V&76l3q zK5i)VwX^dr-0<;YI0H62Dw`nk zO_gSh*c33SXF%MLslkLJz6Jej8XIf<1#zco7Q>ueBNn{Lo}Zs_@a07S9596zC*b3N P00000NkvXXu0mjf1D=47 diff --git a/gno.land/pkg/gnoweb/static/img/github-mark-32px.png b/gno.land/pkg/gnoweb/static/img/github-mark-32px.png deleted file mode 100644 index 8b25551a97921681334176ee143b41510a117d86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1714 zcmaJ?X;2eq7*4oFu!ne{XxAht2qc?8LXr|_LPCfTpaBK7K$c{I0Ld=NLIOeuC;@2) zZ$K%a)k+m-s0>xHmKxL%0V&0TRzzznhgyqrIC$F)0{WwLXLrBvd*^wc_uSc%h%m9E z{W5z3f#4_!7RvAyFh6!S_*<8qJ%KOIm?#E|L=rJQq=gB5C6WLG5;c?r%V0>EmEH#X z5eSwPRa6WXBMs#$5H%GtW2go-in9p>zW@UYDNNWc^XOXZQ? z1QjEV00I#$3^1wQUJ8&-2UsjB-G|9y(LDhMNN3PM{APL4eYi{(m*ERcUnJa{R+-3^ z34^A6;U^v`8N*O6ji%S@sd{fJqD`XFIUJ5zgTe5^5nj414F(y!G&=H(f)Lgzv?>%+ zAsWD}2qhpH7>|TU`X&W6IxDNuO_vET7|j5oG&&VDr!)hUO8+0KR?nh!m<)a!?|%yG zqOwq!CWCcIhE{<$E|F|@g>nP6FoYr6C<8>D?ID9%&5J(4oSbR1I^byW*g@__U z4QsF&uJSEcFeleM3~ChjEQGbHOjsGDMbyAl(p=Ttv9RaVo8~I#js@@Y9C^_2U})yn zzSHU%6FxuY?d;&65MyR({^lU*3$z$ZllDb(o&<7d;A_`h2U+3~BJ2Hv`{W}KEU801#cv_B|9Cm!ynR{S`AMsSn z;7E=B;mb!wx$L;S>yGXG^6=&WlQn9$s?&L%Y1D8TI^MlKB1DqsEng$>f4=xYWBoPI z_S1p!sJ#d2?YI4kPA{k}Eby?F=f-J9zIc`YDl^pzjVm~9ebE?Hn?t0Nx+la|D0MB; z9)2xv1G>a1|A9kQ>~DV<=X3-4yC&n!m8-3K#P z{X@0zRuQsy$+N ziSCoLJU{Z$nQy4A4Y5UJ07$5FA~qL2%Q+cLaqDU?Lz3?=BC5;Nk6BbTmmceEaM>-Z zi>O&-dSE=%ex;vcvCOk{*JQ5^_4M z4lW7%l9IqY(z7pV(?I@@8=KPFO82)O{VDI18-*d-k$YmI^XiuPs_LuFw<^ZcD}yP5 c*NrbeloN*74g`U%%F6r~k%+>C^#XapzmV0H-2eap diff --git a/gno.land/pkg/gnoweb/static/img/github-mark-64px.png b/gno.land/pkg/gnoweb/static/img/github-mark-64px.png deleted file mode 100644 index 182a1a3f734fc1b7d712c68b04c29bad9460d6cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2625 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 W4I>>0bz>}E>ge79v - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-email.svg b/gno.land/pkg/gnoweb/static/img/ico-email.svg deleted file mode 100644 index ff397fe664d..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-email.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-telegram.svg b/gno.land/pkg/gnoweb/static/img/ico-telegram.svg deleted file mode 100644 index 32932830dc3..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-telegram.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-twitter.svg b/gno.land/pkg/gnoweb/static/img/ico-twitter.svg deleted file mode 100644 index cf666e3842d..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-twitter.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-youtube.svg b/gno.land/pkg/gnoweb/static/img/ico-youtube.svg deleted file mode 100644 index 36efdd185f0..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-youtube.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/list-alt.png b/gno.land/pkg/gnoweb/static/img/list-alt.png deleted file mode 100644 index 14296a4d28f8dc3c4b97fe44cfda47d840d9a430..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 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 Kv_kpSyZ;Y(*HHTa diff --git a/gno.land/pkg/gnoweb/static/img/logo-square.svg b/gno.land/pkg/gnoweb/static/img/logo-square.svg deleted file mode 100644 index e6ab42f5ad4..00000000000 --- a/gno.land/pkg/gnoweb/static/img/logo-square.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/gno.land/pkg/gnoweb/static/img/logo-v1.png b/gno.land/pkg/gnoweb/static/img/logo-v1.png deleted file mode 100644 index 702fce47a52b10bf195d8a624f925584162082e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11122 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 diff --git a/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg b/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg deleted file mode 100644 index 0005420c58d..00000000000 --- a/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - -Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - diff --git a/gno.land/pkg/gnoweb/static/invites.txt b/gno.land/pkg/gnoweb/static/invites.txt deleted file mode 100644 index 7bef15f954f..00000000000 --- a/gno.land/pkg/gnoweb/static/invites.txt +++ /dev/null @@ -1,48 +0,0 @@ -g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1 -g13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1 -g1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1 -g1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1 -g18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1 -g19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1 -g187982000zsc493znqt828s90cmp6hcp2erhu6m:1 -g1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1 -g16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1 -g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1 -g1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1 -g1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1 -g19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1 -g1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1 -g14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1 -g1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1 -g15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1 -g1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1 -g1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1 -g1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1 -g152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1 -g1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1 -g1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1 -g1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1 -g1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1 -g1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1 -g13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1 -g19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1 -g1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1 -g1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1 -g19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1 -g1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1 -g13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1 -g1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1 -g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1 -g1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1 -g1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1 -g1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1 -g14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1 -g19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1 -g1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1 -g1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1 -g1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1 -g1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1 -g1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1 -g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq:10 -g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10 -g14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5 diff --git a/gno.land/pkg/gnoweb/static/js/highlight.min.js b/gno.land/pkg/gnoweb/static/js/highlight.min.js deleted file mode 100644 index 5135b77ab5b..00000000000 --- a/gno.land/pkg/gnoweb/static/js/highlight.min.js +++ /dev/null @@ -1,331 +0,0 @@ -/*! - Highlight.js v11.9.0 (git: b7ec4bfafc) - (c) 2006-2024 undefined and other contributors - License: BSD-3-Clause - */ - var hljs=function(){"use strict";function e(t){ - return t instanceof Map?t.clear=t.delete=t.set=()=>{ - throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{ - throw Error("set is read-only") - }),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{ - const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i) - })),t}class t{constructor(e){ - void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} - ignoreMatch(){this.isMatchIgnored=!0}}function n(e){ - return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") - }function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] - ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope - ;class o{constructor(e,t){ - this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ - this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{ - if(e.startsWith("language:"))return e.replace("language:","language-") - ;if(e.includes(".")){const n=e.split(".") - ;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") - }return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)} - closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ - this.buffer+=``}}const r=(e={})=>{const t={children:[]} - ;return Object.assign(t,e),t};class a{constructor(){ - this.rootNode=r(),this.stack=[this.rootNode]}get top(){ - return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ - this.top.children.push(e)}openNode(e){const t=r({scope:e}) - ;this.add(t),this.stack.push(t)}closeNode(){ - if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ - for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} - walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ - return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), - t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ - "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ - a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e} - addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ - this.closeNode()}__addSublanguage(e,t){const n=e.root - ;t&&(n.scope="language:"+t),this.add(n)}toHTML(){ - return new o(this,this.options).value()}finalize(){ - return this.closeAllNodes(),!0}}function l(e){ - return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")} - function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")} - function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{ - const t=e[e.length-1] - ;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} - })(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"} - function p(e){return RegExp(e.toString()+"|").exec("").length-1} - const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ - ;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n - ;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break} - s+=i.substring(0,e.index), - i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0], - "("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)} - const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={ - begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'", - illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", - contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t, - contains:[]},n);s.contains.push({scope:"doctag", - begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", - end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) - ;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) - ;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s - },S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({ - __proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{ - scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N, - C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number", - begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{ - "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ - t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E, - MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0}, - NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w, - PHRASAL_WORDS_MODE:{ - begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ - },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, - end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]}, - RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", - SHEBANG:(e={})=>{const t=/^#![ ]*\// - ;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t, - end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, - TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x, - UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){ - "."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){ - void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){ - t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", - e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, - void 0===e.relevance&&(e.relevance=0))}function L(e,t){ - Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){ - if(e.match){ - if(e.begin||e.end)throw Error("begin & end are not supported with match") - ;e.begin=e.match,delete e.match}}function P(e,t){ - void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return - ;if(e.starts)throw Error("beforeMatch cannot be used with starts") - ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] - })),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={ - relevance:0,contains:[Object.assign(n,{endsParent:!0})] - },e.relevance=0,delete n.beforeMatch - },H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword" - ;function $(e,t,n=C){const i=Object.create(null) - ;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{ - Object.assign(i,$(e[n],t,n))})),i;function s(e,n){ - t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") - ;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ - return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{ - console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{ - z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) - },K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={} - ;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1]) - ;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{ - e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, - delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ - _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope - }),(e=>{if(Array.isArray(e.begin)){ - if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), - K - ;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"), - K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{ - if(Array.isArray(e.end)){ - if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"), - K - ;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"), - K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){ - function t(t,n){ - return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) - }class n{constructor(){ - this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} - addRule(e,t){ - t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), - this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) - ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|" - }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex - ;const t=this.matcherRe.exec(e);if(!t)return null - ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] - ;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){ - this.rules=[],this.multiRegexes=[], - this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ - if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n - ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), - t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ - return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ - this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ - const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex - ;let n=t.exec(e) - ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ - const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} - return n&&(this.regexIndex+=n.position+1, - this.regexIndex===this.count&&this.considerAll()),n}} - if(e.compilerExtensions||(e.compilerExtensions=[]), - e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") - ;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o - ;if(o.isCompiled)return a - ;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))), - o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null - ;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords), - c=o.keywords.$pattern, - delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)), - a.keywordPatternRe=t(c,!0), - r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/), - o.end&&(a.endRe=t(a.end)), - a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)), - o.illegal&&(a.illegalRe=t(o.illegal)), - o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{ - variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{ - starts:e.starts?i(e.starts):null - }):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a) - })),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s - ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" - }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" - }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ - return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ - constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} - const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{ - const i=Object.create(null),s=Object.create(null),o=[];let r=!0 - ;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ - disableAutodetect:!0,name:"Plain text",contains:[]};let p={ - ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, - languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", - cssSelector:"pre code",languages:null,__emitter:c};function b(e){ - return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s="" - ;"object"==typeof t?(i=e, - n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."), - G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), - s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o) - ;const r=o.result?o.result:E(o.language,o.code,n) - ;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){ - const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R) - ;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n="" - ;for(;t;){n+=R.substring(e,t.index) - ;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){ - const[e,i]=o - ;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{ - const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0] - ;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i - ;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{ - if(""===R)return;let e=null;if("string"==typeof N.subLanguage){ - if(!i[N.subLanguage])return void M.addText(R) - ;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top - }else e=x(R,N.subLanguage.length?N.subLanguage:null) - ;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language) - })():l(),R=""}function u(e,t){ - ""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1 - ;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue} - const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}} - function h(e,t){ - return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope), - e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), - R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{ - value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t) - ;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e) - ;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){ - for(;e.endsParent&&e.parent;)e=e.parent;return e}} - if(e.endsWithParent)return f(e.parent,n,i)}function b(e){ - return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){ - const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N - ;N.endScope&&N.endScope._wrap?(g(), - u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(), - d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t), - g(),o.excludeEnd&&(R=t));do{ - N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent - }while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length} - let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0 - ;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){ - if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`) - ;throw t.languageName=e,t.badRule=w.rule,t}return 1} - if(w=o,"begin"===o.type)return(e=>{ - const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]] - ;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n) - ;return i.skip?R+=n:(i.excludeBegin&&(R+=n), - g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o) - ;if("illegal"===o.type&&!s){ - const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') - ;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e} - if("illegal"===o.type&&""===a)return 1 - ;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches") - ;return R+=a,a.length}const _=O(e) - ;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"') - ;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[] - ;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope) - ;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{ - if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){ - I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A - ;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e) - ;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e, - value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){ - if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n), - illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A, - context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{ - language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N} - ;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{ - const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)} - ;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1))) - ;s.unshift(n);const o=s.sort(((e,t)=>{ - if(e.relevance!==t.relevance)return t.relevance-e.relevance - ;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 - ;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r - ;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{ - let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" - ;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1]) - ;return t||(X(a.replace("{}",n[1])), - X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} - return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return - ;if(N("before:highlightElement",{el:e,language:n - }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) - ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), - console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), - console.warn("The element with unescaped HTML:"), - console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) - ;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i) - ;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n - ;e.classList.add("hljs"),e.classList.add("language-"+i) - })(e,n,o.language),e.result={language:o.language,re:o.relevance, - relevance:o.relevance},o.secondBest&&(e.secondBest={ - language:o.secondBest.language,relevance:o.secondBest.relevance - }),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){ - "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0 - }function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]} - function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ - s[e.toLowerCase()]=t}))}function k(e){const t=O(e) - ;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{ - e[n]&&e[n](t)}))} - "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ - y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_, - highlightElement:w, - highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"), - G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)}, - initHighlighting:()=>{ - _(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, - initHighlightingOnLoad:()=>{ - _(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") - },registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){ - if(W("Language definition for '{}' could not be registered.".replace("{}",e)), - !r)throw t;W(t),s=l} - s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{ - languageName:e})},unregisterLanguage:e=>{delete i[e] - ;for(const t of Object.keys(s))s[t]===e&&delete s[t]}, - listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, - autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{ - e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ - e["before:highlightBlock"](Object.assign({block:t.el},t)) - }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ - e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)}, - removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{ - r=!1},n.safeMode=()=>{r=!0},n.versionString="11.9.0",n.regex={concat:h, - lookahead:g,either:f,optional:d,anyNumberOfTimes:u} - ;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n - },ne=te({});return ne.newInstance=()=>te({}),ne}() - ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `go` grammar compiled for Highlight.js 11.9.0 */ - (()=>{var e=(()=>{"use strict";return e=>{const n={ - keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], - type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], - literal:["true","false","iota","nil"], - built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] - };return{name:"Go",aliases:["golang"],keywords:n,illegal:"{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={ - scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{ - literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/, - relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0 - },e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], - illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `plaintext` grammar compiled for Highlight.js 11.9.0 */ - (()=>{var t=(()=>{"use strict";return t=>({name:"Plain text", - aliases:["text","txt"],disableAutodetect:!0})})() - ;hljs.registerLanguage("plaintext",t)})(); \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/marked.min.js b/gno.land/pkg/gnoweb/static/js/marked.min.js deleted file mode 100644 index 3cc149db48e..00000000000 --- a/gno.land/pkg/gnoweb/static/js/marked.min.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * marked v12.0.2 - a markdown parser - * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1");e=x(e.replace(/^ *>[ \t]?/gm,""),"\n");const n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'

'+(n?e:c(e,!0))+"
\n":"
"+(n?e:c(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); -/** - * Minified by jsDelivr using Terser v5.19.2. - * Original file: /npm/marked-highlight@2.1.1/lib/index.umd.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).markedHighlight={})}(this,(function(e){"use strict";function t(e){return(e||"").match(/\S*/)[0]}function n(e){return t=>{"string"==typeof t&&t!==e.text&&(e.escaped=!0,e.text=t)}}const i=/[&<>"']/,o=new RegExp(i.source,"g"),r=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,g=new RegExp(r.source,"g"),h={"&":"&","<":"<",">":">",'"':""","'":"'"},s=e=>h[e];function c(e,t){if(t){if(i.test(e))return e.replace(o,s)}else if(r.test(e))return e.replace(g,s);return e}e.markedHighlight=function(e){if("function"==typeof e&&(e={highlight:e}),!e||"function"!=typeof e.highlight)throw new Error("Must provide highlight function");return"string"!=typeof e.langPrefix&&(e.langPrefix="language-"),{async:!!e.async,walkTokens(i){if("code"!==i.type)return;const o=t(i.lang);if(e.async)return Promise.resolve(e.highlight(i.text,o,i.lang||"")).then(n(i));const r=e.highlight(i.text,o,i.lang||"");if(r instanceof Promise)throw new Error("markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.");n(i)(r)},renderer:{code(n,i,o){const r=t(i),g=r?` class="${e.langPrefix}${c(r)}"`:"";return n=n.replace(/\n$/,""),`
    ${o?n:c(n,!0)}\n
    `}}}}})); -//# sourceMappingURL=/sm/3bfb625a4ed441ddc1f215743851a4b727156eef53b458bd31c51a627ce891c9.map \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/purify.min.js b/gno.land/pkg/gnoweb/static/js/purify.min.js deleted file mode 100644 index ed613fcc36f..00000000000 --- a/gno.land/pkg/gnoweb/static/js/purify.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! @license DOMPurify 2.3.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.6/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,(function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),z=a(/^data-[\-\w.\u00B7-\uFFFF]/),B=a(/^aria-[\-\w]+$/),P=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),G=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=a(/^html$/i),q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function Y(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.3.6",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,m=t.NamedNodeMap,A=void 0===m?t.NamedNodeMap||t.MozNamedAttrMap:m,$=t.HTMLFormElement,X=t.DOMParser,Z=t.trustedTypes,J=s.prototype,Q=w(J,"cloneNode"),ee=w(J,"nextSibling"),te=w(J,"childNodes"),ne=w(J,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V(Z,r),ie=oe?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=ae.getElementsByTagName,me=r.importNode,fe={};try{fe=x(o).documentMode?o.documentMode:{}}catch(e){}var de={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==fe;var pe=H,he=U,ge=z,ye=B,ve=j,be=G,Te=P,Ne=null,Ae=E({},[].concat(Y(k),Y(S),Y(_),Y(O),Y(M))),Ee=null,xe=E({},[].concat(Y(L),Y(R),Y(I),Y(F))),we=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,Se=null,_e=!0,De=!0,Oe=!1,Ce=!1,Me=!1,Le=!1,Re=!1,Ie=!1,Fe=!1,He=!1,Ue=!0,ze=!0,Be=!1,Pe={},je=null,Ge=E({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),We=null,qe=E({},["audio","video","img","source","image","track"]),Ye=null,Ke=E({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",$e="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml",Ze=Xe,Je=!1,Qe=void 0,et=["application/xhtml+xml","text/html"],tt="text/html",nt=void 0,rt=null,ot=o.createElement("form"),it=function(e){return e instanceof RegExp||e instanceof Function},at=function(e){rt&&rt===e||(e&&"object"===(void 0===e?"undefined":q(e))||(e={}),e=x(e),Ne="ALLOWED_TAGS"in e?E({},e.ALLOWED_TAGS):Ae,Ee="ALLOWED_ATTR"in e?E({},e.ALLOWED_ATTR):xe,Ye="ADD_URI_SAFE_ATTR"in e?E(x(Ke),e.ADD_URI_SAFE_ATTR):Ke,We="ADD_DATA_URI_TAGS"in e?E(x(qe),e.ADD_DATA_URI_TAGS):qe,je="FORBID_CONTENTS"in e?E({},e.FORBID_CONTENTS):Ge,ke="FORBID_TAGS"in e?E({},e.FORBID_TAGS):{},Se="FORBID_ATTR"in e?E({},e.FORBID_ATTR):{},Pe="USE_PROFILES"in e&&e.USE_PROFILES,_e=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ce=e.SAFE_FOR_TEMPLATES||!1,Me=e.WHOLE_DOCUMENT||!1,Ie=e.RETURN_DOM||!1,Fe=e.RETURN_DOM_FRAGMENT||!1,He=e.RETURN_TRUSTED_TYPE||!1,Re=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,ze=!1!==e.KEEP_CONTENT,Be=e.IN_PLACE||!1,Te=e.ALLOWED_URI_REGEXP||Te,Ze=e.NAMESPACE||Xe,e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Qe=Qe=-1===et.indexOf(e.PARSER_MEDIA_TYPE)?tt:e.PARSER_MEDIA_TYPE,nt="application/xhtml+xml"===Qe?function(e){return e}:h,Ce&&(De=!1),Fe&&(Ie=!0),Pe&&(Ne=E({},[].concat(Y(M))),Ee=[],!0===Pe.html&&(E(Ne,k),E(Ee,L)),!0===Pe.svg&&(E(Ne,S),E(Ee,R),E(Ee,F)),!0===Pe.svgFilters&&(E(Ne,_),E(Ee,R),E(Ee,F)),!0===Pe.mathMl&&(E(Ne,O),E(Ee,I),E(Ee,F))),e.ADD_TAGS&&(Ne===Ae&&(Ne=x(Ne)),E(Ne,e.ADD_TAGS)),e.ADD_ATTR&&(Ee===xe&&(Ee=x(Ee)),E(Ee,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&E(Ye,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(je===Ge&&(je=x(je)),E(je,e.FORBID_CONTENTS)),ze&&(Ne["#text"]=!0),Me&&E(Ne,["html","head","body"]),Ne.table&&(E(Ne,["tbody"]),delete ke.tbody),i&&i(e),rt=e)},lt=E({},["mi","mo","mn","ms","mtext"]),ct=E({},["foreignobject","desc","title","annotation-xml"]),st=E({},S);E(st,_),E(st,D);var ut=E({},O);E(ut,C);var mt=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Xe,tagName:"template"});var n=h(e.tagName),r=h(t.tagName);if(e.namespaceURI===$e)return t.namespaceURI===Xe?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||lt[r]):Boolean(st[n]);if(e.namespaceURI===Ve)return t.namespaceURI===Xe?"math"===n:t.namespaceURI===$e?"math"===n&&ct[r]:Boolean(ut[n]);if(e.namespaceURI===Xe){if(t.namespaceURI===$e&&!ct[r])return!1;if(t.namespaceURI===Ve&&!lt[r])return!1;var o=E({},["title","style","font","a","script"]);return!ut[n]&&(o[n]||!st[n])}return!1},ft=function(e){p(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},dt=function(e,t){try{p(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(Ie||Fe)try{ft(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},pt=function(e){var t=void 0,n=void 0;if(Re)e=""+e;else{var r=g(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===Qe&&(e=''+e+"");var i=oe?oe.createHTML(e):e;if(Ze===Xe)try{t=(new X).parseFromString(i,Qe)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ze===Xe?ue.call(t,Me?"html":"body")[0]:Me?t.documentElement:a},ht=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},gt=function(e){return e instanceof $&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof A)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},yt=function(e){return"object"===(void 0===c?"undefined":q(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":q(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},vt=function(e,t,r){de[e]&&f(de[e],(function(e){e.call(n,t,r,rt)}))},bt=function(e){var t=void 0;if(vt("beforeSanitizeElements",e,null),gt(e))return ft(e),!0;if(g(e.nodeName,/[\u0080-\uFFFF]/))return ft(e),!0;var r=nt(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:r,allowedTags:Ne}),!yt(e.firstElementChild)&&(!yt(e.content)||!yt(e.content.firstElementChild))&&T(/<[/\w]/g,e.innerHTML)&&T(/<[/\w]/g,e.textContent))return ft(e),!0;if("select"===r&&T(/

    w%k_tC9o+J?jlEWgdzIe0b`MG#H=N&ZEd~ha9 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..53d081f3a538a63578c15a5cc11219b32e6d5795 GIT binary patch literal 12764 zcmV<2F(b}*Pew8T0RR9105RME4gdfE09y0_05O380RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/public/imgs/gnoland.svg b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg new file mode 100644 index 00000000000..30d2f3ef56a --- /dev/null +++ b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gno.land/pkg/gnoweb/public/js/copy.js b/gno.land/pkg/gnoweb/public/js/copy.js new file mode 100644 index 00000000000..73cd1f9fdc2 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/copy.js @@ -0,0 +1 @@ +var s=class n{DOM;static FEEDBACK_DELAY=1500;btnClicked=null;btnClickedIcons=[];static SELECTORS={button:"[data-copy-btn]",icon:"[data-copy-icon] > use",content:t=>`[data-copy-content="${t}"]`};constructor(){this.DOM={el:document.querySelector("main")},this.DOM.el?this.init():console.warn("Copy: Main container not found.")}init(){this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("click",this.handleClick.bind(this))}handleClick(t){let e=t.target.closest(n.SELECTORS.button);if(!e)return;this.btnClicked=e,this.btnClickedIcons=Array.from(e.querySelectorAll(n.SELECTORS.icon));let i=e.getAttribute("data-copy-btn");if(!i){console.warn("Copy: No content ID found on the button.");return}let c=this.DOM.el?.querySelector(n.SELECTORS.content(i));c?this.copyToClipboard(c):console.warn(`Copy: No content found for ID "${i}".`)}sanitizeContent(t){let o=t.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g,""),e=document.createElement("div");return e.innerHTML=o,e.textContent?.trim()||""}toggleIcons(){this.btnClickedIcons.forEach(t=>{t.classList.toggle("hidden")})}showFeedback(){this.btnClicked&&(this.toggleIcons(),window.setTimeout(()=>{this.toggleIcons()},n.FEEDBACK_DELAY))}async copyToClipboard(t){let o=this.sanitizeContent(t);if(!navigator.clipboard){console.error("Copy: Clipboard API is not supported in this browser."),this.showFeedback();return}try{await navigator.clipboard.writeText(o),console.info("Copy: Text copied successfully."),this.showFeedback()}catch(e){console.error("Copy: Error while copying text.",e),this.showFeedback()}}},r=()=>new s;export{r as default}; diff --git a/gno.land/pkg/gnoweb/public/js/index.js b/gno.land/pkg/gnoweb/public/js/index.js new file mode 100644 index 00000000000..e990dd91f5f --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/index.js @@ -0,0 +1 @@ +(()=>{let s={copy:{selector:"[data-copy-btn]",path:"/public/js/copy.js"},help:{selector:"#help",path:"/public/js/realmhelp.js"},searchBar:{selector:"#header-searchbar",path:"/public/js/searchbar.js"}},r=async({selector:e,path:o})=>{if(document.querySelector(e))try{(await import(o)).default()}catch(t){console.error(`Error while loading script ${o}:`,t)}else console.warn(`Module not loaded: no element matches selector "${e}"`)},l=async()=>{let e=Object.values(s).map(o=>r(o));await Promise.all(e)};document.addEventListener("DOMContentLoaded",l)})(); diff --git a/gno.land/pkg/gnoweb/public/js/realmhelp.js b/gno.land/pkg/gnoweb/public/js/realmhelp.js new file mode 100644 index 00000000000..9b045061a00 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js @@ -0,0 +1 @@ +var s=class a{DOM;funcList;static SELECTORS={container:"#help",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(a.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(a.SELECTORS.func)),this.DOM.addressInput=e.querySelector(a.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(a.SELECTORS.cmdModeSelect),console.log(this.DOM),this.funcList=this.DOM.funcs.map(t=>new r(t)),this.bindEvents())}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM;e?.addEventListener("input",()=>{this.funcList.forEach(n=>n.updateAddr(e.value))}),t?.addEventListener("change",n=>{let d=n.target;this.funcList.forEach(l=>l.updateMode(d.value))})}},r=class a{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(a.SELECTORS.address)),args:Array.from(e.querySelectorAll(a.SELECTORS.args)),modes:Array.from(e.querySelectorAll(a.SELECTORS.mode))},this.funcName=e.dataset.func||null,this.bindEvents()}bindEvents(){this.DOM.el.addEventListener("input",e=>{let t=e.target;t.dataset.role==="help-param-input"&&this.updateArg(t.dataset.param||"",t.value)})}updateArg(e,t){this.DOM.args.filter(n=>n.dataset.arg===e).forEach(n=>{n.textContent=t.trim()||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let n=t.dataset.codeMode===e;t.className=n?"inline":"hidden",t.dataset.copyContent=n?`help-cmd-${this.funcName}`:""})}},i=()=>new s;export{i as default}; diff --git a/gno.land/pkg/gnoweb/public/js/searchbar.js b/gno.land/pkg/gnoweb/public/js/searchbar.js new file mode 100644 index 00000000000..e8012b9b6d9 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/searchbar.js @@ -0,0 +1 @@ +var n=class r{DOM;baseUrl;static SELECTORS={container:"#header-searchbar",inputSearch:"[data-role='header-input-search']",breadcrumb:"[data-role='header-breadcrumb-search']"};constructor(){this.DOM={el:document.querySelector(r.SELECTORS.container),inputSearch:null,breadcrumb:null},this.baseUrl=window.location.origin,this.DOM.el?this.init():console.warn("SearchBar: Main container not found.")}init(){let{el:e}=this.DOM;this.DOM.inputSearch=e?.querySelector(r.SELECTORS.inputSearch)??null,this.DOM.breadcrumb=e?.querySelector(r.SELECTORS.breadcrumb)??null,this.DOM.inputSearch||console.warn("SearchBar: Input element for search not found."),this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("submit",e=>{e.preventDefault(),this.searchUrl()})}searchUrl(){let e=this.DOM.inputSearch?.value.trim();if(e){let t=e;/^https?:\/\//i.test(t)||(t=`${this.baseUrl}${t.startsWith("/")?"":"/"}${t}`);try{window.location.href=new URL(t).href}catch{console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://.")}}else console.error("SearchBar: Please enter a URL to search.")}},i=()=>new n;export{i as default}; diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css new file mode 100644 index 00000000000..1bb292e3460 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/styles.css @@ -0,0 +1,3 @@ +@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Inter.var.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } + +/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility;overflow-x:hidden}@supports (font-variation-settings:normal){html{font-family:Inter var,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{font-size:1rem}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:2rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:.375rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:1.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{font-size:1.125rem;font-weight:500}.realm-content h4,.realm-content p{margin-top:1rem;margin-bottom:1rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content blockquote,.realm-content ol,.realm-content ul{margin-top:1rem;margin-bottom:1rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.25rem}.realm-content img{margin-top:1.5rem;margin-bottom:1.5rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.875rem}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2rem;margin-bottom:2rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:1.5rem;margin-bottom:1.5rem;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"“" "”" "‘" "’"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.5rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1rem;margin-bottom:1rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:1.5rem;margin-bottom:1.5rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:after{font-size:.875rem;font-weight:400;--tw-content:"open";content:var(--tw-content)}@media (min-width:51.25rem){.toc-expend-btn:after{--tw-content:none;content:var(--tw-content)}}.toc-expend-btn:has(#toc-expend:checked):after{--tw-content:"close";content:var(--tw-content)}@media (min-width:51.25rem){.toc-expend-btn:has(#toc-expend:checked):after{--tw-content:none;content:var(--tw-content)}}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.top-0{top:0}.top-14{top:3.5rem}.top-2{top:.5rem}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-flow-dense{grid-auto-flow:dense}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.rounded-l-sm{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r-sm{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r-8{border-right-width:8px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(124 124 124/var(--tw-border-opacity))}.border-r-transparent{border-right-color:transparent}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-200{--tw-text-opacity:1;color:rgb(189 189 189/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:outline-transparent:focus{outline-color:transparent}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static.go b/gno.land/pkg/gnoweb/static.go new file mode 100644 index 00000000000..7900dcd7891 --- /dev/null +++ b/gno.land/pkg/gnoweb/static.go @@ -0,0 +1,28 @@ +package gnoweb + +import ( + "embed" + "net/http" +) + +//go:embed public/* +var assets embed.FS + +func disableCache(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) +} + +// AssetHandler returns the handler to serve static assets. If cache is true, +// these will be served using the static files embedded in the binary; otherwise +// they will served from the filesystem. +func AssetHandler() http.Handler { + return http.FileServer(http.FS(assets)) +} + +func DevAssetHandler(path, dir string) http.Handler { + handler := http.StripPrefix(path, http.FileServer(http.Dir(dir))) + return disableCache(handler) +} diff --git a/gno.land/pkg/gnoweb/static/css/app.css b/gno.land/pkg/gnoweb/static/css/app.css deleted file mode 100644 index c10fc8ec0e0..00000000000 --- a/gno.land/pkg/gnoweb/static/css/app.css +++ /dev/null @@ -1,862 +0,0 @@ -/**** ROBOTO ****/ - -@font-face { - font-family: "Roboto Mono"; - font-style: normal; - font-weight: normal; - font-display: swap; - src: local("Roboto Mono Regular"), url("/static/font/roboto/RobotoMono-Regular.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono"; - font-style: italic; - font-weight: normal; - font-display: swap; - src: local("Roboto Mono Italic"), url("/static/font/roboto/RobotoMono-Italic.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono Bold"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: local("Roboto Mono Bold"), url("/static/font/roboto/RobotoMono-Bold.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono"; - font-style: italic; - font-weight: 700; - font-display: swap; - src: local("Roboto Mono Bold Italic"), url("/static/font/roboto/RobotoMono-BoldItalic.woff") format("woff"); - } - - -/*** DARK/LIGHT THEME COLORS ***/ - -html:not([data-theme="dark"]), -html[data-theme="light"] { - --background-color: #eee; - --input-background-color: #eee; - --text-color: #000; - --link-color: #25172a; - --muted-color: #757575; - --border-color: #d7d9db; - --icon-color: #000; - - --quote-background: #ddd; - --quote-2-background: #aaa4; - --code-background: #d7d9db; - --header-background: #373737; - --header-forground: #ffffff; - --logo-hat: #ffffff; - --logo-beard: #808080; - - --realm-help-background-color: #d7d9db9e; - --realm-help-odd-background-color: #d7d9db45; - --realm-help-code-color: #5d5d5d; - - --highlight-color: #2f3337; - --highlight-bg: #f6f6f6; - --highlight-color: #2f3337; - --highlight-comment: #656e77; - --highlight-keyword: #015692; - --highlight-attribute: #015692; - --highlight-symbol: #803378; - --highlight-namespace: #b75501; - --highlight-keyword: #015692; - --highlight-variable: #54790d; - --highlight-keyword: #015692; - --highlight-literal: #b75501; - --highlight-punctuation: #535a60; - --highlight-variable: #54790d; - --highlight-deletion: #c02d2e; - --highlight-addition: #2f6f44; -} - -html[data-theme="dark"] { - --background-color: #1e1e1e; - --input-background-color: #393939; - --text-color: #c7c7c7; - --link-color: #c7c7c7; - --muted-color: #737373; - --border-color: #606060; - --icon-color: #dddddd; - - --quote-background: #404040; - --quote-2-background: #555555; - --code-background: #606060; - --header-background: #373737; - --header-forground: #ffffff; - --logo-hat: #ffffff; - --logo-beard: #808080; - - --realm-help-background-color: #45454545; - --realm-help-odd-background-color: #4545459e; - --realm-help-code-color: #b6b6b6; - - --highlight-color: #ffffff; - --highlight-bg: #1c1b1b; - --highlight-color: #ffffff; - --highlight-comment: #999999; - --highlight-keyword: #88aece; - --highlight-attribute: #88aece; - --highlight-symbol: #c59bc1; - --highlight-namespace: #f08d49; - --highlight-keyword: #88aece; - --highlight-variable: #b5bd68; - --highlight-keyword: #88aece; - --highlight-literal: #f08d49; - --highlight-punctuation: #cccccc; - --highlight-variable: #b5bd68; - --highlight-deletion: #de7176; - --highlight-addition: #76c490; -} - -.logo-wording path {fill: var(--header-forground, #ffffff); } -.logo-beard { fill: var(--logo-beard, #808080); } -.logo-hat {fill: var(--logo-hat, #ffffff); } - -#theme-toggle { - cursor: pointer; - display: inline-block; - padding: 0; - color: var(--header-forground, #ffffff); -} - -html[data-theme="dark"] #theme-toggle-moon, -html[data-theme="light"] #theme-toggle-sun { - display: none; -} - -/*** BASE HTML ELEMENTS ***/ - -* { - box-sizing: border-box; -} - -html { - font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; - -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; - text-size-adjust: 100%; - -moz-osx-font-smoothing: grayscale; - font-smoothing: antialiased; - font-variant-ligatures: contextual common-ligatures; - font-kerning: normal; - text-rendering: optimizeLegibility; - -moz-text-size-adjust: none; - -webkit-text-size-adjust: none; - text-size-adjust: none; -} - -html, -body { - padding: 0; - margin: 0; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol", "Noto Color Emoji"; background-color: var(--background-color, #eee); - color: var(--text-color, #000); - font-size: 15px; - transition: 0.25s all ease; -} - -h1, -h2, -h3, -h4, -nav { - - font-weight: 600; - letter-spacing: 0.08rem; -} - -:is(h1, h2, h3, h4) a { - text-decoration: none; -} - -h1 { - text-align: center; - font-size: 2rem; - margin-block: 4.2rem 2rem; -} - -h2 { - font-size: 1.625rem; - margin-block: 3.4rem 1.2rem; - line-height: 1.4; -} - -h3 { - font-size: 1.467rem; - margin-block: 2.6rem 1rem; -} - -p { - font-size: 1rem; - margin-block: 1.2rem; - line-height: 1.4; -} - -p:last-child:has(a:only-child) { - margin-block-start: 0.8rem; -} -.stack > p:last-child:has(a:only-child) { - margin-block-start: 0; -} - -hr { - border: none; - height: 1px; - background: var(--border-color, #d7d9db); - width: 100%; - margin-block: 1.5rem 2rem; -} - -nav { - font-weight: 400; -} - -button { - color: var(--text-color, #000); -} - -body { - height: 100%; - width: 100%; -} - -input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -a { - color: var(--link-color, #25172a); -} - -a[href="#"] { - color: var(--muted-color, #757575); -} - -.gno-tmpl-section ul { - padding: 0; -} - -.gno-tmpl-section li , -#header li , -.footer li { - list-style: none; -} - -.gno-tmpl-section blockquote { - margin-inline: 0; -} - -li { - margin-bottom: 0.4rem; -} - -li > * { - vertical-align: middle; -} - -input { - background-color: var(--input-background-color, #eee); - border: 1px solid var(--border-color); - color: var(--text-color, #000); - width: 25em; - padding: 0.4rem 0.5rem; - max-width: 100%;x -} - -blockquote { - background-color: var(--quote-background, #ddd); -} - -blockquote blockquote { - margin: 0; - background-color: var(--quote-2-background, #aaa4); -} - -pre, code { - font-family: "Roboto Mono", "Courier New", "sans-serif"; -} -pre { - background-color: var(--code-background, #d7d9db); - margin: 0; - padding: 0.5rem; -} - -label { - margin-block-end: 0.8rem; - display: block; -} - -label > img { - margin-inline-end: 0.8rem; -} - -code { - white-space: pre-wrap; - overflow-wrap: anywhere; -} -/*** COMPOSITION ***/ -.container { - width: 100%; - max-width: 63.75rem; - margin: auto; - padding: 1.25rem; -} - -.container p > img:only-child { - max-width: 100%; -} -.gno-tmpl-page p img:only-child { - margin-inline: auto; - display: block; - max-width: 100%; -} - -.inline-list { - padding: 1rem; - display: flex; - justify-content: space-between; -} - - - -.stack, -.stack > p { - display: flex; - flex-direction: column; -} - -.stack > p { - margin: 0; -} - -.stack > a, -.stack > p > a{ - margin-block-end: 0.4rem; -} - -.column > h1, -.column > h2, -.column > h3, -.column > h4, -.column > h5, -.column > h6 { - margin-block-start: 0; -} - -.columns-2, -.columns-3 { - display: grid; - grid-template-columns: repeat(1, 1fr); - grid-gap: 3.75rem; - margin: 3.75rem auto; -} - -.footer { - text-align: center; - margin-block-start: 2rem; - background-color: var(--header-background, #d7d9db); - border-top: 1px solid var(--border-color); -} - -.footer > .logo { - display: inline-block; - margin: 1rem; - height: 1.2rem; -} - -/** 51.2rem **/ -@media screen and (min-width: 68.75rem) { - .stack, - .stack > p { - flex-direction: row; - } - .stack *:not(:first-child) { - margin-left: 3.75rem; - } - .stack > a, - .stack > p > a{ - margin-block-end: 0; - } - .columns-2 { - grid-template-columns: repeat(2, 1fr); - } - .columns-3 { - grid-template-columns: repeat(3, 1fr); - } -} - -/*** UTILITIES ***/ - -.is-hidden { - display: none; -} - -.is-muted { - color: var(--muted-color, #757575); -} - -.is-finished { - text-decoration: line-through; -} - -.is-underline { - text-decoration: underline; -} - -/*** BLOCKS ***/ -.tabs button { - border: none; - cursor: pointer; - text-decoration: underline; - padding: 0; - background: none; - color: var(--text-color, #000); -} - -.tabs button[aria-selected="true"] { - font-weight: 700; -} - -.tabs + .jumbotron { - margin-top: 2.5rem; -} -.tabs > .columns-2, -.tabs > .columns-3 { - margin-bottom: 2.5rem; -} - -.accordion-trigger { - display: block; - border: none; - cursor: pointer; - padding: 0.4rem 0; - font-size: 1.125rem; - font-weight: 700; - text-align: left; - background: none; -} - -.accordion-trigger ~ div { - padding: 0.875rem 0 2.2rem; -} - -.accordion > p { - margin-block: 0; -} -/** 51.2rem **/ -@media screen and (min-width: 68.75rem) { - .accordion .accordion-trigger ~ div { - padding: 0.875rem 0 2.2rem 2rem; - } -} - -.gor-accordion button::first-letter { - font-size: 1.5em; - color: var(--text-color, #000); -} - -.jumbotron { - border: 1px solid var(--border-color, #d7d9db); - padding: 1.4rem; - margin: 3.75rem auto; -} - -.jumbotron h1 { - text-align: left; -} - -.jumbotron > *:first-child, -.jumbotron > * > *:first-child { - margin-block-start: 0; -} - -.jumbotron > *:last-child, -.jumbotron > * > *:last-child { - margin-block-end: 0; -} - -/** 68.75rem**/ -@media screen and (min-width: 68.75rem) { - .jumbotron { - margin: 3.75rem -3.5rem; - padding: 3.5rem; - } -} - -#root { - display: flex; - flex-direction: column; - border: 1px solid var(--header-background, #d7d9db); - margin: 20px; - overflow: hidden; - /* height: calc(100vh - 40px); */ -} - -#header { - position: relative; - background-color: var(--header-background, #d7d9db); - padding: 1.333rem; - display: flex; - align-items: center; - justify-content: space-between; -} - -#header > nav { - flex-grow: 2; -} - -#header .logo { - display: flex; - align-items: center; - color: var(--link-color, #25172a); - position: absolute; - height: 2.4rem; - z-index: 2; -} - -.logo > svg { - height: 100%; -} - -#logo_path a { - text-decoration: none; -} - -#logo_path { - padding-right: 0.8rem; -} - -#logo_path a:hover { - text-decoration: underline; -} - -#realm_links a { - font-size: 0.8rem; -} - -#header_buttons { - position: relative; - width: 100%; - height: 3rem; -} - -#header_buttons nav { - height: 100%; - display: flex; - justify-content: flex-end; - align-items: center; -} - -/* enabled conditionally with