Skip to content

Commit

Permalink
feat(examples): hall of fame (#2842)
Browse files Browse the repository at this point in the history
## Description

Depends on #2584 for `avlpager`

Introduces the `r/demo/hof` realm.

The Hall of Fame is an exhibition that holds items. Users can add their
realms to the Hall of Fame by importing the Hall of Fame realm and
calling `hof.Register()` from their `init` function.

The realm is moderated and the registrations be paused at will.

![Screenshot 2024-10-07 at 20 09
43](https://github.com/user-attachments/assets/9beeefc6-d22a-4e81-aa2d-e336d0e6edf8)

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
- [x] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Signed-off-by: moul <[email protected]>
Co-authored-by: moul <[email protected]>
Co-authored-by: Antonio Navarro Perez <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2024
1 parent 6c3cc02 commit 1993c69
Show file tree
Hide file tree
Showing 18 changed files with 482 additions and 12 deletions.
7 changes: 6 additions & 1 deletion examples/gno.land/p/demo/fqname/fqname.gno
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// package-level declaration.
package fqname

import "strings"
import (
"strings"
)

// Parse splits a fully qualified identifier into its package path and name
// components. It handles cases with and without slashes in the package path.
Expand Down Expand Up @@ -63,10 +65,13 @@ func RenderLink(pkgPath, slug string) string {
if slug != "" {
return "[" + pkgPath + "](" + pkgLink + ")." + slug
}

return "[" + pkgPath + "](" + pkgLink + ")"
}

if slug != "" {
return pkgPath + "." + slug
}

return pkgPath
}
6 changes: 3 additions & 3 deletions examples/gno.land/p/demo/ownable/ownable.gno
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error {
o.owner = newOwner
std.Emit(
OwnershipTransferEvent,
"from", string(prevOwner),
"to", string(newOwner),
"from", prevOwner.String(),
"to", newOwner.String(),
)

return nil
Expand All @@ -58,7 +58,7 @@ func (o *Ownable) DropOwnership() error {

std.Emit(
OwnershipTransferEvent,
"from", string(prevOwner),
"from", prevOwner.String(),
"to", "",
)

Expand Down
10 changes: 9 additions & 1 deletion examples/gno.land/p/demo/pausable/pausable.gno
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package pausable

import "gno.land/p/demo/ownable"
import (
"std"

"gno.land/p/demo/ownable"
)

type Pausable struct {
*ownable.Ownable
Expand Down Expand Up @@ -35,6 +39,8 @@ func (p *Pausable) Pause() error {
}

p.paused = true
std.Emit("Paused", "account", p.Owner().String())

return nil
}

Expand All @@ -45,5 +51,7 @@ func (p *Pausable) Unpause() error {
}

p.paused = false
std.Emit("Unpaused", "account", p.Owner().String())

return nil
}
24 changes: 24 additions & 0 deletions examples/gno.land/r/demo/hof/administration.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package hof

import "std"

// Exposing the ownable & pausable APIs
// Should not be needed as soon as MsgCall supports calling methods on exported variables

func Pause() error {
return exhibition.Pause()
}

func Unpause() error {
return exhibition.Unpause()
}

func GetOwner() std.Address {
return owner.Owner()
}

func TransferOwnership(newOwner std.Address) {
if err := owner.TransferOwnership(newOwner); err != nil {
panic(err)
}
}
11 changes: 11 additions & 0 deletions examples/gno.land/r/demo/hof/errors.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hof

import (
"errors"
)

var (
ErrNoSuchItem = errors.New("hof: no such item exists")
ErrDoubleUpvote = errors.New("hof: cannot upvote twice")
ErrDoubleDownvote = errors.New("hof: cannot downvote twice")
)
15 changes: 15 additions & 0 deletions examples/gno.land/r/demo/hof/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module gno.land/r/demo/hof

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/avl/pager v0.0.0-latest
gno.land/p/demo/fqname v0.0.0-latest
gno.land/p/demo/ownable v0.0.0-latest
gno.land/p/demo/pausable v0.0.0-latest
gno.land/p/demo/seqid v0.0.0-latest
gno.land/p/demo/testutils v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/urequire v0.0.0-latest
gno.land/p/moul/txlink v0.0.0-latest
)
132 changes: 132 additions & 0 deletions examples/gno.land/r/demo/hof/hof.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Package hof is the hall of fame realm.
// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by
// importing the Hall of Fame realm and calling hof.Register() from their init function.
package hof

import (
"std"

"gno.land/p/demo/avl"
"gno.land/p/demo/ownable"
"gno.land/p/demo/pausable"
"gno.land/p/demo/seqid"
)

var (
exhibition *Exhibition
owner *ownable.Ownable
)

type (
Exhibition struct {
itemCounter seqid.ID
description string
items *avl.Tree // pkgPath > Item
itemsSorted *avl.Tree // same data but sorted, storing pointers
*pausable.Pausable
}

Item struct {
id seqid.ID
pkgpath string
blockNum int64
upvote *avl.Tree // std.Addr > struct{}{}
downvote *avl.Tree // std.Addr > struct{}{}
}
)

func init() {
exhibition = &Exhibition{
items: avl.NewTree(),
itemsSorted: avl.NewTree(),
}

owner = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5"))
exhibition.Pausable = pausable.NewFromOwnable(owner)
}

// Register registers your realm to the Hall of Fame
// Should be called from within code
func Register() {
if exhibition.IsPaused() {
return
}

submission := std.PrevRealm()
pkgpath := submission.PkgPath()

// Must be called from code
if submission.IsUser() {
return
}

// Must not yet exist
if exhibition.items.Has(pkgpath) {
return
}

id := exhibition.itemCounter.Next()
i := &Item{
id: id,
pkgpath: pkgpath,
blockNum: std.GetHeight(),
upvote: avl.NewTree(),
downvote: avl.NewTree(),
}

exhibition.items.Set(pkgpath, i)
exhibition.itemsSorted.Set(id.String(), i)

std.Emit("Registration")
}

func Upvote(pkgpath string) {
rawItem, ok := exhibition.items.Get(pkgpath)
if !ok {
panic(ErrNoSuchItem.Error())
}

item := rawItem.(*Item)
caller := std.PrevRealm().Addr().String()

if item.upvote.Has(caller) {
panic(ErrDoubleUpvote.Error())
}

item.upvote.Set(caller, struct{}{})
}

func Downvote(pkgpath string) {
rawItem, ok := exhibition.items.Get(pkgpath)
if !ok {
panic(ErrNoSuchItem.Error())
}

item := rawItem.(*Item)
caller := std.PrevRealm().Addr().String()

if item.downvote.Has(caller) {
panic(ErrDoubleDownvote.Error())
}

item.downvote.Set(caller, struct{}{})
}

func Delete(pkgpath string) {
if err := owner.CallerIsOwner(); err != nil {
panic(err)
}

i, ok := exhibition.items.Get(pkgpath)
if !ok {
panic(ErrNoSuchItem.Error())
}

if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed {
panic(ErrNoSuchItem.Error())
}

if _, removed := exhibition.items.Remove(pkgpath); !removed {
panic(ErrNoSuchItem.Error())
}
}
134 changes: 134 additions & 0 deletions examples/gno.land/r/demo/hof/hof_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package hof

import (
"std"
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
)

const rlmPath = "gno.land/r/gnoland/home"

var (
admin = owner.Owner()
adminRealm = std.NewUserRealm(admin)
alice = testutils.TestAddress("alice")
)

func TestRegister(t *testing.T) {
// Test user realm register
aliceRealm := std.NewUserRealm(alice)
std.TestSetRealm(aliceRealm)

Register()
uassert.False(t, itemExists(t, rlmPath))

// Test register while paused
std.TestSetRealm(adminRealm)
Pause()

// Set legitimate caller
std.TestSetRealm(std.NewCodeRealm(rlmPath))

Register()
uassert.False(t, itemExists(t, rlmPath))

// Unpause
std.TestSetRealm(adminRealm)
Unpause()

// Set legitimate caller
std.TestSetRealm(std.NewCodeRealm(rlmPath))
Register()

// Find registered items
uassert.True(t, itemExists(t, rlmPath))
}

func TestUpvote(t *testing.T) {
raw, _ := exhibition.items.Get(rlmPath)
item := raw.(*Item)

rawSorted, _ := exhibition.itemsSorted.Get(item.id.String())
itemSorted := rawSorted.(*Item)

// 0 upvotes by default
urequire.Equal(t, item.upvote.Size(), 0)

std.TestSetRealm(adminRealm)

urequire.NotPanics(t, func() {
Upvote(rlmPath)
})

// Check both trees for 1 upvote
uassert.Equal(t, item.upvote.Size(), 1)
uassert.Equal(t, itemSorted.upvote.Size(), 1)

// Check double upvote
uassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() {
Upvote(rlmPath)
})
}

