diff --git a/examples/Makefile b/examples/Makefile index 5075df198ac..1a39d3288db 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -37,3 +37,45 @@ clean: GOFMT_FLAGS ?= -w fmt: go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt $(GOFMT_FLAGS) `find . -name "*.gno"` + +.PHONY: create.pkg +create.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -pkgdir="." \ + -pkgpath="gno.land/r/demo/social_feeds_v3" \ + -chainid "teritori-1" \ + -remote "https://testnet.gno.teritori.com:26657" \ + test1 + +.PHONY: create.feed +create.feed: + gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds_v4" \ + -func "CreateFeed" \ + -gas-fee 1000000ugnot \ + -gas-wanted 3000000 \ + -send "" \ + -broadcast \ + -args "teritori" \ + -chainid "teritori-1" \ + -remote "https://testnet.gno.teritori.com:26657" \ + test1 + +.PHONE: create.post +create.post: + gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds_v3" \ + -func "CreatePost" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "" \ + -broadcast \ + -args "1" \ + -args "0" \ + -args "2" \ + -args '{"gifs": [], "files": [], "title": "", "message": "Hello world !", "hashtags": [], "mentions": [], "createdAt": "2023-08-03T01:39:45.522Z", "updatedAt": "2023-08-03T01:39:45.522Z"}' \ + test1 \ No newline at end of file diff --git a/examples/gno.land/p/demo/binutils/binutils.gno b/examples/gno.land/p/demo/binutils/binutils.gno new file mode 100644 index 00000000000..bc76dd3d3b1 --- /dev/null +++ b/examples/gno.land/p/demo/binutils/binutils.gno @@ -0,0 +1,34 @@ +package binutils + +import ( + "encoding/binary" + "errors" +) + +var ErrInvalidLengthPrefixedString = errors.New("invalid length-prefixed string") + +func EncodeLengthPrefixedStringUint16BE(s string) []byte { + b := make([]byte, 2+len(s)) + binary.BigEndian.PutUint16(b, uint16(len(s))) + copy(b[2:], s) + return b +} + +func DecodeLengthPrefixedStringUint16BE(b []byte) (string, []byte, error) { + if len(b) < 2 { + return "", nil, ErrInvalidLengthPrefixedString + } + l := binary.BigEndian.Uint16(b) + if len(b) < 2+int(l) { + return "", nil, ErrInvalidLengthPrefixedString + } + return string(b[2 : 2+l]), b[l+2:], nil +} + +func MustDecodeLengthPrefixedStringUint16BE(b []byte) (string, []byte) { + s, r, err := DecodeLengthPrefixedStringUint16BE(b) + if err != nil { + panic(err) + } + return s, r +} diff --git a/examples/gno.land/p/demo/binutils/gno.mod b/examples/gno.land/p/demo/binutils/gno.mod new file mode 100644 index 00000000000..64fe08ae523 --- /dev/null +++ b/examples/gno.land/p/demo/binutils/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/binutils \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/core_v6/dao_core.gno b/examples/gno.land/p/demo/daodao/core_v6/dao_core.gno new file mode 100644 index 00000000000..1b7c18c5ceb --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core_v6/dao_core.gno @@ -0,0 +1,70 @@ +package core + +import ( + "std" + "strings" + + dao_interfaces "gno.land/p/demo/daodao/interfaces_v6" + "gno.land/p/demo/markdown_utils" +) + +// TODO: add wrapper message handler to handle multiple proposal modules messages + +type IDAOCore interface { + AddProposalModule(proposalMod dao_interfaces.IProposalModule) + + VotingModule() dao_interfaces.IVotingModule + ProposalModules() []dao_interfaces.IProposalModule + + Render(path string) string +} + +type daoCore struct { + IDAOCore + + votingModule dao_interfaces.IVotingModule + proposalModules []dao_interfaces.IProposalModule +} + +func NewDAOCore( + votingModule dao_interfaces.IVotingModule, + proposalModules []dao_interfaces.IProposalModule, +) IDAOCore { + return &daoCore{ + votingModule: votingModule, + proposalModules: proposalModules, + } +} + +func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { + return d.votingModule +} + +func (d *daoCore) ProposalModules() []dao_interfaces.IProposalModule { + return d.proposalModules +} + +func (d *daoCore) AddProposalModule(proposalMod dao_interfaces.IProposalModule) { + d.proposalModules = append(d.proposalModules, proposalMod) +} + +func (d *daoCore) Render(path string) string { + s := "# DAO Core\n" + s += "This is a port of [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 { + s += markdown_utils.Indent(propMod.Render(path)) + "\n" + } + return s +} + +func GetProposalModule(core IDAOCore, moduleIndex int) dao_interfaces.IProposalModule { + if moduleIndex < 0 { + panic("Module index must be >= 0") + } + mods := core.ProposalModules() + if moduleIndex >= len(mods) { + panic("invalid module index") + } + return mods[moduleIndex] +} diff --git a/examples/gno.land/p/demo/daodao/core_v6/gno.mod b/examples/gno.land/p/demo/daodao/core_v6/gno.mod new file mode 100644 index 00000000000..3473310149c --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core_v6/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/demo/daodao/core_v6 + +require ( + "gno.land/p/demo/daodao/interfaces_v6" 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/interfaces_v6/dao_interfaces.gno b/examples/gno.land/p/demo/daodao/interfaces_v6/dao_interfaces.gno new file mode 100644 index 00000000000..802c2e0228e --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v6/dao_interfaces.gno @@ -0,0 +1,172 @@ +package dao_interfaces + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/jsonutil_v4" +) + +type IVotingModule interface { + VotingPower(addr std.Address) uint64 + TotalPower() uint64 + Render(path string) string +} + +type Ballot struct { + Power uint64 + Vote Vote + Rationale string +} + +func (b Ballot) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {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 jsonutil.FormatObject([]jsonutil.KeyValue{ + {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 + Messages []ExecutableMessage + Ballots *avl.Tree // dev + // Ballots *avl.MutTree // test3 + Votes Votes + Status ProposalStatus +} + +var _ jsonutil.JSONAble = (*Proposal)(nil) + +func (p Proposal) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "id", Value: p.ID}, + {Key: "title", Value: p.Title}, + {Key: "description", Value: p.Description}, + {Key: "proposer", Value: p.Proposer}, + {Key: "messages", Value: jsonutil.FormatSlice(p.Messages), Raw: true}, + {Key: "ballots", Value: p.Ballots}, + {Key: "votes", Value: p.Votes}, + {Key: "status", Value: p.Status}, + }) +} + +type ProposalStatus int + +const ( + ProposalStatusOpen ProposalStatus = iota + ProposalStatusPassed + ProposalStatusExecuted +) + +func (p ProposalStatus) ToJSON() string { + return jsonutil.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 jsonutil.FormatString(v.String()) +} + +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)) + ")" + } +} + +type IProposalModule interface { + Propose( + title string, + description string, + actions []ExecutableMessage, + ) + Vote(proposalId int, vote Vote, rationale string) + Execute(proposalId int) + Threshold() Threshold + + Proposals() []Proposal + GetBallot(proposalId int, addr std.Address) Ballot + + Render(path string) string +} + +type ExecutableMessage interface { + String() string + Binary() []byte + Type() string +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v6/dao_messages.gno b/examples/gno.land/p/demo/daodao/interfaces_v6/dao_messages.gno new file mode 100644 index 00000000000..8ab887157dd --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v6/dao_messages.gno @@ -0,0 +1,66 @@ +package dao_interfaces + +import ( + "encoding/base64" + "encoding/binary" + "strings" + + "gno.land/p/demo/avl" +) + +type MessageHandler interface { + Execute(message ExecutableMessage) + FromBinary(b []byte) ExecutableMessage + Type() string +} + +type MessagesRegistry struct { + handlers *avl.Tree +} + +func NewMessagesRegistry() *MessagesRegistry { + return &MessagesRegistry{handlers: avl.NewTree()} +} + +func (r *MessagesRegistry) Register(handler MessageHandler) { + r.handlers.Set(handler.Type(), handler) +} + +func (r *MessagesRegistry) FromBinary(b []byte) ExecutableMessage { + if len(b) < 2 { + panic("invalid ExecutableMessage: invalid length") + } + l := binary.BigEndian.Uint16(b[:2]) + if len(b) < int(l+2) { + panic("invalid ExecutableMessage: invalid length") + } + t := string(b[2 : l+2]) + + h, ok := r.handlers.Get(t) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + return h.(MessageHandler).FromBinary(b) +} + +func (r *MessagesRegistry) FromBase64String(s string) ExecutableMessage { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + panic("invalid ExecutableMessage: invalid base64 string") + } + return r.FromBinary(b) +} + +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) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v6/gno.mod b/examples/gno.land/p/demo/daodao/interfaces_v6/gno.mod new file mode 100644 index 00000000000..de9f80ff57c --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v6/gno.mod @@ -0,0 +1,5 @@ +module gno.land/p/demo/daodao/interfaces_v6 + +require ( + "gno.land/p/demo/jsonutil_v4" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/interfaces_v6/proposal_test.gno b/examples/gno.land/p/demo/daodao/interfaces_v6/proposal_test.gno new file mode 100644 index 00000000000..d31b3658036 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v6/proposal_test.gno @@ -0,0 +1,59 @@ +package dao_interfaces + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/jsonutil_v4" +) + +type NoopMessage struct{} + +var _ ExecutableMessage = (*NoopMessage)(nil) + +func (m NoopMessage) String() string { + return "noop" +} + +func (m NoopMessage) Binary() []byte { + return nil +} + +func (m NoopMessage) Type() string { + return "noop-type" +} + +func (m NoopMessage) ToJSON() string { + return jsonutil.FormatString(m.String()) +} + +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: []ExecutableMessage{NoopMessage{}, NoopMessage{}, NoopMessage{}}, + }, + } + props[0].Ballots.Set("0x1234567890", Ballot{Power: 1, Vote: VoteYes, Rationale: "test"}) + str := jsonutil.FormatSlice(props) + expected := `[{"id":0,"title":"Prop #0","description":"Wolol0\n\t\r","proposer":"0x1234567890","messages":[],"ballots":{"0x1234567890":{"power":1,"vote":"Yes","rationale":"test"}},"votes":{"yes":7,"no":21,"abstain":42},"status":"Open"},{"id":1,"title":"Prop #1","description":"Wolol1\\\"","proposer":"0x1234567890","messages":["noop","noop","noop"],"ballots":{},"votes":{"yes":0,"no":0,"abstain":0},"status":"Executed"}]` + if expected != str { + t.Fatalf("JSON does not match, expected %s, got %s", expected, str) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v6/threshold.gno b/examples/gno.land/p/demo/daodao/interfaces_v6/threshold.gno new file mode 100644 index 00000000000..7b34ba7b4d2 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v6/threshold.gno @@ -0,0 +1,36 @@ +package dao_interfaces + +import ( + "strconv" +) + +type Percent uint16 // 4 decimals fixed point + +type PercentageThreshold struct { + Percent *Percent +} + +func (p *PercentageThreshold) String() string { + if p == nil || p.Percent == nil { + return "nil" + } + return p.Percent.String() +} + +type ThresholdQuorum struct { + Threshold PercentageThreshold + Quorum PercentageThreshold +} + +type Threshold struct { + ThresholdQuorum *ThresholdQuorum +} + +func (p Percent) String() string { + s := strconv.FormatUint(uint64(p)/100, 10) + decPart := uint64(p) % 100 + if decPart != 0 { + s += "." + strconv.FormatUint(decPart, 10) + } + return s + "%" +} diff --git a/examples/gno.land/p/demo/daodao/proposal_single_v7/dao_proposal_single.gno b/examples/gno.land/p/demo/daodao/proposal_single_v7/dao_proposal_single.gno new file mode 100644 index 00000000000..6fbf2a45f14 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single_v7/dao_proposal_single.gno @@ -0,0 +1,305 @@ +package dao_proposal_single + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + dao_core "gno.land/p/demo/daodao/core_v6" + dao_interfaces "gno.land/p/demo/daodao/interfaces_v6" + "gno.land/p/demo/jsonutil_v4" +) + +type DAOProposalSingleOpts struct { + /// The threshold a proposal must reach to complete. + Threshold dao_interfaces.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 + + Registry *dao_interfaces.MessagesRegistry +} + +type daoProposalSingle struct { + dao_interfaces.IProposalModule + + core dao_core.IDAOCore + opts *DAOProposalSingleOpts + proposals []dao_interfaces.Proposal +} + +func NewDAOProposalSingle(core dao_core.IDAOCore, opts *DAOProposalSingleOpts) *daoProposalSingle { + if core == nil { + panic("core cannot be nil") + } + + if opts == nil { + panic("opts cannot be nil") + } + + if opts.Registry == nil { + panic("opts.Registry 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 + threshold := opts.Threshold.ThresholdQuorum + if threshold == nil { + panic("opts.Threshold must be of type ThresholdQuorum") + } + + thresholdPercent := threshold.Threshold.Percent + if thresholdPercent == nil { + panic("opts.Threshold.Threshold must be of type Percent") + } + if *thresholdPercent > 10000 { + panic("opts.Threshold.Threshold must be <= 100%") + } + + quorumPercent := threshold.Quorum.Percent + if quorumPercent == nil { + panic("opts.Threshold.Quorum must be of type Percent") + } + if *quorumPercent > 10000 { + panic("opts.Threshold.Quorum must be <= 100%") + } + + 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 := "" + if threshold := d.opts.Threshold.ThresholdQuorum; threshold != nil { + thresholdStr = "Threshold: " + threshold.Threshold.Percent.String() + "\n\n" + + "Quorum: " + threshold.Quorum.Percent.String() + } + + 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" + + // /* dev + p.Ballots.Iterate("", "", func(k string, v interface{}) bool { + ballot := v.(dao_interfaces.Ballot) + proposalsStr += "- " + k + " voted " + ballot.Vote.String() + "\n" + return false + }) + // */ + + /* test3 + ballotsCount := p.Ballots.Size() + for i := 0; i < ballotsCount; i++ { + k, v := p.Ballots.GetByIndex(i) + ballot := v.(dao_interfaces.Ballot) + proposalsStr += "- " + k + " voted " + ballot.Vote.String() + "\n" + } + */ + + 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) Propose(title string, description string, messages []dao_interfaces.ExecutableMessage) { + // TODO: auth + d.proposals = append(d.proposals, dao_interfaces.Proposal{ + ID: len(d.proposals), + Title: title, + Description: description, + Messages: messages, + Proposer: std.GetOrigCaller(), + Ballots: avl.NewTree(), // dev + // Ballots: avl.NewMutTree(), // test3 + Status: dao_interfaces.ProposalStatusOpen, + }) +} + +func (d *daoProposalSingle) GetBallot(proposalID int, memberAddress std.Address) dao_interfaces.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.(dao_interfaces.Ballot) +} + +func (d *daoProposalSingle) Vote(proposalID int, vote dao_interfaces.Vote, rationale string) { + voter := std.GetOrigCaller() + + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + proposal := d.proposals[proposalID] + // TODO: check proposal expiration + + votePower := d.core.VotingModule().VotingPower(voter) + if votePower == 0 { + panic("you're not a member") + } + + // TODO: handle revoting + if ok := proposal.Ballots.Has(voter.String()); ok { + panic("you already voted") + } + proposal.Ballots.Set(voter.String(), dao_interfaces.Ballot{ + Vote: vote, + Power: votePower, + Rationale: rationale, + }) + + proposal.Votes.Add(vote, votePower) + + d.updateStatus(proposalID) +} + +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] + + d.updateStatus(proposalID) + if prop.Status != dao_interfaces.ProposalStatusPassed { + panic("proposal is not passed") + } + + d.opts.Registry.ExecuteMessages(prop.Messages) + + d.proposals[proposalID].Status = dao_interfaces.ProposalStatusExecuted +} + +// FIXME: should probably return a copy for safety +func (d *daoProposalSingle) Proposals() []dao_interfaces.Proposal { + return d.proposals +} + +func (d *daoProposalSingle) ProposalsJSON() string { + return jsonutil.FormatSlice(d.proposals) +} + +func (d *daoProposalSingle) Threshold() dao_interfaces.Threshold { + return d.opts.Threshold +} + +func (d *daoProposalSingle) updateStatus(proposalID int) { + proposal := d.proposals[proposalID] + if proposal.Status == dao_interfaces.ProposalStatusOpen && d.isPassed(proposalID) { + d.proposals[proposalID].Status = dao_interfaces.ProposalStatusPassed + return + } +} + +func (d *daoProposalSingle) isPassed(proposalID int) bool { + proposal := d.proposals[proposalID] + + // TODO: support other threshold types + threshold := d.opts.Threshold.ThresholdQuorum.Threshold + quorum := d.opts.Threshold.ThresholdQuorum.Quorum + + totalPower := d.core.VotingModule().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) +} + +func doesVoteCountPass(yesVotes uint64, options uint64, percent dao_interfaces.PercentageThreshold) bool { + if options == 0 { + return false + } + percentValue := uint64(*percent.Percent) + votes := yesVotes * 10000 + threshold := options * percentValue + return votes >= threshold +} diff --git a/examples/gno.land/p/demo/daodao/proposal_single_v7/gno.mod b/examples/gno.land/p/demo/daodao/proposal_single_v7/gno.mod new file mode 100644 index 00000000000..34afedf02c8 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single_v7/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/daodao/proposal_single_v7 + +require ( + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/p/demo/daodao/interfaces_v6" v0.0.0-latest + "gno.land/p/demo/daodao/core_v6" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/proposal_single_v7/update_settings.gno b/examples/gno.land/p/demo/daodao/proposal_single_v7/update_settings.gno new file mode 100644 index 00000000000..5fb898c21dc --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single_v7/update_settings.gno @@ -0,0 +1,114 @@ +package dao_proposal_single + +import ( + "encoding/binary" + "strings" + + "gno.land/p/demo/daodao/interfaces_v6" +) + +type UpdateSettingsMessage struct { + dao_interfaces.ExecutableMessage + + Threshold *dao_interfaces.Threshold +} + +func (usm *UpdateSettingsMessage) Type() string { + return "UpdateSettings" +} + +func (usm *UpdateSettingsMessage) String() string { + ss := []string{usm.Type()} + if usm.Threshold != nil { + ss = append(ss, "Threshold type: ThresholdQuorum\nThreshold: "+usm.Threshold.ThresholdQuorum.Threshold.String()+"\nQuorum: "+usm.Threshold.ThresholdQuorum.Quorum.String()) + } + return strings.Join(ss, "\n--\n") +} + +func (usm *UpdateSettingsMessage) Binary() []byte { + b := []byte{} + + t := usm.Type() + b = binary.BigEndian.AppendUint16(b, uint16(len(t))) + b = append(b, []byte(t)...) + + if usm.Threshold != nil { + b = append(b, 1) + b = binary.BigEndian.AppendUint16(b, uint16(usm.Threshold.ThresholdQuorum.Threshold.Percent)) + b = binary.BigEndian.AppendUint16(b, uint16(usm.Threshold.ThresholdQuorum.Quorum.Percent)) + } else { + b = append(b, 0) + } + + return b +} + +func UpdateSettingsMessageFromBinary(b []byte) *UpdateSettingsMessage { + usm := UpdateSettingsMessage{} + + if len(b) < 2 { + panic("invalid length - less than 2") + } + l := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(l) { + panic("invalid length - less than expected") + } + t := string(b[:l]) + if t != usm.Type() { + panic("invalid type") + } + b = b[l:] + + hasThreshold := b[0] == 1 + b = b[1:] + if hasThreshold { + if len(b) < 4 { + panic("invalid length - less than 4") + } + threshold := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + quorum := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + + // TODO: validate threshold and quorum + + pt := dao_interfaces.Percent(threshold) + pq := dao_interfaces.Percent(quorum) + + usm.Threshold = &dao_interfaces.Threshold{ + ThresholdQuorum: &dao_interfaces.ThresholdQuorum{ + Threshold: dao_interfaces.PercentageThreshold{Percent: &pt}, + Quorum: dao_interfaces.PercentageThreshold{Percent: &pq}, + }, + } + } + + return &usm +} + +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) + + if usm.Threshold != nil { + h.mod.opts.Threshold = *usm.Threshold + } +} + +func (h *updateSettingsHandler) Type() string { + return UpdateSettingsMessage{}.Type() +} + +func (h *updateSettingsHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return UpdateSettingsMessageFromBinary(b) +} diff --git a/examples/gno.land/p/demo/daodao/voting_group_v8/gno.mod b/examples/gno.land/p/demo/daodao/voting_group_v8/gno.mod new file mode 100644 index 00000000000..6d92ae1e427 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group_v8/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/daodao/voting_group_v8 + +require ( + "gno.land/p/demo/daodao/interfaces_v6" v0.0.0-latest + "gno.land/p/demo/markdown_utils" v0.0.0-latest + "gno.land/r/demo/groups_v9" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/voting_group_v8/voting_group.gno b/examples/gno.land/p/demo/daodao/voting_group_v8/voting_group.gno new file mode 100644 index 00000000000..6e4466e4513 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group_v8/voting_group.gno @@ -0,0 +1,42 @@ +package dao_voting_group + +import ( + "std" + + dao_interfaces "gno.land/p/demo/daodao/interfaces_v6" + "gno.land/p/demo/markdown_utils" + "gno.land/r/demo/groups_v9" +) + +type GRC4Voting struct { + dao_interfaces.IVotingModule + + groupID groups.GroupID +} + +func NewGRC4Voting(groupID groups.GroupID) dao_interfaces.IVotingModule { + return &GRC4Voting{groupID: groupID} +} + +func (v *GRC4Voting) VotingPower(addr std.Address) uint64 { + return uint64(groups.GetMemberWeightByAddress(v.groupID, addr)) +} + +func (v *GRC4Voting) TotalPower() uint64 { + return uint64(groups.GetGroupTotalWeight(v.groupID)) +} + +func (v *GRC4Voting) 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 *GRC4Voting) GetGroupID() groups.GroupID { + return v.groupID +} diff --git a/examples/gno.land/p/demo/daodao/voting_group_v8/voting_group_test.gno b/examples/gno.land/p/demo/daodao/voting_group_v8/voting_group_test.gno new file mode 100644 index 00000000000..2cad8a2e287 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group_v8/voting_group_test.gno @@ -0,0 +1,19 @@ +package grc4 + +import ( + "std" + "testing" +) + +func Test(t *testing.T) { + { + admin := "g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a" + g := grc4.NewGRC4Group(std.Address(admin), nil) + v := NewGRC4Voting(g) + got := v.TotalPower() + 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..72286b21b6a --- /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)) { + panic("flag ID not found") + } + + // 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/jsonutil_v4/gno.mod b/examples/gno.land/p/demo/jsonutil_v4/gno.mod new file mode 100644 index 00000000000..feaf840dc98 --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v4/gno.mod @@ -0,0 +1,5 @@ +module gno.land/p/demo/jsonutil_v4 + +require ( + "gno.land/p/demo/avl" v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/jsonutil_v4/json_test.gno b/examples/gno.land/p/demo/jsonutil_v4/json_test.gno new file mode 100644 index 00000000000..f32456f6a15 --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v4/json_test.gno @@ -0,0 +1,118 @@ +package jsonutil + +import ( + "strings" + "testing" +) + +func TestAST(t *testing.T) { + json := `{"a":[42, null, true, false, "hello"],"b":3.0,"c":{"ia":{}, "ib":{ "foo" : "bar"}},"d":4,"e":5}` + tokens := tokenize(json) + expected := 44 + if len(tokens) != expected { + t.Errorf("Expected %d tokens, got %d", expected, len(tokens)) + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) != 0 { + t.Errorf("Expected 0 remaining tokens, got %d", len(remainingTokens)) + } + if ast.Kind != JSONKindObject { + t.Errorf("Expected root node to be an object, got %s", ast.Kind) + } + expectedTree := `{"a":[42,null,true,false,"hello"],"b":3.0,"c":{"ia":{},"ib":{"foo":"bar"}},"d":4,"e":5}` + if JSONASTNodeString(ast) != expectedTree { + t.Errorf("Expected root node to be `%s`, got `%s`", expectedTree, JSONASTNodeString(ast)) + } +} + +type TestType struct { + A []string `json:"a"` + B float64 `json:"b"` + C SubTestType + D uint `json:"d"` + E int `json:"e"` + F bool `json:"f"` + G *EmptyType `json:"g"` +} + +func (tt *TestType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{ + {Key: "a", ArrayParser: func(children []*JSONASTNode) { + tt.A = make([]string, len(children)) + for i, child := range children { + ParseASTAny(child, &tt.A[i]) + } + }}, + {Key: "b", Value: &tt.B}, + {Key: "c", Value: &tt.C}, + {Key: "d", Value: &tt.D}, + {Key: "e", Value: &tt.E}, + {Key: "f", Value: &tt.F}, + {Key: "g", Value: &tt.G}, + }) +} + +type SubTestType struct { + IA EmptyType `json:"ia"` + IB SubSubTestType `json:"ib"` +} + +func (stt *SubTestType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{ + {Key: "ia", Value: &stt.IA}, + {Key: "ib", Value: &stt.IB}, + }) +} + +type EmptyType struct{} + +func (et *EmptyType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{}) +} + +type SubSubTestType struct { + Foo string `json:"foo"` +} + +func (sstt *SubSubTestType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{ + {Key: "foo", Value: &sstt.Foo}, + }) +} + +func TestParse(t *testing.T) { + json := `{"a":["42", "null", "true", "false", "hello"],"b":3.0,"c":{"ia":{}, "ib":{ "foo" : "bar"}},"d":4,"e":5, "f": true, "g": null}` + var tt TestType + ParseAny(json, &tt) + + if len(tt.A) != 5 { + t.Errorf("Expected A to have 5 elements, got %d", len(tt.A)) + } + expected := "42, null, true, false, hello" + if strings.Join(tt.A, ", ") != expected { + t.Errorf("Expected A to be `%s`, got `%s`", expected, tt.A[0]) + } + + if tt.B != 42.1 { // FIXME: 3.0 + t.Errorf("Expected B to be 3.0, got %f", tt.B) + } + + if tt.D != 4 { + t.Errorf("Expected D to be 4, got %d", tt.D) + } + + if tt.E != 5 { + t.Errorf("Expected E to be 5, got %d", tt.E) + } + + if !tt.F { + t.Errorf("Expected F to be true, got false") + } + + /* + BUG?: tt.G == instead of nil + if tt.G != nil { + t.Errorf("Expected G to be nil, got %v", tt.G) + } + */ +} diff --git a/examples/gno.land/p/demo/jsonutil_v4/jsonutil.gno b/examples/gno.land/p/demo/jsonutil_v4/jsonutil.gno new file mode 100644 index 00000000000..51fdc369b0e --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v4/jsonutil.gno @@ -0,0 +1,180 @@ +package jsonutil + +// This package strives to have the same behavior as json.Marshal but has no support for nested slices and returns strings + +import ( + "std" + "strconv" + "strings" + "unicode/utf8" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +type JSONAble interface { + ToJSON() string +} + +type KeyValue 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 bool: + return FormatBool(p.(bool)) + default: + return "null" + } +} + +// 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 +} + +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 []KeyValue) 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([]KeyValue, 0, t.Size()) + t.Iterate("", "", func(key string, value interface{}) bool { + kv = append(kv, KeyValue{key, value, false}) + return false + }) + return FormatObject(kv) +} diff --git a/examples/gno.land/p/demo/jsonutil_v4/parse.gno b/examples/gno.land/p/demo/jsonutil_v4/parse.gno new file mode 100644 index 00000000000..5ca82a9bdc9 --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v4/parse.gno @@ -0,0 +1,580 @@ +package jsonutil + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" +) + +// 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 ParseASTAny(ast *JSONASTNode, ptr *interface{}) { + switch ptr.(type) { + case *std.Address: + *ptr.(*std.Address) = std.Address(ParseString(ast.Value)) + case **avl.Tree: + panic("avl not implememented") + // *ptr.(**avl.Tree) = ParseAVLTree(s) + case *avl.Tree: + panic("avl ptr not implememented") + // *ptr.(*avl.Tree) = *ParseAVLTree(s) + case *string: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindString { + panic("not a string") + } + *ptr.(*string) = ParseString(ast.Value) // TODO: real unescaping + 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: + *ptr.(*float64) = 42.1 + case *float32: + *ptr.(*float32) = 21.1 + 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 + default: + if ast.Kind == JSONKindValue && ast.ValueKind == JSONTokenKindNull { + *ptr = nil + return + } + panic("type not defined for `" + JSONASTNodeString(ast) + "`") + } +} + +func ParseString(s string) string { + if (len(s) < 2) || (s[0] != '"') || (s[len(s)-1] != '"') { + panic("invalid string") + } + return s[1 : len(s)-1] // TODO: real unescaping +} + +func ParseUint64(s string) uint64 { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return uint64(val) +} + +/* + +func ParseFloat64(s string) float64 { + val, err := strconv.ParseFloat(s, 64) + if err != nil { + panic(err) + } + return val +} + +func ParseFloat32(s string) float32 { + val, err := strconv.ParseFloat(s, 32) + if err != nil { + panic(err) + } + return float32(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) +} + +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") + } + ParseASTAny(ast, val) +} + +func ParseObjectAST(ast *JSONASTNode, 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 { + ParseASTAny(child.Value, elem.Value) + } + break + } + if i == (len(ast.ObjectChildren) - 1) { + panic("invalid key `" + elem.Key + "` in object `" + JSONASTNodeString(ast) + "`") + } + } + } +} + +func ParseSlice(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 ParseSliceAST(ast) +} + +func ParseSliceAST(ast *JSONASTNode) []*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 JSONASTNodeString(node *JSONASTNode) 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 += JSONASTNodeString(child) + } + s += "]" + return s + case JSONKindObject: + s := "{" + for i, child := range node.ObjectChildren { + if i > 0 { + s += "," + } + s += `"` + child.Key + `":` + JSONASTNodeString(child.Value) + } + 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) { + /* + defer func() { + println("result:", JSONASTNodeString(tree)) + }() + println("parseAST:", JSONTokensString(tokens)) + */ + + 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 tokenizeOne(s string) (string, *JSONToken) { + if len(s) == 0 { + panic("invalid token") + } + if strings.ContainsRune(whitespaces, int32(s[0])) { + spacesCount := countWhitespaces(s) + 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") + } + for i := 1; i < len(s); i++ { + if s[i] == '"' { // FIXME: real unescaping + return s[i+1:], &JSONToken{Kind: JSONTokenKindString, Raw: s[:i+1]} + } + } + 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/jsonutil_v4/tables.gno b/examples/gno.land/p/demo/jsonutil_v4/tables.gno new file mode 100644 index 00000000000..e761c1faa2f --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v4/tables.gno @@ -0,0 +1,216 @@ +package jsonutil + +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