Skip to content

Commit 1993c69

Browse files
leohhhnmoulajnavarro
authored
feat(examples): hall of fame (#2842)
## 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]>
1 parent 6c3cc02 commit 1993c69

File tree

18 files changed

+482
-12
lines changed

18 files changed

+482
-12
lines changed

examples/gno.land/p/demo/fqname/fqname.gno

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
// package-level declaration.
55
package fqname
66

7-
import "strings"
7+
import (
8+
"strings"
9+
)
810

911
// Parse splits a fully qualified identifier into its package path and name
1012
// components. It handles cases with and without slashes in the package path.
@@ -63,10 +65,13 @@ func RenderLink(pkgPath, slug string) string {
6365
if slug != "" {
6466
return "[" + pkgPath + "](" + pkgLink + ")." + slug
6567
}
68+
6669
return "[" + pkgPath + "](" + pkgLink + ")"
6770
}
71+
6872
if slug != "" {
6973
return pkgPath + "." + slug
7074
}
75+
7176
return pkgPath
7277
}

examples/gno.land/p/demo/ownable/ownable.gno

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error {
3737
o.owner = newOwner
3838
std.Emit(
3939
OwnershipTransferEvent,
40-
"from", string(prevOwner),
41-
"to", string(newOwner),
40+
"from", prevOwner.String(),
41+
"to", newOwner.String(),
4242
)
4343

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

5959
std.Emit(
6060
OwnershipTransferEvent,
61-
"from", string(prevOwner),
61+
"from", prevOwner.String(),
6262
"to", "",
6363
)
6464

examples/gno.land/p/demo/pausable/pausable.gno

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package pausable
22

3-
import "gno.land/p/demo/ownable"
3+
import (
4+
"std"
5+
6+
"gno.land/p/demo/ownable"
7+
)
48

59
type Pausable struct {
610
*ownable.Ownable
@@ -35,6 +39,8 @@ func (p *Pausable) Pause() error {
3539
}
3640

3741
p.paused = true
42+
std.Emit("Paused", "account", p.Owner().String())
43+
3844
return nil
3945
}
4046

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

4753
p.paused = false
54+
std.Emit("Unpaused", "account", p.Owner().String())
55+
4856
return nil
4957
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package hof
2+
3+
import "std"
4+
5+
// Exposing the ownable & pausable APIs
6+
// Should not be needed as soon as MsgCall supports calling methods on exported variables
7+
8+
func Pause() error {
9+
return exhibition.Pause()
10+
}
11+
12+
func Unpause() error {
13+
return exhibition.Unpause()
14+
}
15+
16+
func GetOwner() std.Address {
17+
return owner.Owner()
18+
}
19+
20+
func TransferOwnership(newOwner std.Address) {
21+
if err := owner.TransferOwnership(newOwner); err != nil {
22+
panic(err)
23+
}
24+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package hof
2+
3+
import (
4+
"errors"
5+
)
6+
7+
var (
8+
ErrNoSuchItem = errors.New("hof: no such item exists")
9+
ErrDoubleUpvote = errors.New("hof: cannot upvote twice")
10+
ErrDoubleDownvote = errors.New("hof: cannot downvote twice")
11+
)

examples/gno.land/r/demo/hof/gno.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module gno.land/r/demo/hof
2+
3+
require (
4+
gno.land/p/demo/avl v0.0.0-latest
5+
gno.land/p/demo/avl/pager v0.0.0-latest
6+
gno.land/p/demo/fqname v0.0.0-latest
7+
gno.land/p/demo/ownable v0.0.0-latest
8+
gno.land/p/demo/pausable v0.0.0-latest
9+
gno.land/p/demo/seqid v0.0.0-latest
10+
gno.land/p/demo/testutils v0.0.0-latest
11+
gno.land/p/demo/uassert v0.0.0-latest
12+
gno.land/p/demo/ufmt v0.0.0-latest
13+
gno.land/p/demo/urequire v0.0.0-latest
14+
gno.land/p/moul/txlink v0.0.0-latest
15+
)

examples/gno.land/r/demo/hof/hof.gno

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Package hof is the hall of fame realm.
2+
// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by
3+
// importing the Hall of Fame realm and calling hof.Register() from their init function.
4+
package hof
5+
6+
import (
7+
"std"
8+
9+
"gno.land/p/demo/avl"
10+
"gno.land/p/demo/ownable"
11+
"gno.land/p/demo/pausable"
12+
"gno.land/p/demo/seqid"
13+
)
14+
15+
var (
16+
exhibition *Exhibition
17+
owner *ownable.Ownable
18+
)
19+
20+
type (
21+
Exhibition struct {
22+
itemCounter seqid.ID
23+
description string
24+
items *avl.Tree // pkgPath > Item
25+
itemsSorted *avl.Tree // same data but sorted, storing pointers
26+
*pausable.Pausable
27+
}
28+
29+
Item struct {
30+
id seqid.ID
31+
pkgpath string
32+
blockNum int64
33+
upvote *avl.Tree // std.Addr > struct{}{}
34+
downvote *avl.Tree // std.Addr > struct{}{}
35+
}
36+
)
37+
38+
func init() {
39+
exhibition = &Exhibition{
40+
items: avl.NewTree(),
41+
itemsSorted: avl.NewTree(),
42+
}
43+
44+
owner = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5"))
45+
exhibition.Pausable = pausable.NewFromOwnable(owner)
46+
}
47+
48+
// Register registers your realm to the Hall of Fame
49+
// Should be called from within code
50+
func Register() {
51+
if exhibition.IsPaused() {
52+
return
53+
}
54+
55+
submission := std.PrevRealm()
56+
pkgpath := submission.PkgPath()
57+
58+
// Must be called from code
59+
if submission.IsUser() {
60+
return
61+
}
62+
63+
// Must not yet exist
64+
if exhibition.items.Has(pkgpath) {
65+
return
66+
}
67+
68+
id := exhibition.itemCounter.Next()
69+
i := &Item{
70+
id: id,
71+
pkgpath: pkgpath,
72+
blockNum: std.GetHeight(),
73+
upvote: avl.NewTree(),
74+
downvote: avl.NewTree(),
75+
}
76+
77+
exhibition.items.Set(pkgpath, i)
78+
exhibition.itemsSorted.Set(id.String(), i)
79+
80+
std.Emit("Registration")
81+
}
82+
83+
func Upvote(pkgpath string) {
84+
rawItem, ok := exhibition.items.Get(pkgpath)
85+
if !ok {
86+
panic(ErrNoSuchItem.Error())
87+
}
88+
89+
item := rawItem.(*Item)
90+
caller := std.PrevRealm().Addr().String()
91+
92+
if item.upvote.Has(caller) {
93+
panic(ErrDoubleUpvote.Error())
94+
}
95+
96+
item.upvote.Set(caller, struct{}{})
97+
}
98+
99+
func Downvote(pkgpath string) {
100+
rawItem, ok := exhibition.items.Get(pkgpath)
101+
if !ok {
102+
panic(ErrNoSuchItem.Error())
103+
}
104+
105+
item := rawItem.(*Item)
106+
caller := std.PrevRealm().Addr().String()
107+
108+
if item.downvote.Has(caller) {
109+
panic(ErrDoubleDownvote.Error())
110+
}
111+
112+
item.downvote.Set(caller, struct{}{})
113+
}
114+
115+
func Delete(pkgpath string) {
116+
if err := owner.CallerIsOwner(); err != nil {
117+
panic(err)
118+
}
119+
120+
i, ok := exhibition.items.Get(pkgpath)
121+
if !ok {
122+
panic(ErrNoSuchItem.Error())
123+
}
124+
125+
if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed {
126+
panic(ErrNoSuchItem.Error())
127+
}
128+
129+
if _, removed := exhibition.items.Remove(pkgpath); !removed {
130+
panic(ErrNoSuchItem.Error())
131+
}
132+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package hof
2+
3+
import (
4+
"std"
5+
"testing"
6+
7+
"gno.land/p/demo/testutils"
8+
"gno.land/p/demo/uassert"
9+
"gno.land/p/demo/urequire"
10+
)
11+
12+
const rlmPath = "gno.land/r/gnoland/home"
13+
14+
var (
15+
admin = owner.Owner()
16+
adminRealm = std.NewUserRealm(admin)
17+
alice = testutils.TestAddress("alice")
18+
)
19+
20+
func TestRegister(t *testing.T) {
21+
// Test user realm register
22+
aliceRealm := std.NewUserRealm(alice)
23+
std.TestSetRealm(aliceRealm)
24+
25+
Register()
26+
uassert.False(t, itemExists(t, rlmPath))
27+
28+
// Test register while paused
29+
std.TestSetRealm(adminRealm)
30+
Pause()
31+
32+
// Set legitimate caller
33+
std.TestSetRealm(std.NewCodeRealm(rlmPath))
34+
35+
Register()
36+
uassert.False(t, itemExists(t, rlmPath))
37+
38+
// Unpause
39+
std.TestSetRealm(adminRealm)
40+
Unpause()
41+
42+
// Set legitimate caller
43+
std.TestSetRealm(std.NewCodeRealm(rlmPath))
44+
Register()
45+
46+
// Find registered items
47+
uassert.True(t, itemExists(t, rlmPath))
48+
}
49+
50+
func TestUpvote(t *testing.T) {
51+
raw, _ := exhibition.items.Get(rlmPath)
52+
item := raw.(*Item)
53+
54+
rawSorted, _ := exhibition.itemsSorted.Get(item.id.String())
55+
itemSorted := rawSorted.(*Item)
56+
57+
// 0 upvotes by default
58+
urequire.Equal(t, item.upvote.Size(), 0)
59+
60+
std.TestSetRealm(adminRealm)
61+
62+
urequire.NotPanics(t, func() {
63+
Upvote(rlmPath)
64+
})
65+
66+
// Check both trees for 1 upvote
67+
uassert.Equal(t, item.upvote.Size(), 1)
68+
uassert.Equal(t, itemSorted.upvote.Size(), 1)
69+
70+
// Check double upvote
71+
uassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() {
72+
Upvote(rlmPath)
73+
})
74+
}
75+
76+
func TestDownvote(t *testing.T) {
77+
raw, _ := exhibition.items.Get(rlmPath)
78+
item := raw.(*Item)
79+
80+
rawSorted, _ := exhibition.itemsSorted.Get(item.id.String())
81+
itemSorted := rawSorted.(*Item)
82+
83+
// 0 downvotes by default
84+
urequire.Equal(t, item.downvote.Size(), 0)
85+
86+
userRealm := std.NewUserRealm(alice)
87+
std.TestSetRealm(userRealm)
88+
89+
urequire.NotPanics(t, func() {
90+
Downvote(rlmPath)
91+
})
92+
93+
// Check both trees for 1 upvote
94+
uassert.Equal(t, item.downvote.Size(), 1)
95+
uassert.Equal(t, itemSorted.downvote.Size(), 1)
96+
97+
// Check double downvote
98+
uassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() {
99+
Downvote(rlmPath)
100+
})
101+
}
102+
103+
func TestDelete(t *testing.T) {
104+
userRealm := std.NewUserRealm(admin)
105+
std.TestSetRealm(userRealm)
106+
std.TestSetOrigCaller(admin)
107+
108+
uassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() {
109+
Delete("nonexistentpkgpath")
110+
})
111+
112+
i, _ := exhibition.items.Get(rlmPath)
113+
id := i.(*Item).id
114+
115+
uassert.NotPanics(t, func() {
116+
Delete(rlmPath)
117+
})
118+
119+
uassert.False(t, exhibition.items.Has(rlmPath))
120+
uassert.False(t, exhibition.itemsSorted.Has(id.String()))
121+
}
122+
123+
func itemExists(t *testing.T, rlmPath string) bool {
124+
t.Helper()
125+
126+
i, ok1 := exhibition.items.Get(rlmPath)
127+
ok2 := false
128+
129+
if ok1 {
130+
_, ok2 = exhibition.itemsSorted.Get(i.(*Item).id.String())
131+
}
132+
133+
return ok1 && ok2
134+
}

0 commit comments

Comments
 (0)