forked from gnolang/gno
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): hall of fame (gnolang#2842)
## Description Depends on gnolang#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
Showing
18 changed files
with
482 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.