func TestDownvote(t *testing.T) {
raw, _ := exhibition.items.Get(rlmPath)
item := raw.(*Item)

rawSorted, _ := exhibition.itemsSorted.Get(item.id.String())
itemSorted := rawSorted.(*Item)

// 0 downvotes by default
urequire.Equal(t, item.downvote.Size(), 0)

userRealm := std.NewUserRealm(alice)
std.TestSetRealm(userRealm)

urequire.NotPanics(t, func() {
Downvote(rlmPath)
})

// Check both trees for 1 upvote
uassert.Equal(t, item.downvote.Size(), 1)
uassert.Equal(t, itemSorted.downvote.Size(), 1)

// Check double downvote
uassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() {
Downvote(rlmPath)
})
}

func TestDelete(t *testing.T) {
userRealm := std.NewUserRealm(admin)
std.TestSetRealm(userRealm)
std.TestSetOrigCaller(admin)

uassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() {
Delete("nonexistentpkgpath")
})

i, _ := exhibition.items.Get(rlmPath)
id := i.(*Item).id

uassert.NotPanics(t, func() {
Delete(rlmPath)
})

uassert.False(t, exhibition.items.Has(rlmPath))
uassert.False(t, exhibition.itemsSorted.Has(id.String()))
}

func itemExists(t *testing.T, rlmPath string) bool {
t.Helper()

i, ok1 := exhibition.items.Get(rlmPath)
ok2 := false

if ok1 {
_, ok2 = exhibition.itemsSorted.Get(i.(*Item).id.String())
}

return ok1 && ok2
}
Loading

0 comments on commit 1993c69

Please sign in to comment.