diff --git a/examples/gno.land/p/n2p5/chonk/chonk.gno b/examples/gno.land/p/n2p5/chonk/chonk.gno new file mode 100644 index 00000000000..8b7425eafd0 --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/chonk.gno @@ -0,0 +1,84 @@ +// Package chonk provides a simple way to store arbitrarily large strings +// in a linked list across transactions for efficient storage and retrieval. +// A Chonk support three operations: Add, Flush, and Scanner. +// - Add appends a string to the Chonk. +// - Flush clears the Chonk. +// - Scanner is used to iterate over the chunks in the Chonk. +package chonk + +// Chonk is a linked list string storage and +// retrieval system for fine bois. +type Chonk struct { + first *chunk + last *chunk +} + +// chunk is a linked list node for Chonk +type chunk struct { + text string + next *chunk +} + +// New creates a reference to a new Chonk +func New() *Chonk { + return &Chonk{} +} + +// Add appends a string to the Chonk. If the Chonk is empty, +// the string will be the first and last chunk. Otherwise, +// the string will be appended to the end of the Chonk. +func (c *Chonk) Add(text string) { + next := &chunk{text: text} + if c.first == nil { + c.first = next + c.last = next + return + } + c.last.next = next + c.last = next +} + +// Flush clears the Chonk by setting the first and last +// chunks to nil. This will allow the garbage collector to +// free the memory used by the Chonk. +func (c *Chonk) Flush() { + c.first = nil + c.last = nil +} + +// Scanner returns a new Scanner for the Chonk. The Scanner +// is used to iterate over the chunks in the Chonk. +func (c *Chonk) Scanner() *Scanner { + return &Scanner{ + next: c.first, + } +} + +// Scanner is a simple string scanner for Chonk. It is used +// to iterate over the chunks in a Chonk from first to last. +type Scanner struct { + current *chunk + next *chunk +} + +// Scan advances the scanner to the next chunk. It returns +// true if there is a next chunk, and false if there is not. +func (s *Scanner) Scan() bool { + if s.next != nil { + s.current = s.next + s.next = s.next.next + return true + } + return false +} + +// Text returns the current chunk. It is only valid to call +// this method after a call to Scan returns true. Expected usage: +// +// scanner := chonk.Scanner() +// for scanner.Scan() { +// fmt.Println(scanner.Text()) +// } +func (s *Scanner) Text() string { + return s.current.text +} diff --git a/examples/gno.land/p/n2p5/chonk/chonk_test.gno b/examples/gno.land/p/n2p5/chonk/chonk_test.gno new file mode 100644 index 00000000000..7caf1012d39 --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/chonk_test.gno @@ -0,0 +1,54 @@ +package chonk + +import ( + "testing" +) + +func TestChonk(t *testing.T) { + t.Parallel() + c := New() + testTable := []struct { + name string + chunks []string + }{ + { + name: "empty", + chunks: []string{}, + }, + { + name: "single chunk", + chunks: []string{"a"}, + }, + { + name: "multiple chunks", + chunks: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + }, + { + name: "multiline chunks", + chunks: []string{"1a\nb\nc\n\n", "d\ne\nf", "g\nh\ni", "j\nk\nl\n\n\n\n"}, + }, + { + name: "empty", + chunks: []string{}, + }, + } + testChonk := func(t *testing.T, c *Chonk, chunks []string) { + for _, chunk := range chunks { + c.Add(chunk) + } + scanner := c.Scanner() + i := 0 + for scanner.Scan() { + if scanner.Text() != chunks[i] { + t.Errorf("expected %s, got %s", chunks[i], scanner.Text()) + } + i++ + } + } + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + testChonk(t, c, test.chunks) + c.Flush() + }) + } +} diff --git a/examples/gno.land/p/n2p5/chonk/gno.mod b/examples/gno.land/p/n2p5/chonk/gno.mod new file mode 100644 index 00000000000..b0dee537b0e --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/chonk diff --git a/examples/gno.land/p/n2p5/mgroup/gno.mod b/examples/gno.land/p/n2p5/mgroup/gno.mod new file mode 100644 index 00000000000..95fdbe2f195 --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/n2p5/mgroup + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest +) diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup.gno b/examples/gno.land/p/n2p5/mgroup/mgroup.gno new file mode 100644 index 00000000000..0c029401ff7 --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/mgroup.gno @@ -0,0 +1,184 @@ +// Package mgroup is a simple managed group managing ownership and membership +// for authorization in gno realms. The ManagedGroup struct is used to manage +// the owner, backup owners, and members of a group. The owner is the primary +// owner of the group and can add and remove backup owners and members. Backup +// owners can claim ownership of the group. This is meant to provide backup +// accounts for the owner in case the owner account is lost or compromised. +// Members are used to authorize actions across realms. +package mgroup + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +var ( + ErrCannotRemoveOwner = errors.New("mgroup: cannot remove owner") + ErrNotBackupOwner = errors.New("mgroup: not a backup owner") + ErrNotMember = errors.New("mgroup: not a member") + ErrInvalidAddress = errors.New("mgroup: address is invalid") +) + +type ManagedGroup struct { + owner *ownable.Ownable + backupOwners *avl.Tree + members *avl.Tree +} + +// New creates a new ManagedGroup with the owner set to the provided address. +// The owner is automatically added as a backup owner and member of the group. +func New(ownerAddress std.Address) *ManagedGroup { + g := &ManagedGroup{ + owner: ownable.NewWithAddress(ownerAddress), + backupOwners: avl.NewTree(), + members: avl.NewTree(), + } + g.AddBackupOwner(ownerAddress) + g.AddMember(ownerAddress) + return g +} + +// AddBackupOwner adds a backup owner to the group by std.Address. +// If the caller is not the owner, an error is returned. +func (g *ManagedGroup) AddBackupOwner(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + g.backupOwners.Set(addr.String(), struct{}{}) + return nil +} + +// RemoveBackupOwner removes a backup owner from the group by std.Address. +// The owner cannot be removed. If the caller is not the owner, an error is returned. +func (g *ManagedGroup) RemoveBackupOwner(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + if addr == g.Owner() { + return ErrCannotRemoveOwner + } + g.backupOwners.Remove(addr.String()) + return nil +} + +// ClaimOwnership allows a backup owner to claim ownership of the group. +// If the caller is not a backup owner, an error is returned. +// The caller is automatically added as a member of the group. +func (g *ManagedGroup) ClaimOwnership() error { + caller := std.PrevRealm().Addr() + // already owner, skip + if caller == g.Owner() { + return nil + } + if !g.IsBackupOwner(caller) { + return ErrNotMember + } + g.owner = ownable.NewWithAddress(caller) + g.AddMember(caller) + return nil +} + +// AddMember adds a member to the group by std.Address. +// If the caller is not the owner, an error is returned. +func (g *ManagedGroup) AddMember(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + g.members.Set(addr.String(), struct{}{}) + return nil +} + +// RemoveMember removes a member from the group by std.Address. +// The owner cannot be removed. If the caller is not the owner, +// an error is returned. +func (g *ManagedGroup) RemoveMember(addr std.Address) error { + if err := g.owner.CallerIsOwner(); err != nil { + return err + } + if !addr.IsValid() { + return ErrInvalidAddress + } + if addr == g.Owner() { + return ErrCannotRemoveOwner + } + g.members.Remove(addr.String()) + return nil +} + +// MemberCount returns the number of members in the group. +func (g *ManagedGroup) MemberCount() int { + return g.members.Size() +} + +// BackupOwnerCount returns the number of backup owners in the group. +func (g *ManagedGroup) BackupOwnerCount() int { + return g.backupOwners.Size() +} + +// IsMember checks if an address is a member of the group. +func (g *ManagedGroup) IsMember(addr std.Address) bool { + return g.members.Has(addr.String()) +} + +// IsBackupOwner checks if an address is a backup owner in the group. +func (g *ManagedGroup) IsBackupOwner(addr std.Address) bool { + return g.backupOwners.Has(addr.String()) +} + +// Owner returns the owner of the group. +func (g *ManagedGroup) Owner() std.Address { + return g.owner.Owner() +} + +// BackupOwners returns a slice of all backup owners in the group, using the underlying +// avl.Tree to iterate over the backup owners. If you have a large group, you may +// want to use BackupOwnersWithOffset to iterate over backup owners in chunks. +func (g *ManagedGroup) BackupOwners() []string { + return g.BackupOwnersWithOffset(0, g.BackupOwnerCount()) +} + +// Members returns a slice of all members in the group, using the underlying +// avl.Tree to iterate over the members. If you have a large group, you may +// want to use MembersWithOffset to iterate over members in chunks. +func (g *ManagedGroup) Members() []string { + return g.MembersWithOffset(0, g.MemberCount()) +} + +// BackupOwnersWithOffset returns a slice of backup owners in the group, using the underlying +// avl.Tree to iterate over the backup owners. The offset and count parameters allow you +// to iterate over backup owners in chunks to support patterns such as pagination. +func (g *ManagedGroup) BackupOwnersWithOffset(offset, count int) []string { + return sliceWithOffset(g.backupOwners, offset, count) +} + +// MembersWithOffset returns a slice of members in the group, using the underlying +// avl.Tree to iterate over the members. The offset and count parameters allow you +// to iterate over members in chunks to support patterns such as pagination. +func (g *ManagedGroup) MembersWithOffset(offset, count int) []string { + return sliceWithOffset(g.members, offset, count) +} + +// sliceWithOffset is a helper function to iterate over an avl.Tree with an offset and count. +func sliceWithOffset(t *avl.Tree, offset, count int) []string { + var result []string + t.IterateByOffset(offset, count, func(k string, _ interface{}) bool { + if k == "" { + return true + } + result = append(result, k) + return false + }) + return result +} diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno new file mode 100644 index 00000000000..7ef0619188f --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno @@ -0,0 +1,420 @@ +package mgroup + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" +) + +func TestManagedGroup(t *testing.T) { + t.Parallel() + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + u3 := testutils.TestAddress("u3") + + t.Run("AddBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + err := g.AddBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.AddBackupOwner(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.AddBackupOwner(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + }) + t.Run("RemoveBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + g.AddBackupOwner(u2) + err := g.RemoveBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u1) + err := g.RemoveBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.RemoveBackupOwner(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error()) + } + } + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.RemoveBackupOwner(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + { + std.TestSetOrigCaller(u1) + err := g.RemoveBackupOwner(u1) + if err != ErrCannotRemoveOwner { + t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error()) + } + } + }) + t.Run("ClaimOwnership", func(t *testing.T) { + t.Parallel() + g := New(u1) + g.AddBackupOwner(u2) + // happy path + { + std.TestSetOrigCaller(u2) + err := g.ClaimOwnership() + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if g.Owner() != u2 { + t.Errorf("expected %v, got %v", u2, g.Owner()) + } + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u2) + err := g.ClaimOwnership() + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u3) + err := g.ClaimOwnership() + if err != ErrNotMember { + t.Errorf("expected %v, got %v", ErrNotMember.Error(), err.Error()) + } + } + }) + t.Run("AddMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + err := g.AddMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.AddMember(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.AddMember(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + }) + t.Run("RemoveMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + g.AddMember(u2) + err := g.RemoveMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if g.IsMember(u2) { + t.Errorf("expected %v to not be a member", u2) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u1) + err := g.RemoveMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.RemoveMember(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.RemoveMember(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + // ensure owner cannot be removed + { + std.TestSetOrigCaller(u1) + err := g.RemoveMember(u1) + if err != ErrCannotRemoveOwner { + t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error()) + } + } + }) + t.Run("MemberCount", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.MemberCount() != 1 { + t.Errorf("expected 0, got %v", g.MemberCount()) + } + g.AddMember(u2) + if g.MemberCount() != 2 { + t.Errorf("expected 1, got %v", g.MemberCount()) + } + g.AddMember(u3) + if g.MemberCount() != 3 { + t.Errorf("expected 2, got %v", g.MemberCount()) + } + g.RemoveMember(u2) + if g.MemberCount() != 2 { + t.Errorf("expected 1, got %v", g.MemberCount()) + } + }) + t.Run("BackupOwnerCount", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.BackupOwnerCount() != 1 { + t.Errorf("expected 0, got %v", g.BackupOwnerCount()) + } + g.AddBackupOwner(u2) + if g.BackupOwnerCount() != 2 { + t.Errorf("expected 1, got %v", g.BackupOwnerCount()) + } + g.AddBackupOwner(u3) + if g.BackupOwnerCount() != 3 { + t.Errorf("expected 2, got %v", g.BackupOwnerCount()) + } + g.RemoveBackupOwner(u2) + if g.BackupOwnerCount() != 2 { + t.Errorf("expected 1, got %v", g.BackupOwnerCount()) + } + }) + t.Run("IsMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + if !g.IsMember(u1) { + t.Errorf("expected %v to be a member", u1) + } + if g.IsMember(u2) { + t.Errorf("expected %v to not be a member", u2) + } + g.AddMember(u2) + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + }) + t.Run("IsBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + if !g.IsBackupOwner(u1) { + t.Errorf("expected %v to be a backup owner", u1) + } + if g.IsBackupOwner(u2) { + t.Errorf("expected %v to not be a backup owner", u2) + } + g.AddBackupOwner(u2) + if !g.IsBackupOwner(u2) { + t.Errorf("expected %v to be a backup owner", u2) + } + }) + t.Run("Owner", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.Owner() != u1 { + t.Errorf("expected %v, got %v", u1, g.Owner()) + } + g.AddBackupOwner(u2) + if g.Owner() != u1 { + t.Errorf("expected %v, got %v", u1, g.Owner()) + } + std.TestSetOrigCaller(u2) + g.ClaimOwnership() + if g.Owner() != u2 { + t.Errorf("expected %v, got %v", u2, g.Owner()) + } + }) + t.Run("BackupOwners", func(t *testing.T) { + t.Parallel() + std.TestSetOrigCaller(u1) + g := New(u1) + g.AddBackupOwner(u2) + g.AddBackupOwner(u3) + owners := g.BackupOwners() + if len(owners) != 3 { + t.Errorf("expected 2, got %v", len(owners)) + } + if owners[0] != u2.String() { + t.Errorf("expected %v, got %v", u2, owners[0]) + } + if owners[1] != u3.String() { + t.Errorf("expected %v, got %v", u3, owners[1]) + } + if owners[2] != u1.String() { + t.Errorf("expected %v, got %v", u3, owners[1]) + } + }) + t.Run("Members", func(t *testing.T) { + t.Parallel() + std.TestSetOrigCaller(u1) + g := New(u1) + g.AddMember(u2) + g.AddMember(u3) + members := g.Members() + if len(members) != 3 { + t.Errorf("expected 2, got %v", len(members)) + } + if members[0] != u2.String() { + t.Errorf("expected %v, got %v", u2, members[0]) + } + if members[1] != u3.String() { + t.Errorf("expected %v, got %v", u3, members[1]) + } + if members[2] != u1.String() { + t.Errorf("expected %v, got %v", u3, members[1]) + } + }) +} + +func TestSliceWithOffset(t *testing.T) { + t.Parallel() + testTable := []struct { + name string + slice []string + offset int + count int + expected []string + expectedCount int + }{ + { + name: "empty", + slice: []string{}, + offset: 0, + count: 0, + expected: []string{}, + expectedCount: 0, + }, + { + name: "single", + slice: []string{"a"}, + offset: 0, + count: 1, + expected: []string{"a"}, + expectedCount: 1, + }, + { + name: "single offset", + slice: []string{"a"}, + offset: 1, + count: 1, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 0, + count: 10, + expected: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + expectedCount: 10, + }, + { + name: "multiple offset", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 5, + count: 5, + expected: []string{"f", "g", "h", "i", "j"}, + expectedCount: 5, + }, + { + name: "multiple offset end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 10, + count: 5, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple offset past end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 11, + count: 5, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple offset count past end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 5, + count: 20, + expected: []string{"f", "g", "h", "i", "j"}, + expectedCount: 5, + }, + } + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + tree := avl.NewTree() + for _, s := range test.slice { + tree.Set(s, struct{}{}) + } + slice := sliceWithOffset(tree, test.offset, test.count) + if len(slice) != test.expectedCount { + t.Errorf("expected %v, got %v", test.expectedCount, len(slice)) + } + }) + } +} diff --git a/examples/gno.land/r/n2p5/config/config.gno b/examples/gno.land/r/n2p5/config/config.gno new file mode 100644 index 00000000000..42cb587eaf5 --- /dev/null +++ b/examples/gno.land/r/n2p5/config/config.gno @@ -0,0 +1,120 @@ +package config + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/n2p5/mgroup" +) + +const ( + originalOwner = "g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t" // n2p5 +) + +var ( + adminGroup = mgroup.New(originalOwner) + description = "" +) + +// AddBackupOwner adds a backup owner to the Owner Group. +// A backup owner can claim ownership of the contract. +func AddBackupOwner(addr std.Address) { + err := adminGroup.AddBackupOwner(addr) + if err != nil { + panic(err) + } +} + +// RemoveBackupOwner removes a backup owner from the Owner Group. +// The primary owner cannot be removed. +func RemoveBackupOwner(addr std.Address) { + err := adminGroup.RemoveBackupOwner(addr) + if err != nil { + panic(err) + } +} + +// ClaimOwnership allows an authorized user in the ownerGroup +// to claim ownership of the contract. +func ClaimOwnership() { + err := adminGroup.ClaimOwnership() + if err != nil { + panic(err) + } +} + +// AddAdmin adds an admin to the Admin Group. +func AddAdmin(addr std.Address) { + err := adminGroup.AddMember(addr) + if err != nil { + panic(err) + } +} + +// RemoveAdmin removes an admin from the Admin Group. +// The primary owner cannot be removed. +func RemoveAdmin(addr std.Address) { + err := adminGroup.RemoveMember(addr) + if err != nil { + panic(err) + } +} + +// Owner returns the current owner of the claims contract. +func Owner() std.Address { + return adminGroup.Owner() +} + +// BackupOwners returns the current backup owners of the claims contract. +func BackupOwners() []string { + return adminGroup.BackupOwners() +} + +// Admins returns the current admin members of the claims contract. +func Admins() []string { + return adminGroup.Members() +} + +// IsAdmin checks if an address is in the config adminGroup. +func IsAdmin(addr std.Address) bool { + return adminGroup.IsMember(addr) +} + +// toMarkdownList formats a slice of strings as a markdown list. +func toMarkdownList(items []string) string { + var result string + for _, item := range items { + result += ufmt.Sprintf("- %s\n", item) + } + return result +} + +func Render(path string) string { + owner := adminGroup.Owner().String() + backupOwners := toMarkdownList(BackupOwners()) + adminMembers := toMarkdownList(Admins()) + return ufmt.Sprintf(` +# Config Dashboard + +This dashboard shows the current configuration owner, backup owners, and admin members. +- The owner has the exclusive ability to manage the backup owners and admin members. +- Backup owners can claim ownership of the contract and become the owner. +- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home). + +#### Owner + +%s + +#### Backup Owners + +%s + +#### Admin Members + +%s + +`, + owner, + backupOwners, + adminMembers) +} diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod new file mode 100644 index 00000000000..33f9276a409 --- /dev/null +++ b/examples/gno.land/r/n2p5/config/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/n2p5/config + +require ( + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/n2p5/mgroup v0.0.0-latest +) diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod new file mode 100644 index 00000000000..779aa914989 --- /dev/null +++ b/examples/gno.land/r/n2p5/home/gno.mod @@ -0,0 +1,7 @@ +module gno.land/r/n2p5/home + +require ( + gno.land/p/n2p5/chonk v0.0.0-latest + gno.land/r/leon/hof v0.0.0-latest + gno.land/r/n2p5/config v0.0.0-latest +) diff --git a/examples/gno.land/r/n2p5/home/home.gno b/examples/gno.land/r/n2p5/home/home.gno new file mode 100644 index 00000000000..69b82e86d68 --- /dev/null +++ b/examples/gno.land/r/n2p5/home/home.gno @@ -0,0 +1,73 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/n2p5/chonk" + + "gno.land/r/leon/hof" + "gno.land/r/n2p5/config" +) + +var ( + active = chonk.New() + preview = chonk.New() +) + +func init() { + hof.Register() +} + +// Add appends a string to the preview Chonk. +func Add(chunk string) { + assertAdmin() + preview.Add(chunk) +} + +// Flush clears the preview Chonk. +func Flush() { + assertAdmin() + preview.Flush() +} + +// Promote promotes the preview Chonk to the active Chonk +// and creates a new preview Chonk. +func Promote() { + assertAdmin() + active = preview + preview = chonk.New() +} + +// Render returns the contents of the scanner for the active or preview Chonk +// based on the path provided. +func Render(path string) string { + var result string + scanner := getScanner(path) + for scanner.Scan() { + result += scanner.Text() + } + return result +} + +// assertAdmin panics if the caller is not an admin as defined in the config realm. +func assertAdmin() { + caller := std.PrevRealm().Addr() + if !config.IsAdmin(caller) { + panic("forbidden: must be admin") + } +} + +// getScanner returns the scanner for the active or preview Chonk based +// on the path provided. +func getScanner(path string) *chonk.Scanner { + if isPreview(path) { + return preview.Scanner() + } + return active.Scanner() +} + +// isPreview returns true if the path prefix is "preview". +func isPreview(path string) bool { + return strings.HasPrefix(path, "preview") +}