From 25c844b2511d1946cfc2adc4d8d77e7e6d57a4cd Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Wed, 5 Jul 2023 16:10:05 +0200 Subject: [PATCH 1/7] feat: da0-da0 port + moderated boards Signed-off-by: Norman Meier --- .../gno.land/p/demo/binutils/binutils.gno | 34 + examples/gno.land/p/demo/binutils/gno.mod | 1 + examples/gno.land/p/demo/cpavl/Makefile | 30 + examples/gno.land/p/demo/cpavl/gno.mod | 1 + examples/gno.land/p/demo/cpavl/node.gno | 463 +++++++++ examples/gno.land/p/demo/cpavl/node_test.gno | 100 ++ examples/gno.land/p/demo/cpavl/tree.gno | 82 ++ .../gno.land/p/demo/cpavl/z_0_filetest.gno | 348 +++++++ .../gno.land/p/demo/cpavl/z_1_filetest.gno | 373 +++++++ .../gno.land/p/demo/cpavl/z_2_filetest.gno | 292 ++++++ examples/gno.land/p/demo/daodao/core/Makefile | 30 + .../gno.land/p/demo/daodao/core/dao_core.gno | 70 ++ examples/gno.land/p/demo/daodao/core/gno.mod | 6 + .../p/demo/daodao/interfaces/Makefile | 30 + .../demo/daodao/interfaces/dao_interfaces.gno | 132 +++ .../p/demo/daodao/interfaces/dao_messages.gno | 66 ++ .../gno.land/p/demo/daodao/interfaces/gno.mod | 1 + .../p/demo/daodao/interfaces/threshold.gno | 36 + .../p/demo/daodao/proposal_single/Makefile | 36 + .../proposal_single/dao_proposal_single.gno | 300 ++++++ .../p/demo/daodao/proposal_single/gno.mod | 7 + .../proposal_single/update_settings.gno | 114 ++ .../p/demo/daodao/voting_group/Makefile | 36 + .../p/demo/daodao/voting_group/gno.mod | 7 + .../demo/daodao/voting_group/voting_group.gno | 42 + .../daodao/voting_group/voting_group_test.gno | 19 + examples/gno.land/p/demo/flags_index/Makefile | 36 + .../p/demo/flags_index/flags_index.gno | 152 +++ examples/gno.land/p/demo/flags_index/gno.mod | 5 + .../gno.land/p/demo/markdown_utils/Makefile | 30 + .../gno.land/p/demo/markdown_utils/gno.mod | 1 + .../p/demo/markdown_utils/markdown_utils.gno | 26 + .../r/demo/bugs/slice_pop_push/gno.mod | 1 + .../bugs/slice_pop_push/slice_pop_push.gno | 19 + .../r/demo/bugs/var_prev_realm/Makefile | 36 + .../r/demo/bugs/var_prev_realm/gno.mod | 1 + .../bugs/var_prev_realm/var_prev_realm.gno | 23 + examples/gno.land/r/demo/dao_realm/Makefile | 45 + .../gno.land/r/demo/dao_realm/dao_realm.gno | 90 ++ examples/gno.land/r/demo/dao_realm/gno.mod | 10 + examples/gno.land/r/demo/groups/group.gno | 35 +- examples/gno.land/r/demo/groups/member.gno | 13 + examples/gno.land/r/demo/groups/messages.gno | 138 +++ examples/gno.land/r/demo/groups/public.gno | 73 +- examples/gno.land/r/demo/modboards/Makefile | 36 + examples/gno.land/r/demo/modboards/README.md | 136 +++ examples/gno.land/r/demo/modboards/board.gno | 172 ++++ 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 | 6 + .../gno.land/r/demo/modboards/messages.gno | 228 ++++ examples/gno.land/r/demo/modboards/misc.gno | 96 ++ examples/gno.land/r/demo/modboards/post.gno | 263 +++++ 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 | 972 ++++++++++++++++++ .../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 + 86 files changed, 6597 insertions(+), 23 deletions(-) create mode 100644 examples/gno.land/p/demo/binutils/binutils.gno create mode 100644 examples/gno.land/p/demo/binutils/gno.mod create mode 100644 examples/gno.land/p/demo/cpavl/Makefile create mode 100644 examples/gno.land/p/demo/cpavl/gno.mod create mode 100644 examples/gno.land/p/demo/cpavl/node.gno create mode 100644 examples/gno.land/p/demo/cpavl/node_test.gno create mode 100644 examples/gno.land/p/demo/cpavl/tree.gno create mode 100644 examples/gno.land/p/demo/cpavl/z_0_filetest.gno create mode 100644 examples/gno.land/p/demo/cpavl/z_1_filetest.gno create mode 100644 examples/gno.land/p/demo/cpavl/z_2_filetest.gno create mode 100644 examples/gno.land/p/demo/daodao/core/Makefile create mode 100644 examples/gno.land/p/demo/daodao/core/dao_core.gno create mode 100644 examples/gno.land/p/demo/daodao/core/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces/Makefile create mode 100644 examples/gno.land/p/demo/daodao/interfaces/dao_interfaces.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/dao_messages.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces/threshold.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/Makefile 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/update_settings.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group/Makefile 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/Makefile 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/markdown_utils/Makefile 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/r/demo/bugs/slice_pop_push/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno create mode 100644 examples/gno.land/r/demo/bugs/var_prev_realm/Makefile create mode 100644 examples/gno.land/r/demo/bugs/var_prev_realm/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/var_prev_realm/var_prev_realm.gno create mode 100644 examples/gno.land/r/demo/dao_realm/Makefile create mode 100644 examples/gno.land/r/demo/dao_realm/dao_realm.gno create mode 100644 examples/gno.land/r/demo/dao_realm/gno.mod create mode 100644 examples/gno.land/r/demo/groups/messages.gno create mode 100644 examples/gno.land/r/demo/modboards/Makefile 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 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/cpavl/Makefile b/examples/gno.land/p/demo/cpavl/Makefile new file mode 100644 index 00000000000..7eecf7b28ff --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/Makefile @@ -0,0 +1,30 @@ +pkgpath=p/demo/cpavl +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.package +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" \ No newline at end of file diff --git a/examples/gno.land/p/demo/cpavl/gno.mod b/examples/gno.land/p/demo/cpavl/gno.mod new file mode 100644 index 00000000000..bd00284e7d8 --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/cpavl diff --git a/examples/gno.land/p/demo/cpavl/node.gno b/examples/gno.land/p/demo/cpavl/node.gno new file mode 100644 index 00000000000..a49dba00ae1 --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/node.gno @@ -0,0 +1,463 @@ +package avl + +//---------------------------------------- +// Node + +type Node struct { + key string + value interface{} + height int8 + size int + leftNode *Node + rightNode *Node +} + +func NewNode(key string, value interface{}) *Node { + return &Node{ + key: key, + value: value, + height: 0, + size: 1, + } +} + +func (node *Node) Size() int { + if node == nil { + return 0 + } + return node.size +} + +func (node *Node) IsLeaf() bool { + return node.height == 0 +} + +func (node *Node) Key() string { + return node.key +} + +func (node *Node) Value() interface{} { + return node.value +} + +func (node *Node) _copy() *Node { + if node.height == 0 { + panic("Why are you copying a value node?") + } + return &Node{ + key: node.key, + height: node.height, + size: node.size, + leftNode: node.leftNode, + rightNode: node.rightNode, + } +} + +func (node *Node) Has(key string) (has bool) { + if node == nil { + return false + } + if node.key == key { + return true + } + if node.height == 0 { + return false + } else { + if key < node.key { + return node.getLeftNode().Has(key) + } else { + return node.getRightNode().Has(key) + } + } +} + +func (node *Node) Get(key string) (index int, value interface{}, exists bool) { + if node == nil { + return 0, nil, false + } + if node.height == 0 { + if node.key == key { + return 0, node.value, true + } else if node.key < key { + return 1, nil, false + } else { + return 0, nil, false + } + } else { + if key < node.key { + return node.getLeftNode().Get(key) + } else { + rightNode := node.getRightNode() + index, value, exists = rightNode.Get(key) + index += node.size - rightNode.size + return index, value, exists + } + } +} + +func (node *Node) GetByIndex(index int) (key string, value interface{}) { + if node.height == 0 { + if index == 0 { + return node.key, node.value + } else { + panic("GetByIndex asked for invalid index") + return "", nil + } + } else { + // TODO: could improve this by storing the sizes + leftNode := node.getLeftNode() + if index < leftNode.size { + return leftNode.GetByIndex(index) + } else { + return node.getRightNode().GetByIndex(index - leftNode.size) + } + } +} + +// XXX consider a better way to do this... perhaps split Node from Node. +func (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) { + if node == nil { + return NewNode(key, value), false + } + if node.height == 0 { + if key < node.key { + return &Node{ + key: node.key, + height: 1, + size: 2, + leftNode: NewNode(key, value), + rightNode: node, + }, false + } else if key == node.key { + return NewNode(key, value), true + } else { + return &Node{ + key: key, + height: 1, + size: 2, + leftNode: node, + rightNode: NewNode(key, value), + }, false + } + } else { + node = node._copy() + if key < node.key { + node.leftNode, updated = node.getLeftNode().Set(key, value) + } else { + node.rightNode, updated = node.getRightNode().Set(key, value) + } + if updated { + return node, updated + } else { + node.calcHeightAndSize() + return node.balance(), updated + } + } +} + +// newNode: The new node to replace node after remove. +// newKey: new leftmost leaf key for node after successfully removing 'key' if changed. +// value: removed value. +func (node *Node) Remove(key string) ( + newNode *Node, newKey string, value interface{}, removed bool, +) { + if node == nil { + return nil, "", nil, false + } + if node.height == 0 { + if key == node.key { + return nil, "", node.value, true + } else { + return node, "", nil, false + } + } else { + if key < node.key { + var newLeftNode *Node + newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key) + if !removed { + return node, "", value, false + } else if newLeftNode == nil { // left node held value, was removed + return node.rightNode, node.key, value, true + } + node = node._copy() + node.leftNode = newLeftNode + node.calcHeightAndSize() + node = node.balance() + return node, newKey, value, true + } else { + var newRightNode *Node + newRightNode, newKey, value, removed = node.getRightNode().Remove(key) + if !removed { + return node, "", value, false + } else if newRightNode == nil { // right node held value, was removed + return node.leftNode, "", value, true + } + node = node._copy() + node.rightNode = newRightNode + if newKey != "" { + node.key = newKey + } + node.calcHeightAndSize() + node = node.balance() + return node, "", value, true + } + } +} + +func (node *Node) getLeftNode() *Node { + return node.leftNode +} + +func (node *Node) getRightNode() *Node { + return node.rightNode +} + +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateRight() *Node { + node = node._copy() + l := node.getLeftNode() + _l := l._copy() + + _lrCached := _l.rightNode + _l.rightNode = node + node.leftNode = _lrCached + + node.calcHeightAndSize() + _l.calcHeightAndSize() + + return _l +} + +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateLeft() *Node { + node = node._copy() + r := node.getRightNode() + _r := r._copy() + + _rlCached := _r.leftNode + _r.leftNode = node + node.rightNode = _rlCached + + node.calcHeightAndSize() + _r.calcHeightAndSize() + + return _r +} + +// NOTE: mutates height and size +func (node *Node) calcHeightAndSize() { + node.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1 + node.size = node.getLeftNode().size + node.getRightNode().size +} + +func (node *Node) calcBalance() int { + return int(node.getLeftNode().height) - int(node.getRightNode().height) +} + +// NOTE: assumes that node can be modified +// TODO: optimize balance & rotate +func (node *Node) balance() (newSelf *Node) { + balance := node.calcBalance() + if balance > 1 { + if node.getLeftNode().calcBalance() >= 0 { + // Left Left Case + return node.rotateRight() + } else { + // Left Right Case + // node = node._copy() + left := node.getLeftNode() + node.leftNode = left.rotateLeft() + // node.calcHeightAndSize() + return node.rotateRight() + } + } + if balance < -1 { + if node.getRightNode().calcBalance() <= 0 { + // Right Right Case + return node.rotateLeft() + } else { + // Right Left Case + // node = node._copy() + right := node.getRightNode() + node.rightNode = right.rotateRight() + // node.calcHeightAndSize() + return node.rotateLeft() + } + } + // Nothing changed + return node +} + +// Shortcut for TraverseInRange. +func (node *Node) Iterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, true, true, cb) +} + +// Shortcut for TraverseInRange. +func (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, false, true, cb) +} + +// TraverseInRange traverses all nodes, including inner nodes. +// Start is inclusive and end is exclusive when ascending, +// Start and end are inclusive when descending. +// Empty start and empty end denote no start and no end. +// If leavesOnly is true, only visit leaf nodes. +// NOTE: To simulate an exclusive reverse traversal, +// just append 0x00 to start. +func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + afterStart := (start == "" || start < node.key) + startOrAfter := (start == "" || start <= node.key) + beforeEnd := false + if ascending { + beforeEnd = (end == "" || node.key < end) + } else { + beforeEnd = (end == "" || node.key <= end) + } + + // Run callback per inner/leaf node. + stop := false + if (!node.IsLeaf() && !leavesOnly) || + (node.IsLeaf() && startOrAfter && beforeEnd) { + stop = cb(node) + if stop { + return stop + } + } + if node.IsLeaf() { + return stop + } + + if ascending { + // check lower nodes, then higher + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } else { + // check the higher nodes first + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } + + return stop +} + +// TraverseByOffset traverses all nodes, including inner nodes. +// A limit of math.MaxInt means no limit. +func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + + // fast paths. these happen only if TraverseByOffset is called directly on a leaf. + if limit <= 0 || offset >= node.size { + return false + } + if node.IsLeaf() { + if offset > 0 { + return false + } + return cb(node) + } + + // go to the actual recursive function. + return node.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + // caller guarantees: offset < node.size; limit > 0. + + if !leavesOnly { + if cb(node) { + return true + } + } + first, second := node.getLeftNode(), node.getRightNode() + if descending { + first, second = second, first + } + if first.IsLeaf() { + // either run or skip, based on offset + if offset > 0 { + offset-- + } else { + cb(first) + limit-- + if limit <= 0 { + return false + } + } + } else { + // possible cases: + // 1 the offset given skips the first node entirely + // 2 the offset skips none or part of the first node, but the limit requires some of the second node. + // 3 the offset skips none or part of the first node, and the limit stops our search on the first node. + if offset >= first.size { + offset -= first.size // 1 + } else { + if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) { + return true + } + // number of leaves which could actually be called from inside + delta := first.size - offset + offset = 0 + if delta >= limit { + return true // 3 + } + limit -= delta // 2 + } + } + + // because of the caller guarantees and the way we handle the first node, + // at this point we know that limit > 0 and there must be some values in + // this second node that we include. + + // => if the second node is a leaf, it has to be included. + if second.IsLeaf() { + return cb(second) + } + // => if it is not a leaf, it will still be enough to recursively call this + // function with the updated offset and limit + return second.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// Only used in testing... +func (node *Node) lmd() *Node { + if node.height == 0 { + return node + } + return node.getLeftNode().lmd() +} + +// Only used in testing... +func (node *Node) rmd() *Node { + if node.height == 0 { + return node + } + return node.getRightNode().rmd() +} + +func maxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} diff --git a/examples/gno.land/p/demo/cpavl/node_test.gno b/examples/gno.land/p/demo/cpavl/node_test.gno new file mode 100644 index 00000000000..a42cb3a97a8 --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/node_test.gno @@ -0,0 +1,100 @@ +package avl + +import ( + "sort" + "strings" + "testing" +) + +func TestTraverseByOffset(t *testing.T) { + const testStrings = `Alfa +Alfred +Alpha +Alphabet +Beta +Beth +Book +Browser` + tt := []struct { + name string + desc bool + }{ + {"ascending", false}, + {"descending", true}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + sl := strings.Split(testStrings, "\n") + + // sort a first time in the order opposite to how we'll be traversing + // the tree, to ensure that we are not just iterating through with + // insertion order. + sort.Sort(sort.StringSlice(sl)) + if !tc.desc { + reverseSlice(sl) + } + + r := NewNode(sl[0], nil) + for _, v := range sl[1:] { + r, _ = r.Set(v, nil) + } + + // then sort sl in the order we'll be traversing it, so that we can + // compare the result with sl. + reverseSlice(sl) + + var result []string + for i := 0; i < len(sl); i++ { + r.TraverseByOffset(i, 1, tc.desc, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + } + + if !slicesEqual(sl, result) { + t.Errorf("want %v got %v", sl, result) + } + + for l := 2; l <= len(sl); l++ { + // "slices" + for i := 0; i <= len(sl); i++ { + max := i + l + if max > len(sl) { + max = len(sl) + } + exp := sl[i:max] + actual := []string{} + + r.TraverseByOffset(i, l, tc.desc, true, func(tr *Node) bool { + actual = append(actual, tr.Key()) + return false + }) + // t.Log(exp, actual) + if !slicesEqual(exp, actual) { + t.Errorf("want %v got %v", exp, actual) + } + } + } + }) + } +} + +func slicesEqual(w1, w2 []string) bool { + if len(w1) != len(w2) { + return false + } + for i := 0; i < len(w1); i++ { + if w1[0] != w2[0] { + return false + } + } + return true +} + +func reverseSlice(ss []string) { + for i := 0; i < len(ss)/2; i++ { + j := len(ss) - 1 - i + ss[i], ss[j] = ss[j], ss[i] + } +} diff --git a/examples/gno.land/p/demo/cpavl/tree.gno b/examples/gno.land/p/demo/cpavl/tree.gno new file mode 100644 index 00000000000..7b33d28fbe3 --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/tree.gno @@ -0,0 +1,82 @@ +package avl + +type IterCbFn func(key string, value interface{}) bool + +//---------------------------------------- +// Tree + +// The zero struct can be used as an empty tree. +type Tree struct { + node *Node +} + +func NewTree() *Tree { + return &Tree{ + node: nil, + } +} + +func (tree *Tree) Size() int { + return tree.node.Size() +} + +func (tree *Tree) Has(key string) (has bool) { + return tree.node.Has(key) +} + +func (tree *Tree) Get(key string) (value interface{}, exists bool) { + _, value, exists = tree.node.Get(key) + return +} + +func (tree *Tree) GetByIndex(index int) (key string, value interface{}) { + return tree.node.GetByIndex(index) +} + +func (tree *Tree) Set(key string, value interface{}) (updated bool) { + newnode, updated := tree.node.Set(key, value) + tree.node = newnode + return updated +} + +func (tree *Tree) Remove(key string) (value interface{}, removed bool) { + newnode, _, value, removed := tree.node.Remove(key) + tree.node = newnode + return value, removed +} + +// Shortcut for TraverseInRange. +func (tree *Tree) Iterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// Shortcut for TraverseInRange. +func (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// Shortcut for TraverseByOffset. +func (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// Shortcut for TraverseByOffset. +func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} diff --git a/examples/gno.land/p/demo/cpavl/z_0_filetest.gno b/examples/gno.land/p/demo/cpavl/z_0_filetest.gno new file mode 100644 index 00000000000..2445c806ff3 --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/z_0_filetest.gno @@ -0,0 +1,348 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "gno.land/p/demo/cpavl" +) + +var node *avl.Node + +func init() { + node = avl.NewNode("key0", "value0") + // node, _ = node.Set("key0", "value0") +} + +func main() { + var updated bool + node, updated = node.Set("key1", "value1") + // println(node, updated) + println(updated, node.Size()) +} + +// Output: +// false 2 + +// Realm: +// switchrealm["gno.land/r/test"] +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key0" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "value0" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:4", +// "ModTime": "5", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5", +// "RefCount": "1" +// } +// } +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key1" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "value1" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5", +// "RefCount": "1" +// } +// } +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key1" +// } +// }, +// {}, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "091729e38bda8724bce4c314f9624b91af679459", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:4" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "0b5493aa4ea42087780bdfcaebab2c3eec351c15", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", +// "RefCount": "1" +// } +// } +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", +// "IsEscaped": true, +// "ModTime": "4", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "File": "", +// "Line": "0", +// "Nonce": "0", +// "PkgPath": "gno.land/r/test" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" +// }, +// "FileName": "main.gno", +// "IsMethod": false, +// "Name": "init.0", +// "PkgPath": "gno.land/r/test", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "File": "main.gno", +// "Line": "10", +// "Nonce": "0", +// "PkgPath": "gno.land/r/test" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" +// }, +// "FileName": "main.gno", +// "IsMethod": false, +// "Name": "main", +// "PkgPath": "gno.land/r/test", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "File": "main.gno", +// "Line": "15", +// "Nonce": "0", +// "PkgPath": "gno.land/r/test" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "6c9948281d4c60b2d95233b76388d54d8b1a2fad", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5" +// } +// } +// } +// } +// ] +// } diff --git a/examples/gno.land/p/demo/cpavl/z_1_filetest.gno b/examples/gno.land/p/demo/cpavl/z_1_filetest.gno new file mode 100644 index 00000000000..8e4c08ce649 --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/z_1_filetest.gno @@ -0,0 +1,373 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "gno.land/p/demo/cpavl" +) + +var node *avl.Node + +func init() { + node = avl.NewNode("key0", "value0") + node, _ = node.Set("key1", "value1") +} + +func main() { + var updated bool + node, updated = node.Set("key2", "value2") + // println(node, updated) + println(updated, node.Size()) +} + +// Output: +// false 3 + +// Realm: +// switchrealm["gno.land/r/test"] +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key2" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "value2" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8", +// "RefCount": "1" +// } +// } +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key2" +// } +// }, +// {}, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "7a8a63e17a567d7b0891ac89d5cd90072a73787d", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "ab5a297f4eb033d88bdf1677f4dc151ccb9fde9f", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7", +// "RefCount": "1" +// } +// } +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:7]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key1" +// } +// }, +// {}, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AwAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "627e8e517e7ae5db0f3b753e2a32b607989198b6", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "fe8afd501233fb95375016199f0443b3c6ab1fbc", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", +// "RefCount": "1" +// } +// } +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", +// "IsEscaped": true, +// "ModTime": "6", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "File": "", +// "Line": "0", +// "Nonce": "0", +// "PkgPath": "gno.land/r/test" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" +// }, +// "FileName": "main.gno", +// "IsMethod": false, +// "Name": "init.0", +// "PkgPath": "gno.land/r/test", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "File": "main.gno", +// "Line": "10", +// "Nonce": "0", +// "PkgPath": "gno.land/r/test" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" +// }, +// "FileName": "main.gno", +// "IsMethod": false, +// "Name": "main", +// "PkgPath": "gno.land/r/test", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "File": "main.gno", +// "Line": "15", +// "Nonce": "0", +// "PkgPath": "gno.land/r/test" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "c5eefc40ed065461b4a920c1349ed734ffdead8f", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7" +// } +// } +// } +// } +// ] +// } +// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:4] diff --git a/examples/gno.land/p/demo/cpavl/z_2_filetest.gno b/examples/gno.land/p/demo/cpavl/z_2_filetest.gno new file mode 100644 index 00000000000..e38d4f1c6df --- /dev/null +++ b/examples/gno.land/p/demo/cpavl/z_2_filetest.gno @@ -0,0 +1,292 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "gno.land/p/demo/cpavl" +) + +var tree avl.Tree + +func init() { + tree.Set("key0", "value0") + tree.Set("key1", "value1") +} + +func main() { + var updated bool + updated = tree.Set("key2", "value2") + println(updated, tree.Size()) +} + +// Output: +// false 3 + +// Realm: +// switchrealm["gno.land/r/test"] +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:10]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key2" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "value2" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:10", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9", +// "RefCount": "1" +// } +// } +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key2" +// } +// }, +// {}, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "213baed7e3326f2403b5f30e5d4397510ba4f37d", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "be751422ef4c2bc068a456f9467d2daca27db8ca", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:10" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8", +// "RefCount": "1" +// } +// } +// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "key1" +// } +// }, +// {}, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AwAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "af4d0b158681d85eb2a7f6888b39a05ca7b790ee", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "ef853d70e334fd2c807d6c2c751da1fcd1e5ad58", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8", +// "ModTime": "0", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:4", +// "RefCount": "1" +// } +// } +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "3a5af0895c2c45b8a5e894644bcd689f1fdc4785", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:4", +// "ModTime": "7", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", +// "RefCount": "1" +// } +// } +// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5] diff --git a/examples/gno.land/p/demo/daodao/core/Makefile b/examples/gno.land/p/demo/daodao/core/Makefile new file mode 100644 index 00000000000..ace02084026 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/Makefile @@ -0,0 +1,30 @@ +pkgpath=p/demo/daodao/core +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.package +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" \ 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..f4bc7a49c3d --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/dao_core.gno @@ -0,0 +1,70 @@ +package core + +import ( + "std" + "strings" + + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "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/gno.mod b/examples/gno.land/p/demo/daodao/core/gno.mod new file mode 100644 index 00000000000..08a109c37fe --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/demo/daodao/core + +require ( + "gno.land/p/demo/daodao/interfaces" 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/Makefile b/examples/gno.land/p/demo/daodao/interfaces/Makefile new file mode 100644 index 00000000000..dad1f8f46c8 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/Makefile @@ -0,0 +1,30 @@ +pkgpath=p/demo/daodao/interfaces +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.package +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/interfaces/dao_interfaces.gno b/examples/gno.land/p/demo/daodao/interfaces/dao_interfaces.gno new file mode 100644 index 00000000000..55dc5c61757 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/dao_interfaces.gno @@ -0,0 +1,132 @@ +package dao_interfaces + +import ( + "std" + "strconv" + + "gno.land/p/demo/cpavl" +) + +type IVotingModule interface { + VotingPower(addr std.Address) uint64 + TotalPower() uint64 + Render(path string) string +} + +type Ballot struct { + Power uint64 + Vote Vote + Rationale string +} + +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 +} + +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 +} + +type ProposalStatus int + +const ( + ProposalStatusOpen ProposalStatus = iota + ProposalStatusPassed + ProposalStatusExecuted +) + +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) 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/dao_messages.gno b/examples/gno.land/p/demo/daodao/interfaces/dao_messages.gno new file mode 100644 index 00000000000..d404d4fa918 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/dao_messages.gno @@ -0,0 +1,66 @@ +package dao_interfaces + +import ( + "encoding/base64" + "encoding/binary" + "strings" + + "gno.land/p/demo/cpavl" +) + +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/gno.mod b/examples/gno.land/p/demo/daodao/interfaces/gno.mod new file mode 100644 index 00000000000..1e8de60ab4f --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/daodao/interfaces \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/interfaces/threshold.gno b/examples/gno.land/p/demo/daodao/interfaces/threshold.gno new file mode 100644 index 00000000000..7b34ba7b4d2 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/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/Makefile b/examples/gno.land/p/demo/daodao/proposal_single/Makefile new file mode 100644 index 00000000000..83605d241a9 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/Makefile @@ -0,0 +1,36 @@ +pkgpath=p/demo/daodao/proposal_single +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +gnohome=$(HOME)/Code/gno + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.pkg +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" + +.PHONY: test +test: + gno test -verbose=true -root-dir="$(gnohome)" ./$(pkgsrc) \ No newline at end of file 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..1a5edb51413 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/dao_proposal_single.gno @@ -0,0 +1,300 @@ +package dao_proposal_single + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/cpavl" + dao_core "gno.land/p/demo/daodao/core" + dao_interfaces "gno.land/p/demo/daodao/interfaces" +) + +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) 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/gno.mod b/examples/gno.land/p/demo/daodao/proposal_single/gno.mod new file mode 100644 index 00000000000..c9ca48053d4 --- /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/cpavl" v0.0.0-latest + "gno.land/p/demo/daodao/interfaces" v0.0.0-latest + "gno.land/p/demo/daodao/core" v0.0.0-latest +) \ No newline at end of file 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..c613862f78b --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/update_settings.gno @@ -0,0 +1,114 @@ +package dao_proposal_single + +import ( + "encoding/binary" + "strings" + + "gno.land/p/demo/daodao/interfaces" +) + +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/Makefile b/examples/gno.land/p/demo/daodao/voting_group/Makefile new file mode 100644 index 00000000000..1c04ac8b710 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group/Makefile @@ -0,0 +1,36 @@ +pkgpath=p/demo/daodao/voting_group +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +gnohome=$(HOME)/Code/gno + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.package +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" + +.PHONY: test +test: + gno test -verbose=true -root-dir="$(gnohome)" ./$(pkgsrc) \ No newline at end of file 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..fdad76e1844 --- /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 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/voting_group_test.gno b/examples/gno.land/p/demo/daodao/voting_group/voting_group_test.gno new file mode 100644 index 00000000000..2cad8a2e287 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group/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/Makefile b/examples/gno.land/p/demo/flags_index/Makefile new file mode 100644 index 00000000000..5f350e75fe3 --- /dev/null +++ b/examples/gno.land/p/demo/flags_index/Makefile @@ -0,0 +1,36 @@ +pkgpath=p/demo/flags_index +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +gnohome=$(HOME)/Code/gno + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.package +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" + +.PHONY: test +test: + gno test -verbose=true -root-dir="$(gnohome)" ./$(pkgsrc) \ No newline at end of file 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..ba429637eb9 --- /dev/null +++ b/examples/gno.land/p/demo/flags_index/flags_index.gno @@ -0,0 +1,152 @@ +package flags_index + +import ( + "strconv" + + "gno.land/p/demo/cpavl" +) + +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 := make([]*FlagCount, len(fi.flagsCounts)-1) + 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 += "- " + 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 += "- " + 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..a6aacf373e9 --- /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/cpavl" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/markdown_utils/Makefile b/examples/gno.land/p/demo/markdown_utils/Makefile new file mode 100644 index 00000000000..f26f612543a --- /dev/null +++ b/examples/gno.land/p/demo/markdown_utils/Makefile @@ -0,0 +1,30 @@ +pkgpath=p/demo/markdown_utils +pkguri=gno.land/$(pkgpath) +pkgsrc=. + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.package +deploy.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="./$(pkgsrc)" \ + -pkgpath="$(pkguri)" \ + $(wallet) + +.PHONY: open.pkg +open.pkg: + open "$(gnoweb)/$(pkgpath)" \ No newline at end of file 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/r/demo/bugs/slice_pop_push/gno.mod b/examples/gno.land/r/demo/bugs/slice_pop_push/gno.mod new file mode 100644 index 00000000000..7f6934e0a35 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/slice_pop_push/gno.mod @@ -0,0 +1 @@ +module "gno.land/r/demo/bugs/slice_pop_push" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno b/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno new file mode 100644 index 00000000000..849748b6424 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno @@ -0,0 +1,19 @@ +package slice_pop_push + +var slice = []string{"undead-element"} + +func Pop() { + slice = slice[:len(slice)-1] +} + +func Push() { + slice = append(slice, "new-element") +} + +func Render(path string) string { + os := []string{"undead-element-2"} + os = os[:len(os)-1] + os = append(os, "new-element-2") + + return os[0] + slice[0] +} diff --git a/examples/gno.land/r/demo/bugs/var_prev_realm/Makefile b/examples/gno.land/r/demo/bugs/var_prev_realm/Makefile new file mode 100644 index 00000000000..a9b570b7673 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/var_prev_realm/Makefile @@ -0,0 +1,36 @@ +realmpath=r/demo/bugs/var_prev_realm_2 +realmuri=gno.land/$(realmpath) + +gnohome=$(HOME)/Code/gno + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.realm +deploy.realm: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="10000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="." \ + -pkgpath="$(realmuri)" \ + $(wallet) + +.PHONY: open.realm +open.realm: + open "$(gnoweb)/$(realmpath)" + + +.PHONY: open.realm-doc +open.realm-doc: + open "$(gnoweb)/$(realmpath)?help" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/var_prev_realm/gno.mod b/examples/gno.land/r/demo/bugs/var_prev_realm/gno.mod new file mode 100644 index 00000000000..0f503c844fd --- /dev/null +++ b/examples/gno.land/r/demo/bugs/var_prev_realm/gno.mod @@ -0,0 +1 @@ +module "gno.land/r/demo/bugs/var_prev_realm" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/var_prev_realm/var_prev_realm.gno b/examples/gno.land/r/demo/bugs/var_prev_realm/var_prev_realm.gno new file mode 100644 index 00000000000..dfac21e6798 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/var_prev_realm/var_prev_realm.gno @@ -0,0 +1,23 @@ +package var_prev_realm + +import ( + "std" + + "gno.land/r/demo/tests" +) + +var ( + varRealm = tests.GetPrevRealm() + initRealm std.Realm +) + +func init() { + initRealm = tests.GetPrevRealm() +} + +func Render(path string) string { + s := "" + s += "Var block realm:\n\nPackage path: " + varRealm.PkgPath() + "\n\nAddress: " + varRealm.Addr().String() + "\n\n" + s += "Init block realm:\n\nPackage path: " + initRealm.PkgPath() + "\n\nAddress: " + initRealm.Addr().String() + "\n\n" + return s +} diff --git a/examples/gno.land/r/demo/dao_realm/Makefile b/examples/gno.land/r/demo/dao_realm/Makefile new file mode 100644 index 00000000000..3a03a8d1e32 --- /dev/null +++ b/examples/gno.land/r/demo/dao_realm/Makefile @@ -0,0 +1,45 @@ +realmpath=r/demo/foo_dao_3 +realmuri=gno.land/$(realmpath) + +gnohome=$(HOME)/Code/gno + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.realm +deploy.realm: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="." \ + -pkgpath="$(realmuri)" \ + $(wallet) + +.PHONY: open.realm +open.realm: + open "$(gnoweb)/$(realmpath)" + + +.PHONY: open.realm-doc +open.realm-doc: + open "$(gnoweb)/$(realmpath)?help" + +.PHONY: query.vote +define queryadmindata +$(realmuri) +GetCore().ProposalModules()[0].GetBallot(0, "g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv").Vote +endef +export queryadmindata +query.vote: + gnokey query -data="$$queryadmindata" "vm/qeval" -remote="$(gnoland)" \ No newline at end of file diff --git a/examples/gno.land/r/demo/dao_realm/dao_realm.gno b/examples/gno.land/r/demo/dao_realm/dao_realm.gno new file mode 100644 index 00000000000..5ed2e8bae17 --- /dev/null +++ b/examples/gno.land/r/demo/dao_realm/dao_realm.gno @@ -0,0 +1,90 @@ +package dao_realm + +import ( + "encoding/base64" + "std" + "strings" + "time" + + dao_core "gno.land/p/demo/daodao/core" + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/daodao/proposal_single" + "gno.land/p/demo/daodao/voting_group" + "gno.land/r/demo/groups" + modboards "gno.land/r/demo/modboards" +) + +var ( + daoCore dao_core.IDAOCore + registry = dao_interfaces.NewMessagesRegistry() + mainBoardName = "foo_dao_3" + groupID groups.GroupID +) + +func init() { + groupID = groups.CreateGroup(mainBoardName) + groups.AddMember(groupID, "g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", 1, "") + groups.AddMember(groupID, "g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", 1, "") + groups.AddMember(groupID, "g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a", 1, "") + groups.AddMember(groupID, "g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym", 1, "") + registry.Register(groups.NewAddMemberHandler()) + registry.Register(groups.NewDeleteMemberHandler()) + + daoCore = dao_core.NewDAOCore(dao_voting_group.NewGRC4Voting(groupID), nil) + + tt := dao_interfaces.Percent(100) // 1% + tq := dao_interfaces.Percent(100) // 1% + proposalMod := dao_proposal_single.NewDAOProposalSingle(daoCore, &dao_proposal_single.DAOProposalSingleOpts{ + MaxVotingPeriod: time.Hour * 24 * 42, + Threshold: dao_interfaces.Threshold{ThresholdQuorum: &dao_interfaces.ThresholdQuorum{ + Threshold: dao_interfaces.PercentageThreshold{Percent: &tt}, + Quorum: dao_interfaces.PercentageThreshold{Percent: &tq}, + }}, + Registry: registry, + }) + // TODO: add a router to support multiple proposal modules + registry.Register(dao_proposal_single.NewUpdateSettingsHandler(proposalMod)) + daoCore.AddProposalModule(proposalMod) + + registry.Register(modboards.NewCreateBoardHandler()) + registry.Register(modboards.NewDeletePostHandler()) + modboards.CreateBoard(mainBoardName) +} + +func Render(path string) string { + return "[[board](/r/demo/modboards:" + mainBoardName + ")]\n\n" + daoCore.Render(path) +} + +func GetCore() dao_core.IDAOCore { + return daoCore +} + +func Vote(moduleIndex int, proposalID int, vote dao_interfaces.Vote, rationale string) { + dao_core.GetProposalModule(daoCore, moduleIndex).Vote(proposalID, vote, rationale) +} + +func Execute(moduleIndex int, proposalID int) { + dao_core.GetProposalModule(daoCore, moduleIndex).Execute(proposalID) +} + +func Propose(moduleIndex int, title string, description string, b64Messages string) { + mod := dao_core.GetProposalModule(daoCore, moduleIndex) + var messages []dao_interfaces.ExecutableMessage + if len(b64Messages) != 0 { + rawMessages := strings.Split(b64Messages, ",") + for _, rawMessage := range rawMessages { + message := registry.FromBase64String(rawMessage) + messages = append(messages, message) + } + } + mod.Propose(title, description, messages) +} + +func GetBinaryMembers() string { + members := groups.GetMembers(groupID) + ss := []string{} + for _, member := range members { + ss = append(ss, base64.RawURLEncoding.EncodeToString(member.Bytes())) + } + return strings.Join(ss, ",") +} diff --git a/examples/gno.land/r/demo/dao_realm/gno.mod b/examples/gno.land/r/demo/dao_realm/gno.mod new file mode 100644 index 00000000000..dae2df2a65f --- /dev/null +++ b/examples/gno.land/r/demo/dao_realm/gno.mod @@ -0,0 +1,10 @@ +module gno.land/r/demo/dao_realm + +require ( + "gno.land/p/demo/daodao/core" v0.0.0-latest + "gno.land/p/demo/daodao/interfaces" v0.0.0-latest + "gno.land/p/demo/daodao/proposal_single" v0.0.0-latest + "gno.land/p/demo/daodao/voting_group" v0.0.0-latest + "gno.land/r/demo/groups" v0.0.0-latest + "gno.land/r/demo/modboards" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/r/demo/groups/group.gno b/examples/gno.land/r/demo/groups/group.gno index a03986e2c76..8caf16be48f 100644 --- a/examples/gno.land/r/demo/groups/group.gno +++ b/examples/gno.land/r/demo/groups/group.gno @@ -15,13 +15,15 @@ func (gid GroupID) String() string { } type Group struct { - id GroupID - url string - name string - lastMemberID MemberID - members avl.Tree - creator std.Address - createdAt time.Time + id GroupID + url string + name string + lastMemberID MemberID + members avl.Tree + membersByAddress avl.Tree + creator std.Address + createdAt time.Time + totalWeight int } func newGroup(url string, name string, creator std.Address) *Group { @@ -45,6 +47,9 @@ func (group *Group) newMember(id MemberID, address std.Address, weight int, meta if group.members.Has(address.String()) { panic("this member for this group already exists") } + if group.membersByAddress.Has(address.String()) { + panic("this address is already in this group") + } return &Member{ id: id, address: address, @@ -64,9 +69,10 @@ func (group *Group) HasPermission(addr std.Address, perm Permission) bool { func (group *Group) RenderGroup() string { str := "Group ID: " + groupIDKey(group.id) + "\n\n" + "Group Name: " + group.name + "\n\n" + - "Group Creator: " + usernameOf(group.creator) + "\n\n" + + "Group Creator: " + string(group.creator) + "\n\n" + "Group createdAt: " + group.createdAt.String() + "\n\n" + - "Group Last MemberID: " + memberIDKey(group.lastMemberID) + "\n\n" + "Group Last MemberID: " + memberIDKey(group.lastMemberID) + "\n\n" + + "Total Weight: " + strconv.Itoa(group.totalWeight) + "\n\n" str += "Group Members: \n\n" group.members.Iterate("", "", func(key string, value interface{}) bool { @@ -77,6 +83,10 @@ func (group *Group) RenderGroup() string { return str } +func (group *Group) GetMembers() *avl.Tree { + return &group.members +} + func (group *Group) deleteGroup() { gidkey := groupIDKey(group.id) _, gGroupsRemoved := gGroups.Remove(gidkey) @@ -94,5 +104,12 @@ func (group *Group) deleteMember(mid MemberID) { g := getGroup(group.id) midkey := memberIDKey(mid) + imember, ok := g.members.Get(midkey) + if !ok { + return + } + member := imember.(*Member) g.members.Remove(midkey) + g.membersByAddress.Remove(member.address.String()) + group.totalWeight -= member.weight } diff --git a/examples/gno.land/r/demo/groups/member.gno b/examples/gno.land/r/demo/groups/member.gno index bdb245e35fc..d6b47522011 100644 --- a/examples/gno.land/r/demo/groups/member.gno +++ b/examples/gno.land/r/demo/groups/member.gno @@ -1,9 +1,12 @@ package groups import ( + "encoding/binary" "std" "strconv" "time" + + "gno.land/p/demo/binutils" ) type MemberID uint64 @@ -25,3 +28,13 @@ func (member *Member) getMemberStr() string { memberDataStr += "\t\t\t[" + memberIDKey(member.id) + ", " + member.address.String() + ", " + strconv.Itoa(member.weight) + ", " + member.metadata + ", " + member.createdAt.String() + "],\n\n" return memberDataStr } + +func (member *Member) Bytes() []byte { + b := []byte{} + b = binary.BigEndian.AppendUint64(b, uint64(member.id)) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(member.address.String())...) + b = binary.BigEndian.AppendUint32(b, uint32(member.weight)) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(member.metadata)...) + b = binary.BigEndian.AppendUint64(b, uint64(member.createdAt.UnixNano())) + return b +} diff --git a/examples/gno.land/r/demo/groups/messages.gno b/examples/gno.land/r/demo/groups/messages.gno new file mode 100644 index 00000000000..1c3ce03e5d6 --- /dev/null +++ b/examples/gno.land/r/demo/groups/messages.gno @@ -0,0 +1,138 @@ +package groups + +import ( + "encoding/binary" + "std" + "strconv" + "strings" + + "gno.land/p/demo/binutils" + "gno.land/p/demo/daodao/interfaces" +) + +type ExecutableMessageAddMember struct { + dao_interfaces.ExecutableMessage + + GroupID GroupID + Address string + Weight int + Metadata string +} + +func (msg *ExecutableMessageAddMember) Type() string { + return "AddMember" +} + +func (msg *ExecutableMessageAddMember) String() string { + var ss []string + ss = append(ss, msg.Type()) + s := "GroupID: " + msg.GroupID.String() + "\n" + s += "Address: " + msg.Address + "\n" + s += "Weight: " + strconv.Itoa(msg.Weight) + "\n" + s += "Metadata: " + msg.Metadata + ss = append(ss, s) + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageAddMember) Binary() []byte { + b := []byte{} + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(msg.Type())...) + b = binary.BigEndian.AppendUint64(b, uint64(msg.GroupID)) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(msg.Address)...) + b = binary.BigEndian.AppendUint32(b, uint32(msg.Weight)) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(msg.Metadata)...) + return b +} + +func ExecutableMessageAddMemberFromBinary(b []byte) *ExecutableMessageAddMember { + msg := &ExecutableMessageAddMember{} + t, b := binutils.MustDecodeLengthPrefixedStringUint16BE(b) + if t != msg.Type() { + panic("invalid type") + } + msg.GroupID, b = GroupID(binary.BigEndian.Uint64(b)), b[8:] + msg.Address, b = binutils.MustDecodeLengthPrefixedStringUint16BE(b) + msg.Weight, b = int(binary.BigEndian.Uint32(b)), b[4:] + msg.Metadata, b = binutils.MustDecodeLengthPrefixedStringUint16BE(b) + return msg +} + +type AddMemberHandler struct { + dao_interfaces.MessageHandler +} + +func NewAddMemberHandler() *AddMemberHandler { + return &AddMemberHandler{} +} + +func (h *AddMemberHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageAddMember) + AddMember(msg.GroupID, msg.Address, msg.Weight, msg.Metadata) +} + +func (h *AddMemberHandler) Type() string { + return ExecutableMessageAddMember{}.Type() +} + +func (h *AddMemberHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageAddMemberFromBinary(b) +} + +type ExecutableMessageDeleteMember struct { + dao_interfaces.ExecutableMessage + + GroupID GroupID + MemberID MemberID +} + +func (msg *ExecutableMessageDeleteMember) Type() string { + return "DeleteMember" +} + +func (msg *ExecutableMessageDeleteMember) String() string { + var ss []string + ss = append(ss, msg.Type()) + s := "GroupID: " + msg.GroupID.String() + s += "MemberID: " + msg.MemberID.String() + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageDeleteMember) Binary() []byte { + b := []byte{} + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(msg.Type())...) + b = binary.BigEndian.AppendUint64(b, uint64(msg.GroupID)) + b = binary.BigEndian.AppendUint64(b, uint64(msg.MemberID)) + return b +} + +func ExecutableMessageDeleteMemberFromBinary(b []byte) *ExecutableMessageDeleteMember { + msg := &ExecutableMessageDeleteMember{} + t, b := binutils.MustDecodeLengthPrefixedStringUint16BE(b) + if t != msg.Type() { + panic("invalid type") + } + msg.GroupID, b = GroupID(binary.BigEndian.Uint64(b)), b[8:] + msg.MemberID, b = MemberID(binary.BigEndian.Uint64(b)), b[8:] + return msg +} + +type DeleteMemberHandler struct { + dao_interfaces.MessageHandler +} + +func NewDeleteMemberHandler() *DeleteMemberHandler { + return &DeleteMemberHandler{} +} + +func (h *DeleteMemberHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageDeleteMember) + DeleteMember(msg.GroupID, msg.MemberID) +} + +func (h *DeleteMemberHandler) Type() string { + return ExecutableMessageDeleteMember{}.Type() +} + +func (h *DeleteMemberHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageDeleteMemberFromBinary(b) +} diff --git a/examples/gno.land/r/demo/groups/public.gno b/examples/gno.land/r/demo/groups/public.gno index 33e7dbdcf35..cc34e7a13ef 100644 --- a/examples/gno.land/r/demo/groups/public.gno +++ b/examples/gno.land/r/demo/groups/public.gno @@ -9,6 +9,14 @@ import ( //---------------------------------------- // Public facing functions +func GetGroupNameFromID(gid GroupID) (string, bool) { + groupI, exists := gGroups.Get(groupIDKey(gid)) + if !exists { + return "", false + } + return groupI.(*Group).name, true +} + func GetGroupIDFromName(name string) (GroupID, bool) { groupI, exists := gGroupsByName.Get(name) if !exists { @@ -18,9 +26,9 @@ func GetGroupIDFromName(name string) (GroupID, bool) { } func CreateGroup(name string) GroupID { - std.AssertOriginCall() - caller := std.GetOrigCaller() - usernameOf(caller) + // std.AssertOriginCall() + caller := std.PrevRealm().Addr() + // usernameOf(caller) url := "/r/demo/groups:" + name group := newGroup(url, name, caller) gidkey := groupIDKey(group.id) @@ -30,29 +38,33 @@ func CreateGroup(name string) GroupID { } func AddMember(gid GroupID, address string, weight int, metadata string) MemberID { - std.AssertOriginCall() - caller := std.GetOrigCaller() - usernameOf(caller) + // std.AssertOriginCall() + caller := std.PrevRealm().Addr() + // usernameOf(caller) group := getGroup(gid) if !group.HasPermission(caller, EditPermission) { panic("unauthorized to edit group") } - user := users.GetUserByAddress(std.Address(address)) - if user == nil { - panic("unknown address " + address) - } + /* + user := users.GetUserByAddress(std.Address(address)) + if user == nil { + panic("unknown address " + address) + } + */ mid := group.lastMemberID member := group.newMember(mid, std.Address(address), weight, metadata) midkey := memberIDKey(mid) group.members.Set(midkey, member) + group.membersByAddress.Set(address, member) + group.totalWeight += weight mid++ group.lastMemberID = mid return member.id } func DeleteGroup(gid GroupID) { - std.AssertOriginCall() - caller := std.GetOrigCaller() + // std.AssertOriginCall() + caller := std.PrevRealm().Addr() group := getGroup(gid) if !group.HasPermission(caller, DeletePermission) { panic("unauthorized to delete group") @@ -61,11 +73,44 @@ func DeleteGroup(gid GroupID) { } func DeleteMember(gid GroupID, mid MemberID) { - std.AssertOriginCall() - caller := std.GetOrigCaller() + // std.AssertOriginCall() + caller := std.PrevRealm().Addr() group := getGroup(gid) if !group.HasPermission(caller, DeletePermission) { panic("unauthorized to delete member") } group.deleteMember(mid) } + +func GetMemberWeightByAddress(gid GroupID, addr std.Address) int { + group := getGroup(gid) + if group == nil { + return 0 + } + member, ok := group.membersByAddress.Get(string(addr)) + if !ok { + return 0 + } + return member.(*Member).weight +} + +func GetGroupTotalWeight(gid GroupID) int { + group := getGroup(gid) + if group == nil { + return 0 + } + return group.totalWeight +} + +func GetMembers(gid GroupID) []Member { + group := getGroup(gid) + if group == nil { + return nil + } + members := []Member{} + group.members.Iterate("", "", func(key string, value interface{}) bool { + members = append(members, *(value.(*Member))) + return false + }) + return members +} diff --git a/examples/gno.land/r/demo/modboards/Makefile b/examples/gno.land/r/demo/modboards/Makefile new file mode 100644 index 00000000000..1fa6939c323 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/Makefile @@ -0,0 +1,36 @@ +realmpath=r/demo/modboards +realmuri=gno.land/$(realmpath) + +gnohome=$(HOME)/Code/gno + +chainid=dev +gnoland=localhost:26657 +gnoweb=http://localhost:8888 +wallet=onblock-test + +#chainid=test3 +#gnoland=test3.gno.land:36657 +#gnoweb=https://test3.gno.land +#wallet=dev + +.PHONY: deploy.realm +deploy.realm: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="10000000" \ + -broadcast="true" \ + -remote="$(gnoland)" \ + -chainid="$(chainid)" \ + -pkgdir="." \ + -pkgpath="$(realmuri)" \ + $(wallet) + +.PHONY: open.realm +open.realm: + open "$(gnoweb)/$(realmpath)" + + +.PHONY: open.realm-doc +open.realm-doc: + open "$(gnoweb)/$(realmpath)?help" \ No newline at end of file diff --git a/examples/gno.land/r/demo/modboards/README.md b/examples/gno.land/r/demo/modboards/README.md new file mode 100644 index 00000000000..66241130cac --- /dev/null +++ b/examples/gno.land/r/demo/modboards/README.md @@ -0,0 +1,136 @@ +This is a demo of Gno smart contract programming. This document was +constructed by Gno onto a smart contract hosted on the data Realm +name ["gno.land/r/boards"](https://gno.land/r/boards/) +([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/boards)). + + + +## Build `gnokey`, create your account, and interact with Gno. + +NOTE: Where you see `--remote gno.land:36657` here, that flag can be replaced +with `--remote localhost:26657` for local testnets. + +### Build `gnokey`. + +```bash +git clone git@github.com:gnolang/gno.git +cd ./gno +make +``` + +### Generate a seed/mnemonic code. + +```bash +./build/gnokey generate +``` + +NOTE: You can generate 24 words with any good bip39 generator. + +### Create a new account using your mnemonic. + +```bash +./build/gnokey add --recover KEYNAME +``` + +NOTE: `KEYNAME` is your key identifier, and should be changed. + +### Verify that you can see your account locally. + +```bash +./build/gnokey list +``` + +## Interact with the blockchain: + +### Get your current balance, account number, and sequence number. + +```bash +./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote gno.land:36657 +``` + +NOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`. + +### Acquire testnet tokens using the official faucet. + +Go to https://gno.land/faucet + +### Create a board with a smart contract call. + +NOTE: `BOARDNAME` will be the slug of the board, and should be changed. + +```bash +./build/gnokey maketx call -pkgpath "gno.land/r/boards" -func "CreateBoard" -args "BOARDNAME" -gas-fee "1000000ugnot" -gas-wanted "2000000" -broadcast -chainid testchain -remote gno.land:36657 KEYNAME +``` + +Interactive documentation: https://gno.land/r/boards?help&__func=CreateBoard + +Next, query for the permanent board ID by querying (you need this to create a new post): + +```bash +./build/gnokey query "vm/qeval" -data "gno.land/r/boards +GetBoardIDFromName(\"BOARDNAME\")" -remote gno.land:36657 +``` + +### Create a post of a board with a smart contract call. + +NOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased. + +```bash +./build/gnokey maketx call -pkgpath "gno.land/r/boards" -func "CreateThread" -args BOARD_ID -args "Hello gno.land" -args\#file "./examples/gno.land/r/boards/example_post.md" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid testchain -remote gno.land:36657 KEYNAME +``` + +Interactive documentation: https://gno.land/r/boards?help&__func=CreateThread + +### Create a comment to a post. + +```bash +./build/gnokey maketx call -pkgpath "gno.land/r/boards" -func "CreateReply" -args "BOARD_ID" -args "1" -args "1" -args "Nice to meet you too." -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid testchain -remote gno.land:36657 KEYNAME +``` + +Interactive documentation: https://gno.land/r/boards?help&__func=CreateReply + +```bash +./build/gnokey query "vm/qrender" -data "gno.land/r/boards +BOARDNAME/1" -remote gno.land:36657 +``` + +### Render page with optional path expression. + +The contents of `https://gno.land/r/boards:` and `https://gno.land/r/boards:gnolang` are rendered by calling +the `Render(path string)` function like so: + +```bash +./build/gnokey query "vm/qrender" -data "gno.land/r/boards +gnolang" +``` + +## Starting a local `gnoland` node: + +### Add test account. + +```bash +./build/gnokey add -recover test1 +``` + +Use this mneonic: +> source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast + +### Start `gnoland` node. + +```bash +./build/gnoland +``` + +NOTE: This can be reset with `make reset` + +### Publish the "gno.land/p/demo/cpavl" package. + +```bash +./build/gnokey maketx addpkg -pkgpath "gno.land/p/demo/cpavl" -pkgdir "examples/gno.land/p/demo/avl" -deposit 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 test1 +``` + +### Publish the "gno.land/r/boards" realm package. + +```bash +./build/gnokey maketx addpkg -pkgpath "gno.land/r/boards" -pkgdir "examples/gno.land/r/boards" -deposit 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 300000000 -broadcast -chainid dev -remote localhost:26657 test1 +``` diff --git a/examples/gno.land/r/demo/modboards/board.gno b/examples/gno.land/r/demo/modboards/board.gno new file mode 100644 index 00000000000..14a84e5cc3b --- /dev/null +++ b/examples/gno.land/r/demo/modboards/board.gno @@ -0,0 +1,172 @@ +package boards + +import ( + "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/cpavl" + "gno.land/p/demo/flags_index" +) + +//---------------------------------------- +// Board + +type BoardID uint64 + +func (bid BoardID) String() string { + return strconv.Itoa(int(bid)) +} + +type Board struct { + id BoardID // only set for public boards. + url string + name string + creator std.Address + threads avl.Tree // Post.id -> *Post + postsCtr uint64 // increments Post.id + createdAt time.Time + deleted avl.Tree // TODO reserved for fast-delete. + flags *flags_index.FlagsIndex +} + +func newBoard(id BoardID, url string, name string, creator std.Address) *Board { + if !reName.MatchString(name) { + panic("invalid name: " + name) + } + exists := gBoardsByName.Has(name) + if exists { + panic("board already exists") + } + return &Board{ + id: id, + url: url, + name: name, + creator: creator, + threads: avl.Tree{}, + createdAt: time.Now(), + deleted: avl.Tree{}, + flags: flags_index.NewFlagsIndex(), + } +} + +/* TODO support this once we figure out how to ensure URL correctness. +// A private board is not tracked by gBoards*, +// but must be persisted by the caller's realm. +// Private boards have 0 id and does not ping +// back the remote board on reposts. +func NewPrivateBoard(url string, name string, creator std.Address) *Board { + return newBoard(0, url, name, creator) +} +*/ + +func (board *Board) IsPrivate() bool { + return board.id == 0 +} + +func (board *Board) GetThread(pid PostID) *Post { + pidkey := postIDKey(pid) + postI, exists := board.threads.Get(pidkey) + if !exists { + return nil + } + return postI.(*Post) +} + +func (board *Board) AddThread(creator std.Address, title string, body string) *Post { + pid := board.incGetPostID() + pidkey := postIDKey(pid) + thread := newPost(board, pid, creator, title, body, pid, 0, 0) + board.threads.Set(pidkey, thread) + return thread +} + +// NOTE: this can be potentially very expensive for threads with many replies. +// TODO: implement optional fast-delete where thread is simply moved. +func (board *Board) DeleteThread(pid PostID) { + pidkey := postIDKey(pid) + _, removed := board.threads.Remove(pidkey) + if !removed { + panic("thread does not exist with id " + pid.String()) + } +} + +func (board *Board) HasPermission(addr std.Address, perm Permission) bool { + if board.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + return false +} + +// Renders the board for display suitable as plaintext in +// console. This is suitable for demonstration or tests, +// but not for prod. +func (board *Board) RenderBoard() string { + str := "" + str += "\\[[post](" + board.GetPostFormURL() + ")]" + + "\\[[see flags](" + board.GetFlagsURL() + ")]" + + "\n\n" + if board.threads.Size() > 0 { + board.threads.Iterate("", "", func(key string, value interface{}) bool { + str += "----------------------------------------\n" + str += value.(*Post).RenderSummary() + "\n" + return false + }) + } + return str +} + +func (board *Board) RenderFlags(limit uint64, offset uint64) string { + flagsCounts := board.flags.GetFlags(limit, offset) + str := "" + for _, flagCount := range flagsCounts { + if str != "" { + str += "----------------------------------------\n" + } + str += "Flag ID: " + string(flagCount.FlagID) + "\n" + str += "Count: " + strconv.FormatUint(flagCount.Count, 10) + "\n" + threadID, postID := parseFlagID(flagCount.FlagID) + post := board.GetThread(threadID) + if threadID != postID { + post = post.GetReply(postID) + } + str += post.RenderSummary() + "\n" + } + return str +} + +func (board *Board) incGetPostID() PostID { + board.postsCtr++ + return PostID(board.postsCtr) +} + +func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string { + if replyID == 0 { + return board.url + "/" + threadID.String() + } else { + return board.url + "/" + threadID.String() + "/" + replyID.String() + } +} + +func (board *Board) GetPostFormURL() string { + return "/r/demo/modboards?help&__func=CreateThread" + + "&bid=" + board.id.String() + + "&body.type=textarea" +} + +func (board *Board) GetFlagsURL() string { + return "/r/demo/modboards:" + board.name + "/flags" +} + +func (board *Board) GetFlagFormURL(threadID PostID, postID PostID) string { + return "/r/demo/modboards?help&__func=FlagPost" + + "&boardID=" + board.id.String() + "&threadID=" + threadID.String() + "&postID=" + postID.String() +} diff --git a/examples/gno.land/r/demo/modboards/boards.gno b/examples/gno.land/r/demo/modboards/boards.gno new file mode 100644 index 00000000000..62e37ccf94e --- /dev/null +++ b/examples/gno.land/r/demo/modboards/boards.gno @@ -0,0 +1,22 @@ +package boards + +import ( + "regexp" + + "gno.land/p/demo/cpavl" +) + +//---------------------------------------- +// Realm (package) state + +var ( + gBoards avl.Tree // id -> *Board + gBoardsCtr int // increments Board.id + gBoardsByName avl.Tree // name -> *Board + gDefaultAnonFee = 100000000 // minimum fee required if anonymous +) + +//---------------------------------------- +// Constants + +var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`) diff --git a/examples/gno.land/r/demo/modboards/example_post.md b/examples/gno.land/r/demo/modboards/example_post.md new file mode 100644 index 00000000000..a452ad4c245 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/example_post.md @@ -0,0 +1,3 @@ +Hey all! 👋 + +This is my first post in this land! \ No newline at end of file diff --git a/examples/gno.land/r/demo/modboards/flags.gno b/examples/gno.land/r/demo/modboards/flags.gno new file mode 100644 index 00000000000..98430d57441 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/flags.gno @@ -0,0 +1,28 @@ +package boards + +import ( + "strconv" + "strings" + + "gno.land/p/demo/flags_index" +) + +func getFlagID(threadID PostID, postID PostID) flags_index.FlagID { + return flags_index.FlagID(threadID.String() + "-" + postID.String()) +} + +func parseFlagID(flagID flags_index.FlagID) (PostID, PostID) { + parts := strings.Split(string(flagID), "-") + if len(parts) != 2 { + panic("invalid flag ID '" + string(flagID) + "'") + } + threadID, err := strconv.Atoi(parts[0]) + if err != nil || threadID == 0 { + panic("invalid thread ID in flag ID '" + parts[0] + "'") + } + postID, err := strconv.Atoi(parts[1]) + if err != nil || postID == 0 { + panic("invalid post ID in flag ID '" + parts[1] + "'") + } + return PostID(threadID), PostID(postID) +} diff --git a/examples/gno.land/r/demo/modboards/gno.mod b/examples/gno.land/r/demo/modboards/gno.mod new file mode 100644 index 00000000000..fbb07721dab --- /dev/null +++ b/examples/gno.land/r/demo/modboards/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/modboards + +require ( + "gno.land/p/demo/cpavl" v0.0.0-latest + "gno.land/r/demo/users" v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/modboards/messages.gno b/examples/gno.land/r/demo/modboards/messages.gno new file mode 100644 index 00000000000..b51a35e408e --- /dev/null +++ b/examples/gno.land/r/demo/modboards/messages.gno @@ -0,0 +1,228 @@ +package boards + +import ( + "encoding/binary" + "std" + "strconv" + "strings" + + "gno.land/p/demo/daodao/interfaces" +) + +// Create board + +type ExecutableMessageCreateBoard struct { + dao_interfaces.ExecutableMessage + + Name string +} + +func (msg *ExecutableMessageCreateBoard) Type() string { + return "CreateBoard" +} + +func (msg *ExecutableMessageCreateBoard) String() string { + var ss []string + ss = append(ss, msg.Type()) + ss = append(ss, "Name: "+msg.Name) + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageCreateBoard) Binary() []byte { + b := []byte{} + + t := msg.Type() + b = binary.BigEndian.AppendUint16(b, uint16(len(t))) + b = append(b, []byte(t)...) + + b = binary.BigEndian.AppendUint16(b, uint16(len(msg.Name))) + b = append(b, []byte(msg.Name)...) + + return b +} + +func ExecutableMessageCreateBoardFromBinary(b []byte) *ExecutableMessageCreateBoard { + msg := &ExecutableMessageCreateBoard{} + + if len(b) < 2 { + panic("invalid length - less than 2") + } + tl := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(tl) { + panic("invalid length - less than expected") + } + t := string(b[:tl]) + if t != msg.Type() { + panic("invalid type") + } + b = b[tl:] + + nl := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(nl) { + panic("invalid length - less than expected") + } + n := string(b[:nl]) + // b = b[nl:] + msg.Name = n + + return msg +} + +type CreateBoardHandler struct { + dao_interfaces.MessageHandler +} + +func NewCreateBoardHandler() *CreateBoardHandler { + return &CreateBoardHandler{} +} + +func (h *CreateBoardHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageCreateBoard) + CreateBoard(msg.Name) +} + +func (h *CreateBoardHandler) Type() string { + return ExecutableMessageCreateBoard{}.Type() +} + +func (h *CreateBoardHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageCreateBoardFromBinary(b) +} + +// Delete post + +type ExecutableMessageDeletePost struct { + dao_interfaces.ExecutableMessage + + BoardID BoardID + ThreadID PostID + PostID PostID + Reason string +} + +func (msg *ExecutableMessageDeletePost) Type() string { + return "DeletePost" +} + +func (msg *ExecutableMessageDeletePost) String() string { + var ss []string + ss = append(ss, msg.Type()) + + board, ok := getBoard(msg.BoardID).(*Board) + s := "" + + if ok { + s += "Board: " + board.name + " (" + board.id.String() + ")" + + thread := board.GetThread(msg.ThreadID) + if thread != nil { + s += "\nThread: " + thread.title + " (" + thread.id.String() + ")" + } else { + s += "\nThread: " + msg.ThreadID.String() + " (not found)" + } + + if msg.PostID != msg.ThreadID { + post := thread.GetReply(msg.PostID) + if post != nil { + s += "\nPost: " + post.title + " (" + post.id.String() + ")" + } else { + s += "\nPost: " + msg.PostID.String() + " (not found)" + } + } + } else { + s += "Board: " + msg.BoardID.String() + " (not found)" + } + + s += "\nReason: " + msg.Reason + + ss = append(ss, s) + + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageDeletePost) Binary() []byte { + b := []byte{} + + t := msg.Type() + b = binary.BigEndian.AppendUint16(b, uint16(len(t))) + b = append(b, []byte(t)...) + + b = binary.BigEndian.AppendUint64(b, uint64(msg.BoardID)) + b = binary.BigEndian.AppendUint64(b, uint64(msg.ThreadID)) + b = binary.BigEndian.AppendUint64(b, uint64(msg.PostID)) + + b = binary.BigEndian.AppendUint16(b, uint16(len(msg.Reason))) + b = append(b, []byte(msg.Reason)...) + + return b +} + +func ExecutableMessageDeletePostFromBinary(b []byte) *ExecutableMessageDeletePost { + msg := &ExecutableMessageDeletePost{} + + if len(b) < 2 { + panic("invalid length - less than 2") + } + tl := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(tl) { + panic("invalid length - less than expected") + } + t := string(b[:tl]) + if t != msg.Type() { + panic("invalid type") + } + b = b[tl:] + + if len(b) < 8 { + panic("invalid length - less than 8") + } + msg.BoardID = BoardID(binary.BigEndian.Uint64(b[:8])) + b = b[8:] + + if len(b) < 8 { + panic("invalid length - less than 8") + } + msg.ThreadID = PostID(binary.BigEndian.Uint64(b[:8])) + b = b[8:] + + if len(b) < 8 { + panic("invalid length - less than 8") + } + msg.PostID = PostID(binary.BigEndian.Uint64(b[:8])) + b = b[8:] + + rl := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(rl) { + panic("invalid length - less than expected") + } + r := string(b[:rl]) + msg.Reason = r + // b = b[rl:] + + return msg +} + +type DeletePostHandler struct { + dao_interfaces.MessageHandler +} + +func NewDeletePostHandler() *DeletePostHandler { + return &DeletePostHandler{} +} + +func (h *DeletePostHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageDeletePost) + DeletePost(msg.BoardID, msg.ThreadID, msg.PostID, msg.Reason) +} + +func (h *DeletePostHandler) Type() string { + return ExecutableMessageDeletePost{}.Type() +} + +func (h *DeletePostHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageDeletePostFromBinary(b) +} diff --git a/examples/gno.land/r/demo/modboards/misc.gno b/examples/gno.land/r/demo/modboards/misc.gno new file mode 100644 index 00000000000..e6235e1d0ab --- /dev/null +++ b/examples/gno.land/r/demo/modboards/misc.gno @@ -0,0 +1,96 @@ +package boards + +import ( + "std" + "strconv" + "strings" + + "gno.land/r/demo/users" +) + +//---------------------------------------- +// private utility methods +// XXX ensure these cannot be called from public. + +func getBoard(bid BoardID) *Board { + bidkey := boardIDKey(bid) + board_, exists := gBoards.Get(bidkey) + if !exists { + return nil + } + board := board_.(*Board) + return board +} + +func incGetBoardID() BoardID { + gBoardsCtr++ + return BoardID(gBoardsCtr) +} + +func padLeft(str string, length int) string { + if len(str) >= length { + return str + } else { + return strings.Repeat(" ", length-len(str)) + str + } +} + +func padZero(u64 uint64, length int) string { + str := strconv.Itoa(int(u64)) + if len(str) >= length { + return str + } else { + return strings.Repeat("0", length-len(str)) + str + } +} + +func boardIDKey(bid BoardID) string { + return padZero(uint64(bid), 10) +} + +func postIDKey(pid PostID) string { + return padZero(uint64(pid), 10) +} + +func indentBody(indent string, body string) string { + lines := strings.Split(body, "\n") + res := "" + for i, line := range lines { + if i > 0 { + res += "\n" + } + res += indent + line + } + return res +} + +// NOTE: length must be greater than 3. +func summaryOf(str string, length int) string { + lines := strings.SplitN(str, "\n", 2) + line := lines[0] + if len(line) > length { + line = line[:(length-3)] + "..." + } else if len(lines) > 1 { + // len(line) <= 80 + line = line + "..." + } + return line +} + +func displayAddressMD(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "[" + addr.String() + "](/r/users:" + addr.String() + ")" + } else { + return "[@" + user.Name() + "](/r/users:" + user.Name() + ")" + } +} + +func usernameOf(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "" + } else { + return user.Name() + } +} diff --git a/examples/gno.land/r/demo/modboards/post.gno b/examples/gno.land/r/demo/modboards/post.gno new file mode 100644 index 00000000000..da3a3b1ba4b --- /dev/null +++ b/examples/gno.land/r/demo/modboards/post.gno @@ -0,0 +1,263 @@ +package boards + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/cpavl" + "gno.land/p/demo/flags_index" +) + +//---------------------------------------- +// Post + +// NOTE: a PostID is relative to the board. +type PostID uint64 + +func (pid PostID) String() string { + return strconv.Itoa(int(pid)) +} + +// A Post is a "thread" or a "reply" depending on context. +// A thread is a Post of a Board that holds other replies. +type Post struct { + board *Board + id PostID + creator std.Address + title string // optional + body string + replies avl.Tree // Post.id -> *Post + repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) + reposts avl.Tree // Board.id -> Post.id + threadID PostID // original Post.id + parentID PostID // parent Post.id (if reply or repost) + repostBoard BoardID // original Board.id (if repost) + createdAt time.Time + updatedAt time.Time +} + +func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post { + return &Post{ + board: board, + id: id, + creator: creator, + title: title, + body: body, + replies: avl.Tree{}, + repliesAll: avl.Tree{}, + reposts: avl.Tree{}, + threadID: threadID, + parentID: parentID, + repostBoard: repostBoard, + createdAt: time.Now(), + } +} + +func (post *Post) IsThread() bool { + return post.parentID == 0 +} + +func (post *Post) GetPostID() PostID { + return post.id +} + +func (post *Post) AddReply(creator std.Address, body string) *Post { + board := post.board + pid := board.incGetPostID() + pidkey := postIDKey(pid) + reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0) + post.replies.Set(pidkey, reply) + if post.threadID == post.id { + post.repliesAll.Set(pidkey, reply) + } else { + thread := board.GetThread(post.threadID) + thread.repliesAll.Set(pidkey, reply) + } + return reply +} + +func (post *Post) Update(title string, body string) { + post.title = title + post.body = body + post.updatedAt = time.Now() +} + +func (thread *Post) GetReply(pid PostID) *Post { + pidkey := postIDKey(pid) + replyI, ok := thread.repliesAll.Get(pidkey) + if !ok { + return nil + } else { + return replyI.(*Post) + } +} + +func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { + if !post.IsThread() { + panic("cannot repost non-thread post") + } + pid := dst.incGetPostID() + pidkey := postIDKey(pid) + repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id) + dst.threads.Set(pidkey, repost) + if !dst.IsPrivate() { + bidkey := boardIDKey(dst.id) + post.reposts.Set(bidkey, pid) + } + return repost +} + +func (thread *Post) DeletePost(pid PostID) { + if thread.id == pid { + panic("should not happen") + } + pidkey := postIDKey(pid) + postI, removed := thread.repliesAll.Remove(pidkey) + if !removed { + panic("post not found in thread") + } + post := postI.(*Post) + if post.parentID != thread.id { + parent := thread.GetReply(post.parentID) + parent.replies.Remove(pidkey) + } else { + thread.replies.Remove(pidkey) + } +} + +func (post *Post) HasPermission(addr std.Address, perm Permission) bool { + if post.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + // post notes inherit permissions of the board. + return post.board.HasPermission(addr, perm) +} + +func (post *Post) GetSummary() string { + return summaryOf(post.body, 80) +} + +func (post *Post) GetURL() string { + if post.IsThread() { + return post.board.GetURLFromThreadAndReplyID( + post.id, 0) + } else { + return post.board.GetURLFromThreadAndReplyID( + post.threadID, post.id) + } +} + +func (post *Post) GetReplyFormURL() string { + return "/r/demo/modboards?help&__func=CreateReply" + + "&bid=" + post.board.id.String() + + "&threadid=" + post.threadID.String() + + "&postid=" + post.id.String() + + "&body.type=textarea" +} + +func (post *Post) GetDeleteFormURL() string { + return "/r/demo/modboards?help&__func=DeletePost" + + "&bid=" + post.board.id.String() + + "&threadid=" + post.threadID.String() + + "&postid=" + post.id.String() +} + +func (post *Post) GetFlagFormURL() string { + return post.board.GetFlagFormURL(post.threadID, post.id) +} + +func (post *Post) RenderSummary() string { + if post == nil { + return "nil post" + } + str := "" + if post.title != "" { + str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" + str += "\n" + } + str += post.GetSummary() + "\n" + str += "\\- " + displayAddressMD(post.creator) + "," + str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")" + str += " \\[[🚩](" + post.GetFlagFormURL() + ")]" + str += " \\[[x](" + post.GetDeleteFormURL() + ")]" + str += " (" + strconv.Itoa(post.replies.Size()) + " replies" + flagCount := post.getFlagCount() + if flagCount > 0 { + str += ", " + strconv.FormatUint(flagCount, 10) + " red flags" + } + str += ")\n" + return str +} + +func (post *Post) RenderPost(indent string, levels int) string { + if post == nil { + return "nil post" + } + str := "" + if post.title != "" { + str += indent + "# " + post.title + "\n" + str += indent + "\n" + } + str += indentBody(indent, post.body) + "\n" // TODO: indent body lines. + str += indent + "\\- " + displayAddressMD(post.creator) + ", " + str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")" + str += " \\[[reply](" + post.GetReplyFormURL() + ")]" + str += " \\[[🚩](" + post.GetFlagFormURL() + ")]" + str += " \\[[x](" + post.GetDeleteFormURL() + ")]" + flagCount := post.getFlagCount() + if flagCount > 0 { + str += " (" + strconv.FormatUint(flagCount, 10) + " red flags)" + } + str += "\n" + if levels > 0 { + if post.replies.Size() > 0 { + post.replies.Iterate("", "", func(key string, value interface{}) bool { + str += indent + "\n" + str += value.(*Post).RenderPost(indent+"> ", levels-1) + return false + }) + } + } else { + if post.replies.Size() > 0 { + str += indent + "\n" + str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" + } + } + return str +} + +// render reply and link to context thread +func (post *Post) RenderInner() string { + if post.IsThread() { + panic("unexpected thread") + } + threadID := post.threadID + // replyID := post.id + parentID := post.parentID + str := "" + str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID( + threadID, 0) + ")_\n\n" + thread := post.board.GetThread(post.threadID) + var parent *Post + if thread.id == parentID { + parent = thread + } else { + parent = thread.GetReply(parentID) + } + str += parent.RenderPost("", 0) + str += "\n" + str += post.RenderPost("> ", 5) + return str +} + +func (post *Post) getFlagCount() uint64 { + return post.board.flags.GetFlagCount(getFlagID(post.threadID, post.id)) +} diff --git a/examples/gno.land/r/demo/modboards/public.gno b/examples/gno.land/r/demo/modboards/public.gno new file mode 100644 index 00000000000..09daa4d6185 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/public.gno @@ -0,0 +1,193 @@ +package boards + +import ( + "std" + "strconv" + + "gno.land/p/demo/flags_index" +) + +//---------------------------------------- +// Public facing functions + +func GetBoardIDFromName(name string) (BoardID, bool) { + boardI, exists := gBoardsByName.Get(name) + if !exists { + return 0, false + } + return boardI.(*Board).id, true +} + +func CreateBoard(name string) BoardID { + bid := incGetBoardID() + caller := std.PrevRealm().Addr() + url := "/r/demo/modboards:" + name + board := newBoard(bid, url, name, caller) + bidkey := boardIDKey(bid) + gBoards.Set(bidkey, board) + gBoardsByName.Set(name, board) + return board.id +} + +func checkAnonFee() bool { + sent := std.GetOrigSend() + anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} + if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { + return true + } + return false +} + +func CreateThread(bid BoardID, title string, body string) PostID { + std.AssertOriginCall() + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.AddThread(caller, title, body) + return thread.id +} + +func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { + std.AssertOriginCall() + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + reply := thread.AddReply(caller, body) + return reply.id + } else { + post := thread.GetReply(postid) + reply := post.AddReply(caller, body) + return reply.id + } +} + +// If dstBoard is private, does not ping back. +// If board specified by bid is private, panics. +func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { + std.AssertOriginCall() + caller := std.GetOrigCaller() + if usernameOf(caller) == "" { + // TODO: allow with gDefaultAnonFee payment. + if !checkAnonFee() { + panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + } + } + board := getBoard(bid) + if board == nil { + panic("src board not exist") + } + if board.IsPrivate() { + panic("cannot repost from a private board") + } + dst := getBoard(dstBoardID) + if dst == nil { + panic("dst board not exist") + } + thread := board.GetThread(postid) + if thread == nil { + panic("thread not exist") + } + repost := thread.AddRepostTo(caller, title, body, dst) + return repost.id +} + +func DeletePost(bid BoardID, threadid, postid PostID, reason string) { + caller := std.PrevRealm().Addr() + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + // delete thread + if !thread.HasPermission(caller, DeletePermission) { + panic("unauthorized") + } + board.DeleteThread(threadid) + } else { + // delete thread's post + post := thread.GetReply(postid) + if post == nil { + panic("post not exist") + } + if !post.HasPermission(caller, DeletePermission) { + panic("unauthorized") + } + thread.DeletePost(postid) + } + board.flags.ClearFlagCount(getFlagID(threadid, postid)) +} + +func EditPost(bid BoardID, threadid, postid PostID, title, body string) { + std.AssertOriginCall() + caller := std.GetOrigCaller() + board := getBoard(bid) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + // edit thread + if !thread.HasPermission(caller, EditPermission) { + panic("unauthorized") + } + thread.Update(title, body) + } else { + // edit thread's post + post := thread.GetReply(postid) + if post == nil { + panic("post not exist") + } + if !post.HasPermission(caller, EditPermission) { + panic("unauthorized") + } + post.Update(title, body) + } +} + +// Maybe we should require a deposit that is returned if the post is deleted +func FlagPost(boardID BoardID, threadID PostID, postID PostID) { + // check that the target exists + board := getBoard(boardID) + if board == nil { + panic("board not exist") + } + thread := board.GetThread(threadID) + if thread == nil { + panic("thread not exist") + } + if postID != threadID { + post := thread.GetReply(postID) + if post == nil { + panic("post not exist") + } + } + + // flag + board.flags.Flag(getFlagID(threadID, postID), std.PrevRealm().Addr().String()) +} diff --git a/examples/gno.land/r/demo/modboards/render.gno b/examples/gno.land/r/demo/modboards/render.gno new file mode 100644 index 00000000000..d8604c4cdcd --- /dev/null +++ b/examples/gno.land/r/demo/modboards/render.gno @@ -0,0 +1,92 @@ +package boards + +import ( + "strconv" + "strings" +) + +//---------------------------------------- +// Render functions + +func RenderBoard(bid BoardID) string { + board := getBoard(bid) + if board == nil { + return "missing board" + } + return board.RenderBoard() +} + +func Render(path string) string { + if path == "" { + str := "These are all the boards of this realm:\n\n" + gBoards.Iterate("", "", func(key string, value interface{}) bool { + board := value.(*Board) + str += " * [" + board.url + "](" + board.url + ")\n" + return false + }) + return str + } + parts := strings.Split(path, "/") + if len(parts) == 1 { + // /r/demo/modboards:BOARD_NAME + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + return boardI.(*Board).RenderBoard() + } else if len(parts) == 2 { + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + board := boardI.(*Board) + + if parts[1] == "flags" { + // /r/demo/modboards:BOARD_NAME/flags + return board.RenderFlags(1000, 0) // TODO: pagination + } else if parts[1] == "dump-flags" { + // /r/demo/modboards:BOARD_NAME/dump-flags + return board.flags.Dump() + } + + // /r/demo/modboards:BOARD_NAME/THREAD_ID + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + thread := board.GetThread(PostID(pid)) + if thread == nil { + return "thread does not exist with id: " + parts[1] + } + return thread.RenderPost("", 5) + } else if len(parts) == 3 { + // /r/demo/modboards:BOARD_NAME/THREAD_ID/REPLY_ID + name := parts[0] + boardI, exists := gBoardsByName.Get(name) + if !exists { + return "board does not exist: " + name + } + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + board := boardI.(*Board) + thread := board.GetThread(PostID(pid)) + if thread == nil { + return "thread does not exist with id: " + parts[1] + } + rid, err := strconv.Atoi(parts[2]) + if err != nil { + return "invalid reply id: " + parts[2] + } + reply := thread.GetReply(PostID(rid)) + if reply == nil { + return "reply does not exist with id: " + parts[2] + } + return reply.RenderInner() + } else { + return "unrecognized path " + path + } +} diff --git a/examples/gno.land/r/demo/modboards/role.gno b/examples/gno.land/r/demo/modboards/role.gno new file mode 100644 index 00000000000..64073d64f34 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/role.gno @@ -0,0 +1,8 @@ +package boards + +type Permission string + +const ( + DeletePermission Permission = "role:delete" + EditPermission Permission = "role:edit" +) diff --git a/examples/gno.land/r/demo/modboards/z_0_a_filetest.gno b/examples/gno.land/r/demo/modboards/z_0_a_filetest.gno new file mode 100644 index 00000000000..54d56a2f8b8 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_0_a_filetest.gno @@ -0,0 +1,22 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +import ( + "gno.land/r/demo/modboards" +) + +var bid boards.BoardID + +func init() { + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid := boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + boards.CreateReply(bid, pid, pid, "Reply of the second post") +} + +func main() { + println(boards.Render("test_board")) +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/demo/modboards/z_0_b_filetest.gno b/examples/gno.land/r/demo/modboards/z_0_b_filetest.gno new file mode 100644 index 00000000000..8f47076df14 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_0_b_filetest.gno @@ -0,0 +1,23 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 199000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var bid boards.BoardID + +func init() { + users.Register("", "gnouser", "my profile") + bid = boards.CreateBoard("test_board") +} + +func main() { + println(boards.Render("test_board")) +} + +// Error: +// payment must not be less than 200000000 diff --git a/examples/gno.land/r/demo/modboards/z_0_c_filetest.gno b/examples/gno.land/r/demo/modboards/z_0_c_filetest.gno new file mode 100644 index 00000000000..8378379c817 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_0_c_filetest.gno @@ -0,0 +1,23 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var bid boards.BoardID + +func init() { + users.Register("", "gnouser", "my profile") + boards.CreateThread(1, "First Post (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board")) +} + +// Error: +// board not exist diff --git a/examples/gno.land/r/demo/modboards/z_0_d_filetest.gno b/examples/gno.land/r/demo/modboards/z_0_d_filetest.gno new file mode 100644 index 00000000000..702d8e5b322 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_0_d_filetest.gno @@ -0,0 +1,24 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var bid boards.BoardID + +func init() { + users.Register("", "gnouser", "my profile") + bid = boards.CreateBoard("test_board") + boards.CreateReply(bid, 0, 0, "Reply of the second post") +} + +func main() { + println(boards.Render("test_board")) +} + +// Error: +// thread not exist diff --git a/examples/gno.land/r/demo/modboards/z_0_e_filetest.gno b/examples/gno.land/r/demo/modboards/z_0_e_filetest.gno new file mode 100644 index 00000000000..f2806324ec5 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_0_e_filetest.gno @@ -0,0 +1,23 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var bid boards.BoardID + +func init() { + users.Register("", "gnouser", "my profile") + boards.CreateReply(bid, 0, 0, "Reply of the second post") +} + +func main() { + println(boards.Render("test_board")) +} + +// Error: +// board not exist diff --git a/examples/gno.land/r/demo/modboards/z_0_filetest.gno b/examples/gno.land/r/demo/modboards/z_0_filetest.gno new file mode 100644 index 00000000000..7e7a0ce0b66 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_0_filetest.gno @@ -0,0 +1,39 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var bid boards.BoardID + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid := boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + boards.CreateReply(bid, pid, pid, "Reply of the second post") +} + +func main() { + println(boards.Render("test_board")) +} + +// Output: +// \[[post](/r/demo/modboards?help&__func=CreateThread&bid=1&body.type=textarea)] +// +// ---------------------------------------- +// ## [First Post (title)](/r/demo/modboards:test_board/1) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/modboards:test_board/1) \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) +// +// ---------------------------------------- +// ## [Second Post (title)](/r/demo/modboards:test_board/2) +// +// Body of the second post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/modboards:test_board/2) \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) diff --git a/examples/gno.land/r/demo/modboards/z_10_a_filetest.gno b/examples/gno.land/r/demo/modboards/z_10_a_filetest.gno new file mode 100644 index 00000000000..bee6fd1ca65 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_10_a_filetest.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + // boardId 2 not exist + boards.DeletePost(2, pid, pid, "") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// board not exist diff --git a/examples/gno.land/r/demo/modboards/z_10_b_filetest.gno b/examples/gno.land/r/demo/modboards/z_10_b_filetest.gno new file mode 100644 index 00000000000..2de671571ff --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_10_b_filetest.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + // pid of 2 not exist + boards.DeletePost(bid, 2, 2, "") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// thread not exist diff --git a/examples/gno.land/r/demo/modboards/z_10_c_filetest.gno b/examples/gno.land/r/demo/modboards/z_10_c_filetest.gno new file mode 100644 index 00000000000..765f5463861 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_10_c_filetest.gno @@ -0,0 +1,48 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID + rid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") + rid = boards.CreateReply(bid, pid, pid, "First reply of the First post\n") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + boards.DeletePost(bid, pid, rid, "") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// +// > First reply of the First post +// > +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] +// +// ---------------------------------------------------- +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] diff --git a/examples/gno.land/r/demo/modboards/z_10_filetest.gno b/examples/gno.land/r/demo/modboards/z_10_filetest.gno new file mode 100644 index 00000000000..68a9f69cf64 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_10_filetest.gno @@ -0,0 +1,39 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + boards.DeletePost(bid, pid, pid, "") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// +// ---------------------------------------------------- +// thread does not exist with id: 1 diff --git a/examples/gno.land/r/demo/modboards/z_11_a_filetest.gno b/examples/gno.land/r/demo/modboards/z_11_a_filetest.gno new file mode 100644 index 00000000000..c4bf2a01bb1 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_11_a_filetest.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + // board 2 not exist + boards.EditPost(2, pid, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// board not exist diff --git a/examples/gno.land/r/demo/modboards/z_11_b_filetest.gno b/examples/gno.land/r/demo/modboards/z_11_b_filetest.gno new file mode 100644 index 00000000000..0f3cb9baa24 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_11_b_filetest.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + // thread 2 not exist + boards.EditPost(bid, 2, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// thread not exist diff --git a/examples/gno.land/r/demo/modboards/z_11_c_filetest.gno b/examples/gno.land/r/demo/modboards/z_11_c_filetest.gno new file mode 100644 index 00000000000..3759eb9229a --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_11_c_filetest.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + // post 2 not exist + boards.EditPost(bid, pid, 2, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// post not exist diff --git a/examples/gno.land/r/demo/modboards/z_11_d_filetest.gno b/examples/gno.land/r/demo/modboards/z_11_d_filetest.gno new file mode 100644 index 00000000000..9be9dcb1261 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_11_d_filetest.gno @@ -0,0 +1,52 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID + rid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") + rid = boards.CreateReply(bid, pid, pid, "First reply of the First post\n") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + boards.EditPost(bid, pid, rid, "", "Edited: First reply of the First post\n") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// +// > First reply of the First post +// > +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] +// +// ---------------------------------------------------- +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// +// > Edited: First reply of the First post +// > +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] diff --git a/examples/gno.land/r/demo/modboards/z_11_filetest.gno b/examples/gno.land/r/demo/modboards/z_11_filetest.gno new file mode 100644 index 00000000000..12248751f18 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_11_filetest.gno @@ -0,0 +1,42 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + pid = boards.CreateThread(bid, "First Post in (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + boards.EditPost(bid, pid, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") + println("----------------------------------------------------") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// +// ---------------------------------------------------- +// # Edited: First Post in (title) +// +// Edited: Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] diff --git a/examples/gno.land/r/demo/modboards/z_1_filetest.gno b/examples/gno.land/r/demo/modboards/z_1_filetest.gno new file mode 100644 index 00000000000..2628a78b016 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_1_filetest.gno @@ -0,0 +1,28 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var board *boards.Board + +func init() { + users.Register("", "gnouser", "my profile") + + _ = boards.CreateBoard("test_board_1") + _ = boards.CreateBoard("test_board_2") +} + +func main() { + println(boards.Render("")) +} + +// Output: +// These are all the boards of this realm: +// +// * [/r/demo/modboards:test_board_1](/r/demo/modboards:test_board_1) +// * [/r/demo/modboards:test_board_2](/r/demo/modboards:test_board_2) diff --git a/examples/gno.land/r/demo/modboards/z_2_filetest.gno b/examples/gno.land/r/demo/modboards/z_2_filetest.gno new file mode 100644 index 00000000000..d0773befe0a --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_2_filetest.gno @@ -0,0 +1,38 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid = boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + boards.CreateReply(bid, pid, pid, "Reply of the second post") +} + +func main() { + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # Second Post (title) +// +// Body of the second post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// +// > Reply of the second post +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/3) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] diff --git a/examples/gno.land/r/demo/modboards/z_3_filetest.gno b/examples/gno.land/r/demo/modboards/z_3_filetest.gno new file mode 100644 index 00000000000..36c99e42bac --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_3_filetest.gno @@ -0,0 +1,40 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid = boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") +} + +func main() { + rid := boards.CreateReply(bid, pid, pid, "Reply of the second post") + println(rid) + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// 3 +// # Second Post (title) +// +// Body of the second post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// +// > Reply of the second post +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/3) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] diff --git a/examples/gno.land/r/demo/modboards/z_4_filetest.gno b/examples/gno.land/r/demo/modboards/z_4_filetest.gno new file mode 100644 index 00000000000..f6d3e2a34fd --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_4_filetest.gno @@ -0,0 +1,972 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid = boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + rid := boards.CreateReply(bid, pid, pid, "Reply of the second post") + println(rid) +} + +func main() { + rid2 := boards.CreateReply(bid, pid, pid, "Second reply of the second post") + println(rid2) + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// 3 +// 4 +// # Second Post (title) +// +// Body of the second post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// +// > Reply of the second post +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/3) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// +// > Second reply of the second post +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/4) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=4&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=4)] + +// Realm: +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/modboards"] +// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:101]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "0000000003" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:102" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:101", +// "ModTime": "109", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:109", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:110]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "0000000004" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:110", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:109", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:109]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "0000000004" +// } +// }, +// {}, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "b58581159917d8d7ad0992009d7184fc8ca00fcc", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:101" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "fb593e86d35aaf607e0d21e6bd4f84519c44585f", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:110" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:109", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:96", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:112]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:112", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:113]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:113", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:114]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:114", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:115]={ +// "Fields": [ +// { +// "N": "AAAAgJSeXbo=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "65536" +// } +// }, +// { +// "N": "AbSNdvQQIhE=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "1024" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "time.Location" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "336074805fc853987abe6f7fe3ad97a6a6f3077a:2" +// }, +// "Index": "188", +// "TV": null +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:115", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:116]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "65536" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "1024" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "time.Location" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:116", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Board" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Board" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:81" +// } +// } +// } +// }, +// { +// "N": "BAAAAAAAAAA=", +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.PostID" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "std.Address" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "Second reply of the second post" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Tree" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "130542396d7549d1d516a3ef4a63bb44ef3da06f", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:112" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Tree" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "80acd8746478317194b8546170335c796a4dfb3f", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:113" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Tree" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "c1a8f769f3b9d52dd38ac4759116edaca287636f", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:114" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.PostID" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.PostID" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.BoardID" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "time.Time" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "8164abed5231309c88497013f7da72a1b5d427b0", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:115" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "time.Time" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "c3a60b602b564d07677a212372f4ac1cae4270fd", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:116" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111", +// "IsEscaped": true, +// "ModTime": "0", +// "RefCount": "2" +// } +// } +// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:108]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "0000000003" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:102" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:108", +// "ModTime": "117", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:117", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:118]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "0000000004" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.Post" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:118", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:117", +// "RefCount": "1" +// } +// } +// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:117]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "0000000004" +// } +// }, +// {}, +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "64" +// } +// }, +// { +// "N": "AgAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "6a86bc7763703c8f2b9d286368921159d6db121c", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:108" +// } +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "52faa8a2dfefd4b6b6249eff2f9c123ad455e81d", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:118" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:117", +// "ModTime": "0", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:97", +// "RefCount": "1" +// } +// } +// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:81]={ +// "Fields": [ +// { +// "N": "AQAAAAAAAAA=", +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/modboards.BoardID" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "/r/demo/modboards:test_board" +// } +// }, +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "test_board" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "std.Address" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Tree" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "5b4b593f1d4b37cb99166247ea28174f91087fdd", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:82" +// } +// }, +// { +// "N": "BAAAAAAAAAA=", +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "65536" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "time.Time" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "7e9fd9bb5e90a06c7751585cd80f23aedddde25b", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:83" +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Tree" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "edb1857302fa916c562cd077cdf2a3626e29ae2b", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:81", +// "IsEscaped": true, +// "ModTime": "108", +// "RefCount": "6" +// } +// } +// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:96]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "609e7f519c65f94503427a14f973b4b83989cdc8", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:109" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:96", +// "ModTime": "108", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:95", +// "RefCount": "1" +// } +// } +// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:97]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": null, +// "Index": "0", +// "TV": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "6760340f5b40e05221dc530940683b0b9a422503", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:117" +// } +// } +// } +// } +// ], +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:97", +// "ModTime": "108", +// "OwnerID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:95", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/modboards"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/users"] +// switchrealm["gno.land/r/demo/modboards"] +// switchrealm["gno.land/r/boards_test"] diff --git a/examples/gno.land/r/demo/modboards/z_5_b_filetest.gno b/examples/gno.land/r/demo/modboards/z_5_b_filetest.gno new file mode 100644 index 00000000000..d558a08029b --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_5_b_filetest.gno @@ -0,0 +1,31 @@ +package main + +// SEND: 2000000000ugnot + +import ( + "std" + "strconv" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + +func main() { + users.Register("", "gnouser", "my profile") + // create board via registered user + bid := boards.CreateBoard("test_board") + + // create post via anon user + test2 := testutils.TestAddress("test2") + std.TestSetOrigCaller(test2) + std.TestSetOrigSend(std.Coins{{"ugnot", 9000000}}, nil) + + pid := boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// please register, otherwise minimum fee 100000000 is required if anonymous diff --git a/examples/gno.land/r/demo/modboards/z_5_c_filetest.gno b/examples/gno.land/r/demo/modboards/z_5_c_filetest.gno new file mode 100644 index 00000000000..cb276ae728f --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_5_c_filetest.gno @@ -0,0 +1,39 @@ +package main + +// SEND: 2000000000ugnot + +import ( + "std" + "strconv" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + +func main() { + users.Register("", "gnouser", "my profile") + // create board via registered user + bid := boards.CreateBoard("test_board") + + // create post via anon user + test2 := testutils.TestAddress("test2") + std.TestSetOrigCaller(test2) + std.TestSetOrigSend(std.Coins{{"ugnot", 101000000}}, nil) + + pid := boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + boards.CreateReply(bid, pid, pid, "Reply of the first post") + + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # First Post (title) +// +// Body of the first post. (body) +// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// +// > Reply of the first post +// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/1/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] diff --git a/examples/gno.land/r/demo/modboards/z_5_d_filetest.gno b/examples/gno.land/r/demo/modboards/z_5_d_filetest.gno new file mode 100644 index 00000000000..7c3b6d019d3 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_5_d_filetest.gno @@ -0,0 +1,32 @@ +package main + +// SEND: 2000000000ugnot + +import ( + "std" + "strconv" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + +func main() { + users.Register("", "gnouser", "my profile") + // create board via registered user + bid := boards.CreateBoard("test_board") + pid := boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + + // create reply via anon user + test2 := testutils.TestAddress("test2") + std.TestSetOrigCaller(test2) + std.TestSetOrigSend(std.Coins{{"ugnot", 9000000}}, nil) + boards.CreateReply(bid, pid, pid, "Reply of the first post") + + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Error: +// please register, otherwise minimum fee 100000000 is required if anonymous diff --git a/examples/gno.land/r/demo/modboards/z_5_filetest.gno b/examples/gno.land/r/demo/modboards/z_5_filetest.gno new file mode 100644 index 00000000000..a72e48ce01b --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_5_filetest.gno @@ -0,0 +1,43 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid = boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + rid := boards.CreateReply(bid, pid, pid, "Reply of the second post") +} + +func main() { + rid2 := boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # Second Post (title) +// +// Body of the second post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// +// > Reply of the second post +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/3) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// +// > Second reply of the second post +// > +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/4) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=4&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=4)] diff --git a/examples/gno.land/r/demo/modboards/z_6_filetest.gno b/examples/gno.land/r/demo/modboards/z_6_filetest.gno new file mode 100644 index 00000000000..4bd50751137 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_6_filetest.gno @@ -0,0 +1,49 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID + rid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid = boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + rid = boards.CreateReply(bid, pid, pid, "Reply of the second post") +} + +func main() { + boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") + boards.CreateReply(bid, pid, rid, "First reply of the first reply\n") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # Second Post (title) +// +// Body of the second post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// +// > Reply of the second post +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/3) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// > +// > > First reply of the first reply +// > > +// > > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/5) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=5&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=5)] +// +// > Second reply of the second post +// > +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/4) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=4&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=4)] diff --git a/examples/gno.land/r/demo/modboards/z_7_filetest.gno b/examples/gno.land/r/demo/modboards/z_7_filetest.gno new file mode 100644 index 00000000000..6b8a49dc778 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_7_filetest.gno @@ -0,0 +1,31 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +func init() { + // register + users.Register("", "gnouser", "my profile") + + // create board and post + bid := boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") +} + +func main() { + println(boards.Render("test_board")) +} + +// Output: +// \[[post](/r/demo/modboards?help&__func=CreateThread&bid=1&body.type=textarea)] +// +// ---------------------------------------- +// ## [First Post (title)](/r/demo/modboards:test_board/1) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/modboards:test_board/1) \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) diff --git a/examples/gno.land/r/demo/modboards/z_8_filetest.gno b/examples/gno.land/r/demo/modboards/z_8_filetest.gno new file mode 100644 index 00000000000..acd0809b933 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_8_filetest.gno @@ -0,0 +1,44 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 2000000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + bid boards.BoardID + pid boards.PostID + rid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + bid = boards.CreateBoard("test_board") + boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") + pid = boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") + rid = boards.CreateReply(bid, pid, pid, "Reply of the second post") +} + +func main() { + boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") + rid2 := boards.CreateReply(bid, pid, rid, "First reply of the first reply\n") + println(boards.Render("test_board/" + strconv.Itoa(int(pid)) + "/" + strconv.Itoa(int(rid2)))) +} + +// Output: +// _[see thread](/r/demo/modboards:test_board/2)_ +// +// Reply of the second post +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/3) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// +// _[see all 1 replies](/r/demo/modboards:test_board/2/3)_ +// +// > First reply of the first reply +// > +// > \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:test_board/2/5) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=1&threadid=2&postid=5&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=1&threadid=2&postid=5)] diff --git a/examples/gno.land/r/demo/modboards/z_9_a_filetest.gno b/examples/gno.land/r/demo/modboards/z_9_a_filetest.gno new file mode 100644 index 00000000000..9710a58372f --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_9_a_filetest.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var dstBoard boards.BoardID + +func init() { + users.Register("", "gnouser", "my profile") + + dstBoard = boards.CreateBoard("dst_board") + + boards.CreateRepost(0, 0, "First Post in (title)", "Body of the first post. (body)", dstBoard) +} + +func main() { +} + +// Error: +// src board not exist diff --git a/examples/gno.land/r/demo/modboards/z_9_b_filetest.gno b/examples/gno.land/r/demo/modboards/z_9_b_filetest.gno new file mode 100644 index 00000000000..96e134f3645 --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_9_b_filetest.gno @@ -0,0 +1,31 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + srcBoard boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + srcBoard = boards.CreateBoard("first_board") + pid = boards.CreateThread(srcBoard, "First Post in (title)", "Body of the first post. (body)") + + boards.CreateRepost(srcBoard, pid, "First Post in (title)", "Body of the first post. (body)", 0) +} + +func main() { +} + +// Error: +// dst board not exist diff --git a/examples/gno.land/r/demo/modboards/z_9_filetest.gno b/examples/gno.land/r/demo/modboards/z_9_filetest.gno new file mode 100644 index 00000000000..7dd5a4f908b --- /dev/null +++ b/examples/gno.land/r/demo/modboards/z_9_filetest.gno @@ -0,0 +1,37 @@ +// PKGPATH: gno.land/r/boards_test +package boards_test + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/modboards" + "gno.land/r/demo/users" +) + +var ( + firstBoard boards.BoardID + secondBoard boards.BoardID + pid boards.PostID +) + +func init() { + users.Register("", "gnouser", "my profile") + + firstBoard = boards.CreateBoard("first_board") + secondBoard = boards.CreateBoard("second_board") + pid = boards.CreateThread(firstBoard, "First Post in (title)", "Body of the first post. (body)") + + boards.CreateRepost(firstBoard, pid, "First Post in (title)", "Body of the first post. (body)", secondBoard) +} + +func main() { + println(boards.Render("second_board/" + strconv.Itoa(int(pid)))) +} + +// Output: +// # First Post in (title) +// +// Body of the first post. (body) +// \- [@gnouser](/r/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/modboards:second_board/1/1) \[[reply](/r/demo/modboards?help&__func=CreateReply&bid=2&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/modboards?help&__func=DeletePost&bid=2&threadid=1&postid=1)] From 07f159453d09f0002ee42057b714989182016132 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Thu, 6 Jul 2023 12:53:11 +0200 Subject: [PATCH 2/7] TMP Signed-off-by: Norman Meier --- IDEA.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 IDEA.txt diff --git a/IDEA.txt b/IDEA.txt new file mode 100644 index 00000000000..c3dfc0f7ef1 --- /dev/null +++ b/IDEA.txt @@ -0,0 +1,31 @@ +create interface packages + + type TheInterface interface { + Whatever() + } + +create a realm for each of these interface package with funcs + + func Register(v TheInferface) + + implementerRealm := std.PrevRealm() + tree.Set(implementerRealm, v) + + func Get(address std.Address) TheInterface + + return tree.Get(address) + + +when you implement an interface in a new realm + + var implem = newImplem() + + func init() { + the_interface_realm.Register(implem) + } + +when you want to consume the interface via realm ref (package path or address) in some realm created before the implem + + func DoSomething(realmRef Realm) { + the_interface_realm.Get(realmRef).Whatever() + } \ No newline at end of file From c4dc14842aea37826fb7f35559e6ff861f4efe8b Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Sun, 9 Jul 2023 17:25:52 +0200 Subject: [PATCH 3/7] TMP Signed-off-by: Norman Meier --- .../gno.land/p/demo/flags_index/flags_index.gno | 16 +++++++++++++--- examples/gno.land/r/demo/dao_realm/dao_realm.gno | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/p/demo/flags_index/flags_index.gno b/examples/gno.land/p/demo/flags_index/flags_index.gno index ba429637eb9..e137542e961 100644 --- a/examples/gno.land/p/demo/flags_index/flags_index.gno +++ b/examples/gno.land/p/demo/flags_index/flags_index.gno @@ -100,7 +100,7 @@ func (fi *FlagsIndex) ClearFlagCount(flagID FlagID) { fi.flagsCountsByID.Remove(string(flagID)) // remove from byCount, we need to recreate the slice since splicing is broken - newByCount := make([]*FlagCount, len(fi.flagsCounts)-1) + newByCount := []*FlagCount{} for i := range fi.flagsCounts { if fi.flagsCounts[i].FlagID == flagID { continue @@ -129,12 +129,22 @@ func (fi *FlagsIndex) Dump() string { str += "## flagsCounts:\n" for i := range fi.flagsCounts { - str += "- " + string(fi.flagsCounts[i].FlagID) + " " + strconv.FormatUint(fi.flagsCounts[i].Count, 10) + "\n" + 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 += "- " + key + ": " + string(value.(*FlagCount).FlagID) + " " + strconv.FormatUint(value.(*FlagCount).Count, 10) + "\n" + 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 }) diff --git a/examples/gno.land/r/demo/dao_realm/dao_realm.gno b/examples/gno.land/r/demo/dao_realm/dao_realm.gno index 5ed2e8bae17..16cf96ca808 100644 --- a/examples/gno.land/r/demo/dao_realm/dao_realm.gno +++ b/examples/gno.land/r/demo/dao_realm/dao_realm.gno @@ -17,7 +17,7 @@ import ( var ( daoCore dao_core.IDAOCore registry = dao_interfaces.NewMessagesRegistry() - mainBoardName = "foo_dao_3" + mainBoardName = "dao_realm" groupID groups.GroupID ) From 2638f45a3735808a02546f5f00f6e5a164621ff6 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Sun, 9 Jul 2023 21:17:45 +0200 Subject: [PATCH 4/7] TMP Signed-off-by: Norman Meier --- .../r/demo/bugs/mis_ownership/gno.mod | 5 ++ .../demo/bugs/mis_ownership/mis_ownership.gno | 16 ++++++ .../bugs/slice_pop_push/slice_pop_push.gno | 16 +++--- .../r/demo/bugs/steal_ownership/gno.mod | 1 + .../bugs/steal_ownership/steal_ownership.gno | 7 +++ gnovm/pkg/gnolang/realm.go | 49 ++++++++++++++++++- gnovm/pkg/gnolang/store.go | 3 ++ gnovm/pkg/gnolang/values.go | 42 +++++++++++++++- result.txt | 5 ++ 9 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 examples/gno.land/r/demo/bugs/mis_ownership/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/mis_ownership/mis_ownership.gno create mode 100644 examples/gno.land/r/demo/bugs/steal_ownership/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/steal_ownership/steal_ownership.gno create mode 100644 result.txt diff --git a/examples/gno.land/r/demo/bugs/mis_ownership/gno.mod b/examples/gno.land/r/demo/bugs/mis_ownership/gno.mod new file mode 100644 index 00000000000..5e8da42caa9 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/mis_ownership/gno.mod @@ -0,0 +1,5 @@ +module "gno.land/r/demo/bugs/mis_ownership" + +require ( + "gno.land/r/demo/bugs/steal_ownership" v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/bugs/mis_ownership/mis_ownership.gno b/examples/gno.land/r/demo/bugs/mis_ownership/mis_ownership.gno new file mode 100644 index 00000000000..def1b41685a --- /dev/null +++ b/examples/gno.land/r/demo/bugs/mis_ownership/mis_ownership.gno @@ -0,0 +1,16 @@ +package mis_ownership + +import "gno.land/r/demo/bugs/steal_ownership" + +var ( + x = uint32(42) + ptr = &x +) + +func init() { + steal_ownership.Steal(ptr) +} + +func Mutate() { + *ptr = 21 +} diff --git a/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno b/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno index 849748b6424..38bbad987b3 100644 --- a/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno +++ b/examples/gno.land/r/demo/bugs/slice_pop_push/slice_pop_push.gno @@ -1,19 +1,21 @@ package slice_pop_push +import ( + "strings" +) + var slice = []string{"undead-element"} -func Pop() { +func Pop() []string { slice = slice[:len(slice)-1] + return slice } -func Push() { +func Push() []string { slice = append(slice, "new-element") + return slice } func Render(path string) string { - os := []string{"undead-element-2"} - os = os[:len(os)-1] - os = append(os, "new-element-2") - - return os[0] + slice[0] + return strings.Join(slice, ",") } diff --git a/examples/gno.land/r/demo/bugs/steal_ownership/gno.mod b/examples/gno.land/r/demo/bugs/steal_ownership/gno.mod new file mode 100644 index 00000000000..459566d1807 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/steal_ownership/gno.mod @@ -0,0 +1 @@ +module "gno.land/r/demo/bugs/steal_ownership" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/steal_ownership/steal_ownership.gno b/examples/gno.land/r/demo/bugs/steal_ownership/steal_ownership.gno new file mode 100644 index 00000000000..9aeaf4ff1dc --- /dev/null +++ b/examples/gno.land/r/demo/bugs/steal_ownership/steal_ownership.gno @@ -0,0 +1,7 @@ +package steal_ownership + +var ptr *uint32 + +func Steal(xptr *uint32) { + ptr = xptr +} diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 40f770c7720..70e67cb9ec9 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -130,6 +130,15 @@ func (rlm *Realm) DidUpdate(po, xo, co Object) { if rlm == nil { return } + + shouldDebug := false + if (po != nil && strings.Contains(po.String(), "-element")) || + (xo != nil && strings.Contains(xo.String(), "-element")) || + (co != nil && strings.Contains(co.String(), "-element")) { + fmt.Println("intersting DidUpdate", xo, co) + shouldDebug = true + } + if debug { if co != nil && co.GetIsDeleted() { panic("cannot attach a deleted object") @@ -142,6 +151,9 @@ func (rlm *Realm) DidUpdate(po, xo, co Object) { } } if po == nil || !po.GetIsReal() { + if shouldDebug { + fmt.Println("DidUpdate - Do nothing 1") + } return // do nothing. } if po.GetObjectID().PkgID != rlm.ID { @@ -151,17 +163,32 @@ func (rlm *Realm) DidUpdate(po, xo, co Object) { // Updates to .newCreated/.newEscaped /.newDeleted made here. (first gen) // More appends happen during FinalizeRealmTransactions(). (second+ gen) rlm.MarkDirty(po) + if shouldDebug { + fmt.Println("MarkDirty po", po) + } if co != nil { co.IncRefCount() if co.GetRefCount() > 1 { + if shouldDebug { + fmt.Println("DidUpdate - ref count case") + } if co.GetIsEscaped() { + fmt.Println("DidUpdate - already escaped") // already escaped } else { + fmt.Println("DidUpdate - mark escaped") rlm.MarkNewEscaped(co) } - } else if co.GetIsReal() { + } + if co.GetIsReal() { + if shouldDebug { + fmt.Println("MarkDirty co", co) + } rlm.MarkDirty(co) } else { + if shouldDebug { + fmt.Println("DidUpdate - else case") + } co.SetOwner(po) rlm.MarkNewReal(co) } @@ -211,6 +238,9 @@ func (rlm *Realm) MarkNewReal(oo Object) { } func (rlm *Realm) MarkDirty(oo Object) { + if strings.Contains(oo.(Value).String(), "-element") { + fmt.Println("MarkDirty implem", oo) + } if debug { if !oo.GetIsReal() && !oo.GetIsNewReal() { panic("should not happen") @@ -638,6 +668,9 @@ func (rlm *Realm) markDirtyAncestors(store Store) { // Saves .created and .updated objects. func (rlm *Realm) saveUnsavedObjects(store Store) { for _, co := range rlm.created { + if strings.Contains(co.String(), "-element") { + fmt.Println("saveUnsavedObjects: created:", co.String()) + } // for i := len(rlm.created) - 1; i >= 0; i-- { // co := rlm.created[i] if !co.GetIsNewReal() { @@ -649,6 +682,9 @@ func (rlm *Realm) saveUnsavedObjects(store Store) { } } for _, uo := range rlm.updated { + if strings.Contains(uo.String(), "-element") { + fmt.Println("saveUnsavedObjects: updated:", uo.String()) + } // for i := len(rlm.updated) - 1; i >= 0; i-- { // uo := rlm.updated[i] if !uo.GetIsDirty() { @@ -1057,6 +1093,9 @@ func copyValueWithRefs(parent Object, val Value) Value { case nil: return nil case StringValue: + if cv == "undead-element" || cv == "new-element" { + fmt.Println("copyValueWithRefs", cv) + } return cv case BigintValue: return cv @@ -1088,6 +1127,9 @@ func copyValueWithRefs(parent Object, val Value) Value { } } case *ArrayValue: + if strings.Contains(cv.String(), "-element") { + fmt.Println("copyValueWithRefs slice", cv) + } if cv.Data == nil { list := make([]TypedValue, len(cv.List)) for i, etv := range cv.List { @@ -1104,6 +1146,11 @@ func copyValueWithRefs(parent Object, val Value) Value { } } case *SliceValue: + if strings.Contains(cv.String(), "-element") { + fmt.Println("copyValueWithRefs slice", cv) + fmt.Println("copyValue base", cv.Base.(*ArrayValue)) + + } return &SliceValue{ Base: toRefValue(parent, cv.Base), Offset: cv.Offset, diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index d3628edf216..3e0d3433c81 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -296,6 +296,9 @@ func (ds *defaultStore) loadObjectSafe(oid ObjectID) Object { // package values. func (ds *defaultStore) SetObject(oo Object) { oid := oo.GetObjectID() + if array, ok := oo.(Value).(*ArrayValue); ok && strings.Contains(array.String(), "-element") { + fmt.Println("SetObject intersting array", array) + } // replace children/fields with Ref. o2 := copyValueWithRefs(nil, oo) // marshal to binary. diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 3de74ac0130..a12937b92d8 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -204,6 +204,12 @@ func (pv *PointerValue) GetBase(store Store) Object { // TODO: document as something that enables into-native assignment. // TODO: maybe consider this as entrypoint for DataByteValue too? func (pv PointerValue) Assign2(alloc *Allocator, store Store, rlm *Realm, tv2 TypedValue, cu bool) { + shouldDebug := false + if strings.Contains(tv2.String(), "-element") { + fmt.Println("Assign2", tv2) + // rlm.MarkDirty(tv2.V.(*SliceValue).GetBase(store)) + shouldDebug = true + } // Special cases. if pv.Index == PointerIndexNative { // Special case if extended object && native. @@ -276,10 +282,44 @@ func (pv PointerValue) Assign2(alloc *Allocator, store Store, rlm *Realm, tv2 Ty } // General case if rlm != nil && pv.Base != nil { + if shouldDebug { + fmt.Println("Assign2 - DidUpdate", pv.TV, tv2) + } + oo1 := pv.TV.GetFirstObject(store) + + markDirty := false + if pv.TV != nil && pv.TV.T != nil && pv.TV.V != nil && pv.TV.T.Kind() == SliceKind && tv2.T != nil && tv2.V != nil && tv2.T.Kind() == SliceKind && pv.TV.V.(*SliceValue).Length != tv2.V.(*SliceValue).Length { + //fmt.Println("MarkDirty weird", pv.TV, tv2) + markDirty = true + } + pv.TV.Assign(alloc, tv2, cu) - oo2 := pv.TV.GetFirstObject(store) + oo2 := tv2.GetFirstObject(store) + if shouldDebug { + fmt.Printf("Assign2 - DidUpdate after %p %p\n", oo1, oo2) + } rlm.DidUpdate(pv.Base.(Object), oo1, oo2) + if markDirty { + //rlm.MarkDirty(oo1) + } + + /* + if shouldDebug { + if pv.TV != nil && tv2.V != nil && oo1 != nil && oo2 != nil { + switch pv.TV.V.(type) { + case *SliceValue: + switch tv2.V.(type) { + case *SliceValue: + base := pv.TV.V.(*SliceValue).GetBase(store) + if base == tv2.V.(*SliceValue).GetBase(store) && base != nil { + rlm.MarkDirty(base) + } + } + } + } + } + */ } else { pv.TV.Assign(alloc, tv2, cu) } diff --git a/result.txt b/result.txt new file mode 100644 index 00000000000..3703bbf12db --- /dev/null +++ b/result.txt @@ -0,0 +1,5 @@ +❯ gnokey maketx call -pkgpath "gno.land/r/demo/bugs/mis_ownership" -func "Edit" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -broadcast -chainid "dev" -remote "127.0.0.1:26657" onblock-test Native + +Enter password. +--= Error =-- +Data: cannot modify external-realm or non-realm object: object pkg path: gno.land/r/demo/bugs/steal_ownership, realm: gno.land/r/demo/bugs/mis_ownership \ No newline at end of file From a6fea884db6520680f18fa1d5c6dd2d749e5a535 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Tue, 11 Jul 2023 13:59:39 +0200 Subject: [PATCH 5/7] feat: grc20 support Signed-off-by: Norman Meier --- .../gno.land/r/demo/dao_realm/dao_realm.gno | 5 + examples/gno.land/r/demo/dao_realm/gno.mod | 1 + .../gno.land/r/demo/grc20_registry/gno.mod | 6 + .../r/demo/grc20_registry/grc20_registry.gno | 31 ++++ examples/gno.land/r/demo/tori/gno.mod | 8 ++ examples/gno.land/r/demo/tori/messages.gno | 136 ++++++++++++++++++ examples/gno.land/r/demo/tori/tori.gno | 113 +++++++++++++++ gnovm/pkg/gnolang/realm.go | 20 ++- 8 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 examples/gno.land/r/demo/grc20_registry/gno.mod create mode 100644 examples/gno.land/r/demo/grc20_registry/grc20_registry.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 diff --git a/examples/gno.land/r/demo/dao_realm/dao_realm.gno b/examples/gno.land/r/demo/dao_realm/dao_realm.gno index 16cf96ca808..4a8655d9090 100644 --- a/examples/gno.land/r/demo/dao_realm/dao_realm.gno +++ b/examples/gno.land/r/demo/dao_realm/dao_realm.gno @@ -12,6 +12,7 @@ import ( "gno.land/p/demo/daodao/voting_group" "gno.land/r/demo/groups" modboards "gno.land/r/demo/modboards" + tori "gno.land/r/demo/tori" ) var ( @@ -49,6 +50,10 @@ func init() { registry.Register(modboards.NewCreateBoardHandler()) registry.Register(modboards.NewDeletePostHandler()) modboards.CreateBoard(mainBoardName) + + // TODO: replace with grc20_admin_registry + registry.Register(tori.NewMintToriHandler()) + registry.Register(tori.NewBurnToriHandler()) } func Render(path string) string { diff --git a/examples/gno.land/r/demo/dao_realm/gno.mod b/examples/gno.land/r/demo/dao_realm/gno.mod index dae2df2a65f..5d4e48c01a6 100644 --- a/examples/gno.land/r/demo/dao_realm/gno.mod +++ b/examples/gno.land/r/demo/dao_realm/gno.mod @@ -7,4 +7,5 @@ require ( "gno.land/p/demo/daodao/voting_group" v0.0.0-latest "gno.land/r/demo/groups" v0.0.0-latest "gno.land/r/demo/modboards" v0.0.0-latest + "gno.land/r/demo/tori" v0.0.0-latest ) \ No newline at end of file diff --git a/examples/gno.land/r/demo/grc20_registry/gno.mod b/examples/gno.land/r/demo/grc20_registry/gno.mod new file mode 100644 index 00000000000..ea560f63150 --- /dev/null +++ b/examples/gno.land/r/demo/grc20_registry/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/grc20_registry + +require ( + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/p/demo/grc/grc20" v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/grc20_registry/grc20_registry.gno b/examples/gno.land/r/demo/grc20_registry/grc20_registry.gno new file mode 100644 index 00000000000..9bcd60ff2eb --- /dev/null +++ b/examples/gno.land/r/demo/grc20_registry/grc20_registry.gno @@ -0,0 +1,31 @@ +package grc20_registry + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc20" +) + +var ( + registry = avl.NewTree() // addr -> IGRC20 + lol grc20.IGRC20 +) + +func Register(token grc20.IGRC20) { + caller := std.PrevRealm().Addr() + // registry.Set(caller.String(), token) + lol = token +} + +func Get(addr std.Address) (grc20.IGRC20, bool) { + coinI, ok := registry.Get(addr.String()) + if !ok { + return nil, false + } + coin, ok := coinI.(grc20.IGRC20) + if !ok { + panic("should not happen") + } + return coin, true +} diff --git a/examples/gno.land/r/demo/tori/gno.mod b/examples/gno.land/r/demo/tori/gno.mod new file mode 100644 index 00000000000..8be64e8b4ab --- /dev/null +++ b/examples/gno.land/r/demo/tori/gno.mod @@ -0,0 +1,8 @@ +module gno.land/r/demo/tori + +require ( + "gno.land/p/demo/ufmt" v0.0.0-latest + "gno.land/p/demo/grc/grc20" v0.0.0-latest + "gno.land/r/demo/users" v0.0.0-latest + "gno.land/r/demo/grc20_registry" v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/tori/messages.gno b/examples/gno.land/r/demo/tori/messages.gno new file mode 100644 index 00000000000..c9e2171ee05 --- /dev/null +++ b/examples/gno.land/r/demo/tori/messages.gno @@ -0,0 +1,136 @@ +package tori + +import ( + "encoding/binary" + "std" + "strconv" + "strings" + + "gno.land/p/demo/binutils" + "gno.land/p/demo/daodao/interfaces" + "gno.land/r/demo/users" +) + +type ExecutableMessageMintTori struct { + dao_interfaces.ExecutableMessage + + Address users.AddressOrName + Amount uint64 +} + +func (msg *ExecutableMessageMintTori) Type() string { + return "MintTori" +} + +func (msg *ExecutableMessageMintTori) String() string { + var ss []string + ss = append(ss, msg.Type()) + s := "Address: " + string(msg.Address) + "\n" + s += "Amount: " + strconv.FormatUint(msg.Amount, 10) + ss = append(ss, s) + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageMintTori) Binary() []byte { + b := []byte{} + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(msg.Type())...) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(string(msg.Address))...) + b = binary.BigEndian.AppendUint64(b, msg.Amount) + return b +} + +func ExecutableMessageMintToriFromBinary(b []byte) *ExecutableMessageMintTori { + msg := &ExecutableMessageMintTori{} + t, b := binutils.MustDecodeLengthPrefixedStringUint16BE(b) + if t != msg.Type() { + panic("invalid type") + } + var addr string + addr, b = binutils.MustDecodeLengthPrefixedStringUint16BE(b) + msg.Address = users.AddressOrName(addr) + msg.Amount, b = binary.BigEndian.Uint64(b), b[8:] + return msg +} + +type MintToriHandler struct { + dao_interfaces.MessageHandler +} + +func NewMintToriHandler() *MintToriHandler { + return &MintToriHandler{} +} + +func (h *MintToriHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageMintTori) + Mint(msg.Address, msg.Amount) +} + +func (h *MintToriHandler) Type() string { + return ExecutableMessageMintTori{}.Type() +} + +func (h *MintToriHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageMintToriFromBinary(b) +} + +type ExecutableMessageBurnTori struct { + dao_interfaces.ExecutableMessage + + Address users.AddressOrName + Amount uint64 +} + +func (msg *ExecutableMessageBurnTori) Type() string { + return "BurnTori" +} + +func (msg *ExecutableMessageBurnTori) String() string { + var ss []string + ss = append(ss, msg.Type()) + s := "Address: " + string(msg.Address) + "\n" + s += "Amount: " + strconv.FormatUint(msg.Amount, 10) + ss = append(ss, s) + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageBurnTori) Binary() []byte { + b := []byte{} + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(msg.Type())...) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(string(msg.Address))...) + b = binary.BigEndian.AppendUint64(b, msg.Amount) + return b +} + +func ExecutableMessageBurnToriFromBinary(b []byte) *ExecutableMessageBurnTori { + msg := &ExecutableMessageBurnTori{} + t, b := binutils.MustDecodeLengthPrefixedStringUint16BE(b) + if t != msg.Type() { + panic("invalid type") + } + var addr string + addr, b = binutils.MustDecodeLengthPrefixedStringUint16BE(b) + msg.Address = users.AddressOrName(addr) + msg.Amount, b = binary.BigEndian.Uint64(b), b[8:] + return msg +} + +type BurnToriHandler struct { + dao_interfaces.MessageHandler +} + +func NewBurnToriHandler() *BurnToriHandler { + return &BurnToriHandler{} +} + +func (h *BurnToriHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageBurnTori) + Burn(msg.Address, msg.Amount) +} + +func (h *BurnToriHandler) Type() string { + return ExecutableMessageBurnTori{}.Type() +} + +func (h *BurnToriHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageBurnToriFromBinary(b) +} diff --git a/examples/gno.land/r/demo/tori/tori.gno b/examples/gno.land/r/demo/tori/tori.gno new file mode 100644 index 00000000000..12aa95e670e --- /dev/null +++ b/examples/gno.land/r/demo/tori/tori.gno @@ -0,0 +1,113 @@ +package tori + +import ( + "std" + "strings" + + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20_registry" + "gno.land/r/demo/users" +) + +var ( + tori *grc20.AdminToken + userTori grc20.IGRC20 + admin std.Address = std.DerivePkgAddr("gno.land/r/demo/dao_realm") // TODO: helper to change admin +) + +func init() { + tori = grc20.NewAdminToken("Tori", "TORI", 6) + userTori = tori.GRC20() + grc20_registry.Register(userTori) // found another bug, calling this sets some grc20 internal object's pkgid to the registry realm id +} + +// method proxies as public functions. +// + +// getters. + +func TotalSupply() uint64 { + return tori.TotalSupply() +} + +func BalanceOf(owner users.AddressOrName) uint64 { + balance, err := tori.BalanceOf(owner.Resolve()) + if err != nil { + panic(err) + } + return balance +} + +func Allowance(owner, spender users.AddressOrName) uint64 { + allowance, err := tori.Allowance(owner.Resolve(), spender.Resolve()) + if err != nil { + panic(err) + } + return allowance +} + +// setters. + +func Transfer(to users.AddressOrName, amount uint64) { + caller := std.PrevRealm().Addr() + tori.Transfer(caller, to.Resolve(), amount) +} + +func Approve(spender users.AddressOrName, amount uint64) { + caller := std.PrevRealm().Addr() + tori.Approve(caller, spender.Resolve(), amount) +} + +func TransferFrom(from, to users.AddressOrName, amount uint64) { + caller := std.PrevRealm().Addr() + tori.TransferFrom(caller, from.Resolve(), to.Resolve(), amount) +} + +// faucet. + +func Faucet() { + // FIXME: add limits? + // FIXME: add payment in gnot? + caller := std.PrevRealm().Addr() + tori.Mint(caller, 1000*10000) // 1k +} + +// administration. + +func Mint(address users.AddressOrName, amount uint64) { + caller := std.PrevRealm().Addr() + assertIsAdmin(caller) + tori.Mint(address.Resolve(), amount) +} + +func Burn(address users.AddressOrName, amount uint64) { + caller := std.PrevRealm().Addr() + assertIsAdmin(caller) + tori.Burn(address.Resolve(), amount) +} + +// render. +// + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return tori.RenderHome() + case c == 2 && parts[0] == "balance": + owner := users.AddressOrName(parts[1]) + balance, _ := tori.BalanceOf(owner.Resolve()) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func assertIsAdmin(address std.Address) { + if address != admin { + panic("restricted access") + } +} diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 70e67cb9ec9..5c584080fcc 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -69,8 +69,16 @@ func (pid PkgID) Bytes() []byte { return pid.Hashlet[:] } +var pathsFromIds = make(map[string]string) + func PkgIDFromPkgPath(path string) PkgID { - return PkgID{HashBytes([]byte(path))} + id := PkgID{HashBytes([]byte(path))} + pathsFromIds[id.String()] = path + return id +} + +func PkgPathFromPkgID(id PkgID) string { + return pathsFromIds[id.String()] } func ObjectIDFromPkgPath(path string) ObjectID { @@ -157,7 +165,12 @@ func (rlm *Realm) DidUpdate(po, xo, co Object) { return // do nothing. } if po.GetObjectID().PkgID != rlm.ID { - panic("cannot modify external-realm or non-realm object") + opid := po.GetObjectID().PkgID + prettyName := PkgPathFromPkgID(opid) + if prettyName == "" { + prettyName = opid.String() + } + panic(fmt.Sprintf("cannot modify external-realm or non-realm object: object pkg path: %s, realm: %s", prettyName, rlm.Path)) } // From here on, po is real (not new-real). // Updates to .newCreated/.newEscaped /.newDeleted made here. (first gen) @@ -179,8 +192,7 @@ func (rlm *Realm) DidUpdate(po, xo, co Object) { fmt.Println("DidUpdate - mark escaped") rlm.MarkNewEscaped(co) } - } - if co.GetIsReal() { + } else if co.GetIsReal() { if shouldDebug { fmt.Println("MarkDirty co", co) } From cee2bec9a59875519b663e9791cf62abe4cd0055 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Thu, 13 Jul 2023 19:18:35 +0200 Subject: [PATCH 6/7] bug examples Signed-off-by: Norman Meier --- .../gno.land/r/demo/bugs/bugmin/bugmin.gno | 22 +++++++++++++++++++ examples/gno.land/r/demo/bugs/bugmin/gno.mod | 1 + .../gno.land/r/demo/bugs/slice_splice/gno.mod | 1 + .../r/demo/bugs/slice_splice/slice_splice.gno | 20 +++++++++++++++++ .../r/demo/bugs/two_slices_one_array/gno.mod | 1 + .../two_slices_one_array.gno | 20 +++++++++++++++++ 6 files changed, 65 insertions(+) create mode 100644 examples/gno.land/r/demo/bugs/bugmin/bugmin.gno create mode 100644 examples/gno.land/r/demo/bugs/bugmin/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/slice_splice/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/slice_splice/slice_splice.gno create mode 100644 examples/gno.land/r/demo/bugs/two_slices_one_array/gno.mod create mode 100644 examples/gno.land/r/demo/bugs/two_slices_one_array/two_slices_one_array.gno diff --git a/examples/gno.land/r/demo/bugs/bugmin/bugmin.gno b/examples/gno.land/r/demo/bugs/bugmin/bugmin.gno new file mode 100644 index 00000000000..74d658193bb --- /dev/null +++ b/examples/gno.land/r/demo/bugs/bugmin/bugmin.gno @@ -0,0 +1,22 @@ +package bugmin + +import ( + "strconv" + "strings" +) + +var slice = []string{"undead-element"} + +func Pop() { + slice = slice[:len(slice)-1] +} + +func Push() { + slice = append(slice, "new-element") +} + +func Render(path string) string { + str := strings.Join(slice, ", ") + "\n\n" + str += "len: " + strconv.Itoa(len(slice)) + "\n\n" + return str +} diff --git a/examples/gno.land/r/demo/bugs/bugmin/gno.mod b/examples/gno.land/r/demo/bugs/bugmin/gno.mod new file mode 100644 index 00000000000..36cdd9d76a0 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/bugmin/gno.mod @@ -0,0 +1 @@ +module "gno.land/r/demo/bugs/bugmin" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/slice_splice/gno.mod b/examples/gno.land/r/demo/bugs/slice_splice/gno.mod new file mode 100644 index 00000000000..f7a638504d0 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/slice_splice/gno.mod @@ -0,0 +1 @@ +module "gno.land/r/demo/bugs/slice_splice" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/slice_splice/slice_splice.gno b/examples/gno.land/r/demo/bugs/slice_splice/slice_splice.gno new file mode 100644 index 00000000000..aa7a2eb03ba --- /dev/null +++ b/examples/gno.land/r/demo/bugs/slice_splice/slice_splice.gno @@ -0,0 +1,20 @@ +package slice_splice + +import ( + "strings" +) + +var slice = []string{} + +func Push() { + slice = append(slice, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10") +} + +func Splice() []string { + slice = slice[4:5] + return slice +} + +func Render(path string) string { + return strings.Join(slice, ",") +} diff --git a/examples/gno.land/r/demo/bugs/two_slices_one_array/gno.mod b/examples/gno.land/r/demo/bugs/two_slices_one_array/gno.mod new file mode 100644 index 00000000000..acc48a55204 --- /dev/null +++ b/examples/gno.land/r/demo/bugs/two_slices_one_array/gno.mod @@ -0,0 +1 @@ +module "gno.land/r/demo/bugs/two_slices_one_array" \ No newline at end of file diff --git a/examples/gno.land/r/demo/bugs/two_slices_one_array/two_slices_one_array.gno b/examples/gno.land/r/demo/bugs/two_slices_one_array/two_slices_one_array.gno new file mode 100644 index 00000000000..3799f569e6b --- /dev/null +++ b/examples/gno.land/r/demo/bugs/two_slices_one_array/two_slices_one_array.gno @@ -0,0 +1,20 @@ +package two_slices_one_array + +import ( + "strings" +) + +var ( + slice = []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"} + sub1 []string = nil + sub2 []string = nil +) + +func Split() { + sub1 = slice[0:5] + sub2 = slice[5:] +} + +func Render(path string) string { + return "slice: " + strings.Join(slice, ",") + "\n\n" + "sub1: " + strings.Join(sub1, ",") + "\n\n" + "sub2: " + strings.Join(sub2, ",") +} From ec7f2cefafafd079496581fe04ef05a18652f56f Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 18 Jul 2023 14:31:25 +0200 Subject: [PATCH 7/7] example for n0izn0iz --- .../gno.land/r/demo/dao_realm/dao_realm.gno | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/examples/gno.land/r/demo/dao_realm/dao_realm.gno b/examples/gno.land/r/demo/dao_realm/dao_realm.gno index 4a8655d9090..50de9c0f2de 100644 --- a/examples/gno.land/r/demo/dao_realm/dao_realm.gno +++ b/examples/gno.land/r/demo/dao_realm/dao_realm.gno @@ -93,3 +93,41 @@ func GetBinaryMembers() string { } return strings.Join(ss, ",") } + +func RegisterNewFeature(handle NewHandle) { + assertRealmIsInDAOPrefix() // if strings.HasPrefix(std.PrevRealm().PkgPath(), "dao_realm/") + assertIsWhitelistedCaller() + registry.Register(handle) +} + +var whitelisted []std.Address // can be done with https://github.com/gnolang/gno/pull/880 + +func ToggleWhitelistedAddress(addr std.Address) { + assertIsAdminOrDAOApproved() + whitelisted = append(whitelisted, addr) +} + + + + +-------------- +import "dao_realm" + +func init() { + dao_realm.Register(handle) // works IF caller checks if std.PrevRealm().PkgPath() starts with the namespace +} + +var registered bool + +func RegisterSelf() { // this one works whenever someone calls it, except, you need to manually whitelist the contract address first + if registered { return } + dao_realm.Register(handle) +} + +--- + + +1. upload dao_realm +2. upload dao_feat2 +3. call dao_realm.ToggleWhitelistedAddress(dao_feat2) +4. call dao_feat2.RegisterSelf()