diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 9129cd17665..2ec9741595f 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -91,7 +91,7 @@ caller := std.OriginCaller() ## OriginPkgAddress ```go -func OriginPkgAddress() string +func OriginPkgAddress() Address ``` Returns the address of the first (entry point) realm/package in a sequence of realm/package calls. diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index bc03fdb7ce6..10e30914bec 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -35,7 +35,7 @@ type Board struct { readOnly bool } -func newBoard(id BoardID, name string, creator std.Address) *Board { +func newBoard(id BoardID, name string, creator std.Address, p Permissions) *Board { return &Board{ id: id, name: name, @@ -43,7 +43,7 @@ func newBoard(id BoardID, name string, creator std.Address) *Board { threads: avl.Tree{}, createdAt: time.Now(), deleted: avl.Tree{}, - perms: createDefaultBoardPermissions(creator), + perms: p, } } @@ -110,7 +110,7 @@ func (board *Board) DeleteThread(pid PostID) { // // Pager is used for pagination if it's not nil. func (board *Board) Render(p *PaginationOpts) string { - sb := &strings.Builder{} + var sb strings.Builder sb.WriteString(newButtonLink("post", board.GetPostFormURL()) + " ") sb.WriteString(newButtonLink("members", board.GetMembersURL())) @@ -154,6 +154,30 @@ func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { return board.GetURL() + "/" + threadID.String() + "/" + replyID.String() } +func (board *Board) GetRenameFormURL() string { + return txlink.Call("RenameBoard", "name", board.name) +} + +func (board *Board) GetFreezeFormURL() string { + return txlink.Call("FreezeBoard", "boardID", board.id.String()) +} + +func (board *Board) GetFlaggingThresholdFormURL() string { + return txlink.Call("SetFlaggingThreshold", "boardID", board.id.String()) +} + +func (board *Board) GetInviteMemberFormURL() string { + return txlink.Call("InviteMember", "boardID", board.id.String()) +} + +func (board *Board) GetRemoveMemberFormURL() string { + return txlink.Call("RemoveMember", "boardID", board.id.String()) +} + +func (board *Board) GetChangeMemberRoleFormURL() string { + return txlink.Call("ChangeMemberRole", "boardID", board.id.String()) +} + func (board *Board) GetPostFormURL() string { return txlink.Call("CreateThread", "boardID", board.id.String()) } diff --git a/examples/gno.land/r/nt/boards2/v1/board_test.gno b/examples/gno.land/r/nt/boards2/v1/board_test.gno index f77b55ecc7f..a7738517c0b 100644 --- a/examples/gno.land/r/nt/boards2/v1/board_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/board_test.gno @@ -5,6 +5,7 @@ import ( "strings" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" "gno.land/p/moul/txlink" ) @@ -36,16 +37,17 @@ func TestBoard_GetURL(t *testing.T) { name := "foobar_test_get_url123" want := pkgPath + ":" + name - var addr std.Address - - board := newBoard(1, name, addr) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + board := newBoard(1, name, addr, perms) got := board.GetURL() uassert.Equal(t, want, got) } func TestBoard_GetThread(t *testing.T) { - var addr std.Address - b := newBoard(1, "test123", addr) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(1, "test123", addr, perms) _, ok := b.GetThread(12345) uassert.False(t, ok) @@ -56,8 +58,9 @@ func TestBoard_GetThread(t *testing.T) { } func TestBoard_DeleteThread(t *testing.T) { - var addr std.Address - b := newBoard(1, "test123", addr) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(1, "test123", addr, perms) post := b.AddThread(addr, "foo", "bar") id := post.GetPostID() @@ -72,7 +75,9 @@ var boardUrlPrefix = strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land" func TestBoard_GetURLFromThreadID(t *testing.T) { boardName := "test12345" - b := newBoard(BoardID(11), boardName, "") + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(BoardID(11), boardName, addr, perms) want := boardUrlPrefix + ":" + boardName + "/10" got := b.GetURLFromThreadID(10) @@ -81,7 +86,9 @@ func TestBoard_GetURLFromThreadID(t *testing.T) { func TestBoard_GetURLFromReplyID(t *testing.T) { boardName := "test12345" - b := newBoard(BoardID(11), boardName, "") + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(BoardID(11), boardName, addr, perms) want := boardUrlPrefix + ":" + boardName + "/10/20" got := b.GetURLFromReplyID(10, 20) @@ -90,8 +97,11 @@ func TestBoard_GetURLFromReplyID(t *testing.T) { func TestBoard_GetPostFormURL(t *testing.T) { bid := BoardID(386) - b := newBoard(bid, "foo1234", "") + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(bid, "foo1234", addr, perms) expect := txlink.Call("CreateThread", "boardID", bid.String()) + got := b.GetPostFormURL() uassert.Equal(t, expect, got) } diff --git a/examples/gno.land/r/nt/boards2/v1/boards.gno b/examples/gno.land/r/nt/boards2/v1/boards.gno index 8a0c57bd9dc..a1c737c4b91 100644 --- a/examples/gno.land/r/nt/boards2/v1/boards.gno +++ b/examples/gno.land/r/nt/boards2/v1/boards.gno @@ -7,7 +7,7 @@ import ( ) var ( - gPerm Permissions // TODO: Support assigning a different implementation + gPerms Permissions gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board @@ -17,8 +17,8 @@ func init() { // TODO: Define and change the default realm owner (or owners) owner := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - // Initialize the default realm permissions - gPerm = createDefaultPermissions(owner) + // Initialize default realm permissions + gPerms = createDefaultPermissions(owner) } // incGetBoardID returns a new board ID. diff --git a/examples/gno.land/r/nt/boards2/v1/pagination.gno b/examples/gno.land/r/nt/boards2/v1/pagination.gno index bd8ec4579a9..edb47ca5d5b 100644 --- a/examples/gno.land/r/nt/boards2/v1/pagination.gno +++ b/examples/gno.land/r/nt/boards2/v1/pagination.gno @@ -27,7 +27,10 @@ func (opts *PaginationOpts) Iterate(tree *avl.Tree, cb func(k string, val interf } } - return page + if page.TotalPages > 1 { + return page + } + return nil } func mustGetPagination(rawPath string, pageSize int) *PaginationOpts { diff --git a/examples/gno.land/r/nt/boards2/v1/permissions.gno b/examples/gno.land/r/nt/boards2/v1/permissions.gno index a0be26aaacb..2d6de0ec3f2 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions.gno @@ -18,6 +18,7 @@ const ( PermissionMemberInvite = "member:invite" PermissionMemberRemove = "member:remove" PermissionRoleChange = "role:change" + PermissionPermissionsUpdate = "permissions:update" ) const ( diff --git a/examples/gno.land/r/nt/boards2/v1/post.gno b/examples/gno.land/r/nt/boards2/v1/post.gno index 8de64d9165c..2341dbfe147 100644 --- a/examples/gno.land/r/nt/boards2/v1/post.gno +++ b/examples/gno.land/r/nt/boards2/v1/post.gno @@ -213,6 +213,13 @@ func (post *Post) GetURL() string { } func (post *Post) GetReplyFormURL() string { + if post.IsThread() { + return txlink.Call("CreateReply", + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), + "replyID", "0", + ) + } return txlink.Call("CreateReply", "boardID", post.board.id.String(), "threadID", post.threadID.String(), @@ -241,6 +248,24 @@ func (post *Post) GetDeleteFormURL() string { ) } +func (post *Post) GetEditFormURL() string { + if post.IsThread() { + return txlink.Call("EditThread", + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), + "title", post.GetTitle(), + "body", post.GetBody(), + ) + } + + return txlink.Call("EditReply", + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), + "replyID", post.id.String(), + "body", post.GetBody(), + ) +} + func (post *Post) GetFlagFormURL() string { if post.IsThread() { return txlink.Call("FlagThread", @@ -325,14 +350,15 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString("\n") } - postURL := post.GetURL() - // Buttons & counters sb.WriteString(indent) - sb.WriteString(`\- `) + if !post.IsThread() { + sb.WriteString(" \n" + indent) + } + sb.WriteString(newUserLink(post.creator)) sb.WriteString(", ") - sb.WriteString(newLink(post.createdAt.Format(dateFormat), postURL)) + sb.WriteString(post.createdAt.Format(dateFormat)) if post.repostsCount > 0 { sb.WriteString(", ") @@ -340,6 +366,8 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString(" reposts") } + sb.WriteString(" - ") + if srcPost != nil { sb.WriteString(" ") sb.WriteString(newButtonLink("see source post", srcPost.GetURL())) @@ -353,6 +381,9 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString(newButtonLink("repost", post.GetRepostFormURL())) } + sb.WriteString(" ") + sb.WriteString(newButtonLink("edit", post.GetEditFormURL())) + sb.WriteString(" ") sb.WriteString(newButtonLink("flag", post.GetFlagFormURL())) @@ -367,7 +398,7 @@ func (post *Post) Render(p *PaginationOpts, indent string, levels int) string { } // TODO: pass a builder as arg into Render. - sb := &strings.Builder{} + var sb strings.Builder if post.title != "" { sb.WriteString(indent) @@ -378,7 +409,7 @@ func (post *Post) Render(p *PaginationOpts, indent string, levels int) string { sb.WriteString("\n") } - post.renderPostContent(sb, indent) + post.renderPostContent(&sb, indent) if post.replies.Size() == 0 { return sb.String() diff --git a/examples/gno.land/r/nt/boards2/v1/post_test.gno b/examples/gno.land/r/nt/boards2/v1/post_test.gno index fc6070a1db7..1f4926a7436 100644 --- a/examples/gno.land/r/nt/boards2/v1/post_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/post_test.gno @@ -10,7 +10,9 @@ import ( ) func TestPostUpdate(t *testing.T) { - board := newBoard(1, "test123", testutils.TestAddress("creator")) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + board := newBoard(1, "test123", addr, perms) creator := testutils.TestAddress("creator") post := newPost(board, 1, creator, "Title", "Body", 1, 0, 0) title := "New Title" @@ -48,6 +50,9 @@ func TestPostSetVisible(t *testing.T) { } func TestPostAddRepostTo(t *testing.T) { + // TODO: Improve this unit test + addr := testutils.TestAddress("creatorDstBoard") + perms := createDefaultBoardPermissions(addr) cases := []struct { name, title, body string dstBoard *Board @@ -59,7 +64,7 @@ func TestPostAddRepostTo(t *testing.T) { name: "repost thread", title: "Repost Title", body: "Repost body", - dstBoard: newBoard(42, "dst123", testutils.TestAddress("creatorDstBoard")), + dstBoard: newBoard(42, "dst123", addr, perms), setup: func() *Post { return createTestThread(t) }, }, { @@ -112,17 +117,24 @@ func TestNewThread(t *testing.T) { boardID := BoardID(1) threadID := PostID(42) boardName := "test123" - board := newBoard(boardID, boardName, creator) + perms := createDefaultBoardPermissions(creator) + board := newBoard(boardID, boardName, creator, perms) url := ufmt.Sprintf( "/r/nt/boards2/v1:%s/%d", boardName, uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateReply&boardID=%d&replyID=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&boardID=%d&replyID=0&threadID=%d", uint(boardID), uint(threadID), + ) + editURL := ufmt.Sprintf( + "/r/nt/boards2/v1$help&func=EditThread&boardID=%d&body=%s&threadID=%d&title=%s", + uint(boardID), + body, uint(threadID), + strings.ReplaceAll(title, " ", "+"), ) repostURL := ufmt.Sprintf( "/r/nt/boards2/v1$help&func=CreateRepost&boardID=%d&threadID=%d", @@ -151,6 +163,7 @@ func TestNewThread(t *testing.T) { uassert.False(t, thread.HasReplies()) uassert.Equal(t, url, thread.GetURL()) uassert.Equal(t, replyURL, thread.GetReplyFormURL()) + uassert.Equal(t, editURL, thread.GetEditFormURL()) uassert.Equal(t, repostURL, thread.GetRepostFormURL()) uassert.Equal(t, deleteURL, thread.GetDeleteFormURL()) uassert.Equal(t, flagURL, thread.GetFlagFormURL()) @@ -282,7 +295,8 @@ func TestNewReply(t *testing.T) { parentID := PostID(1) replyID := PostID(2) boardName := "test123" - board := newBoard(boardID, boardName, creator) + perms := createDefaultBoardPermissions(creator) + board := newBoard(boardID, boardName, creator, perms) url := ufmt.Sprintf( "/r/nt/boards2/v1:%s/%d/%d", boardName, @@ -391,7 +405,8 @@ func createTestThread(t *testing.T) *Post { t.Helper() creator := testutils.TestAddress("creator") - board := newBoard(1, "test_board_123", creator) + perms := createDefaultBoardPermissions(creator) + board := newBoard(1, "test_board_123", creator, perms) return board.AddThread(creator, "Title", "Body") } diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index f7d801b49c6..c42aa0e7a6e 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -5,6 +5,31 @@ import ( "strings" ) +// SetPermissions sets a permissions implementation for boards2 realm or a board. +func SetPermissions(bid BoardID, p Permissions) { + if p == nil { + panic("permissions is required") + } + + if bid != 0 { + assertBoardExists(bid) + } + + caller := std.OriginCaller() + args := Args{bid} + gPerms.WithPermission(caller, PermissionPermissionsUpdate, args, func(Args) { + // When board ID is zero it means that realm permissions are being updated + if bid == 0 { + gPerms = p + return + } + + // Otherwise update the permissions of a single board + board := mustGetBoard(bid) + board.perms = p + }) +} + // GetBoardIDFromName searches a board by name and returns it's ID. func GetBoardIDFromName(name string) (_ BoardID, found bool) { v, found := gBoardsByName.Get(name) @@ -23,10 +48,11 @@ func CreateBoard(name string) BoardID { caller := std.OriginCaller() id := incGetBoardID() args := Args{name, id} - gPerm.WithPermission(caller, PermissionBoardCreate, args, func(Args) { + gPerms.WithPermission(caller, PermissionBoardCreate, args, func(Args) { assertBoardNameNotExists(name) - board := newBoard(id, name, caller) + perms := createDefaultBoardPermissions(caller) + board := newBoard(id, name, caller, perms) gBoardsByID.Set(id.Key(), board) gBoardsByName.Set(name, board) }) @@ -461,5 +487,5 @@ func mustGetPermissions(bid BoardID) Permissions { board := mustGetBoard(bid) return board.perms } - return gPerm + return gPerms } diff --git a/examples/gno.land/r/nt/boards2/v1/render.gno b/examples/gno.land/r/nt/boards2/v1/render.gno index 6122c26fb75..8d356765043 100644 --- a/examples/gno.land/r/nt/boards2/v1/render.gno +++ b/examples/gno.land/r/nt/boards2/v1/render.gno @@ -1,11 +1,13 @@ package boards2 import ( + "net/url" "strconv" "strings" "gno.land/p/demo/mux" "gno.land/p/jeronimoalbi/pager" + "gno.land/p/moul/txlink" ) const ( @@ -14,6 +16,11 @@ const ( repliesPageSize = 30 ) +const ( + menuAdmin = "admin" + menuMembership = "membership" +) + func Render(path string) string { router := mux.NewRouter() router.HandleFunc("", renderBoardsList) @@ -31,6 +38,8 @@ func Render(path string) string { } func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) { + renderBoardListMenu(res, req) + res.Write("These are all the boards of this realm:\n\n") p := mustGetPagination(req.RawPath, boardsPageSize) page := p.Iterate(&gBoardsByID, func(_ string, value interface{}) bool { @@ -47,6 +56,26 @@ func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) { } } +func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) { + res.Write(newButtonLink("create board", txlink.Call("CreateBoard")) + " - ") + + menu := getCurrentSubmenu(req.RawPath) + if menu == menuMembership { + res.Write("**membership**") + } else { + res.Write(newButtonLink("membership", submenuURL(menuMembership))) + } + + res.Write("\n\n") + + if menu == menuMembership { + res.Write("↳") + res.Write(newButtonLink("invite", txlink.Call("InviteMember", "boardID", "0")) + " ") + res.Write(newButtonLink("remove member", txlink.Call("RemoveMember", "boardID", "0")) + " ") + res.Write(newButtonLink("change member role", txlink.Call("ChangeMemberRole", "boardID", "0")) + "\n\n") + } +} + func renderBoard(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") v, found := gBoardsByName.Get(name) @@ -56,10 +85,46 @@ func renderBoard(res *mux.ResponseWriter, req *mux.Request) { } board := v.(*Board) + renderBoardMenu(board, res, req) + p := mustGetPagination(req.RawPath, threadsPageSize) res.Write(board.Render(p)) } +func renderBoardMenu(board *Board, res *mux.ResponseWriter, req *mux.Request) { + res.Write(newButtonLink("post", board.GetPostFormURL()) + " - ") + + menu := getCurrentSubmenu(req.RawPath) + if menu == menuMembership { + res.Write("**membership** - ") + } else { + res.Write(newButtonLink("membership", submenuURL(menuMembership)) + " - ") + } + + if menu == menuAdmin { + res.Write("**admin**") + } else { + res.Write(newButtonLink("admin", submenuURL(menuAdmin))) + } + + res.Write("\n\n") + + if menu != "" { + res.Write("↳") + } + + switch menu { + case menuAdmin: + res.Write(newButtonLink("rename board", board.GetRenameFormURL()) + " ") + res.Write(newButtonLink("freeze", board.GetFreezeFormURL()) + " ") + res.Write(newButtonLink("change flagging threshold", board.GetFlaggingThresholdFormURL()) + "\n\n") + case menuMembership: + res.Write(newButtonLink("invite", board.GetInviteMemberFormURL()) + " ") + res.Write(newButtonLink("remove member", board.GetRemoveMemberFormURL()) + " ") + res.Write(newButtonLink("change member role", board.GetChangeMemberRoleFormURL()) + "\n\n") + } +} + func renderThread(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") v, found := gBoardsByName.Get(name) @@ -129,7 +194,7 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { } func renderMembers(res *mux.ResponseWriter, req *mux.Request) { - perms := gPerm + perms := gPerms name := req.GetVar("board") if name != "" { v, found := gBoardsByName.Get(name) @@ -173,3 +238,18 @@ func rolesToString(roles []Role) string { } return strings.Join(names, ", ") } + +func submenuURL(name string) string { + // TODO: Submenu URL works because no other GET arguments are being used + return "?submenu=" + name +} + +func getCurrentSubmenu(rawURL string) string { + _, rawQuery, found := strings.Cut(rawURL, "?") + if !found { + return "" + } + + query, _ := url.ParseQuery(rawQuery) + return query.Get("submenu") +} diff --git a/examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno new file mode 100644 index 00000000000..4c16b2ea07f --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno @@ -0,0 +1,33 @@ +package main + +import ( + "std" + + "gno.land/p/nt/commondao" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + bid = boards2.BoardID(0) // Operate on realm instead of individual boards +) + +var perms boards2.Permissions + +func init() { + // Create a new permissions instance without users + perms = boards2.NewDefaultPermissions(commondao.New()) + + std.TestSetOriginCaller(owner) +} + +func main() { + boards2.SetPermissions(bid, perms) + + // Owner that setted new permissions is not a member of the new permissions + println(boards2.IsMember(bid, owner)) +} + +// Output: +// false diff --git a/examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno new file mode 100644 index 00000000000..3d65b049b5b --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/p/nt/commondao" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const ( + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm instead of individual boards +) + +var perms boards2.Permissions + +func init() { + // Create a new permissions instance + perms = boards2.NewDefaultPermissions(commondao.New()) + + std.TestSetOriginCaller(user) +} + +func main() { + boards2.SetPermissions(bid, perms) +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno new file mode 100644 index 00000000000..84c4cad0fe3 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/p/nt/commondao" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + perms boards2.Permissions + bid boards2.BoardID +) + +func init() { + // Create a new permissions instance without users + perms = boards2.NewDefaultPermissions(commondao.New()) + + std.TestSetOriginCaller(owner) + bid = boards2.CreateBoard("foobar") +} + +func main() { + boards2.SetPermissions(bid, perms) + + // Owner that setted new board permissions is not a member of the new permissions + println(boards2.IsMember(bid, owner)) +} + +// Output: +// false diff --git a/gnovm/pkg/gnolang/values_string.go b/gnovm/pkg/gnolang/values_string.go index b18bdb0dddc..18a1ece679f 100644 --- a/gnovm/pkg/gnolang/values_string.go +++ b/gnovm/pkg/gnolang/values_string.go @@ -12,11 +12,17 @@ type protectedStringer interface { ProtectedString(*seenValues) string } -// This indicates the maximum anticipated depth of the stack when printing a Value type. -const defaultSeenValuesSize = 32 +const ( + // defaultSeenValuesSize indicates the maximum anticipated depth of the stack when printing a Value type. + defaultSeenValuesSize = 32 + + // nestedLimit indicates the maximum nested level when printing a deeply recursive value. + nestedLimit = 10 +) type seenValues struct { values []Value + nc int // nested counter, to limit recursivity } func (sv *seenValues) Put(v Value) { @@ -46,7 +52,7 @@ func (sv *seenValues) Pop() { } func newSeenValues() *seenValues { - return &seenValues{values: make([]Value, 0, defaultSeenValuesSize)} + return &seenValues{values: make([]Value, 0, defaultSeenValuesSize), nc: nestedLimit} } func (v StringValue) String() string { @@ -74,6 +80,10 @@ func (av *ArrayValue) ProtectedString(seen *seenValues) string { return fmt.Sprintf("%p", av) } + seen.nc-- + if seen.nc < 0 { + return "..." + } seen.Put(av) defer seen.Pop() diff --git a/gnovm/tests/files/recurse1.gno b/gnovm/tests/files/recurse1.gno new file mode 100644 index 00000000000..1d5f1c7fc72 --- /dev/null +++ b/gnovm/tests/files/recurse1.gno @@ -0,0 +1,12 @@ +package main + +func main() { + var x interface{} + for i := 0; i < 10000; i++ { + x = [1]interface{}{x} + } + println(x) +} + +// Output: +// array[(array[(array[(array[(array[(array[(array[(array[(array[(array[(... [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})] [1]interface{})]