From 1993c69c84f8adc8824934f07c8f6180789a8aad Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:12:23 -0600 Subject: [PATCH] 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)
Contributors' checklist... - [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).
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Antonio Navarro Perez --- examples/gno.land/p/demo/fqname/fqname.gno | 7 +- examples/gno.land/p/demo/ownable/ownable.gno | 6 +- .../gno.land/p/demo/pausable/pausable.gno | 10 +- .../gno.land/r/demo/hof/administration.gno | 24 ++++ examples/gno.land/r/demo/hof/errors.gno | 11 ++ examples/gno.land/r/demo/hof/gno.mod | 15 ++ examples/gno.land/r/demo/hof/hof.gno | 132 +++++++++++++++++ examples/gno.land/r/demo/hof/hof_test.gno | 134 ++++++++++++++++++ examples/gno.land/r/demo/hof/render.gno | 113 +++++++++++++++ examples/gno.land/r/gnoland/home/gno.mod | 1 + examples/gno.land/r/gnoland/home/home.gno | 14 +- .../gno.land/r/gnoland/home/home_filetest.gno | 6 +- examples/gno.land/r/leon/home/gno.mod | 1 + examples/gno.land/r/leon/home/home.gno | 3 + examples/gno.land/r/manfred/home/gno.mod | 5 +- examples/gno.land/r/manfred/home/home.gno | 6 +- examples/gno.land/r/morgan/home/gno.mod | 2 + examples/gno.land/r/morgan/home/home.gno | 4 + 18 files changed, 482 insertions(+), 12 deletions(-) create mode 100644 examples/gno.land/r/demo/hof/administration.gno create mode 100644 examples/gno.land/r/demo/hof/errors.gno create mode 100644 examples/gno.land/r/demo/hof/gno.mod create mode 100644 examples/gno.land/r/demo/hof/hof.gno create mode 100644 examples/gno.land/r/demo/hof/hof_test.gno create mode 100644 examples/gno.land/r/demo/hof/render.gno diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno index 8cccdb9e8b7..07d9e4b4621 100644 --- a/examples/gno.land/p/demo/fqname/fqname.gno +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -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. @@ -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 } diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index a77b22461a9..48a1c15fffa 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -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 @@ -58,7 +58,7 @@ func (o *Ownable) DropOwnership() error { std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), + "from", prevOwner.String(), "to", "", ) diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index eae3456ba61..e9cce63c1e3 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -1,6 +1,10 @@ package pausable -import "gno.land/p/demo/ownable" +import ( + "std" + + "gno.land/p/demo/ownable" +) type Pausable struct { *ownable.Ownable @@ -35,6 +39,8 @@ func (p *Pausable) Pause() error { } p.paused = true + std.Emit("Paused", "account", p.Owner().String()) + return nil } @@ -45,5 +51,7 @@ func (p *Pausable) Unpause() error { } p.paused = false + std.Emit("Unpaused", "account", p.Owner().String()) + return nil } diff --git a/examples/gno.land/r/demo/hof/administration.gno b/examples/gno.land/r/demo/hof/administration.gno new file mode 100644 index 00000000000..4b5b212eddf --- /dev/null +++ b/examples/gno.land/r/demo/hof/administration.gno @@ -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) + } +} diff --git a/examples/gno.land/r/demo/hof/errors.gno b/examples/gno.land/r/demo/hof/errors.gno new file mode 100644 index 00000000000..7277f65fa76 --- /dev/null +++ b/examples/gno.land/r/demo/hof/errors.gno @@ -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") +) diff --git a/examples/gno.land/r/demo/hof/gno.mod b/examples/gno.land/r/demo/hof/gno.mod new file mode 100644 index 00000000000..ac5c91295a6 --- /dev/null +++ b/examples/gno.land/r/demo/hof/gno.mod @@ -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 +) diff --git a/examples/gno.land/r/demo/hof/hof.gno b/examples/gno.land/r/demo/hof/hof.gno new file mode 100644 index 00000000000..2722c019497 --- /dev/null +++ b/examples/gno.land/r/demo/hof/hof.gno @@ -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()) + } +} diff --git a/examples/gno.land/r/demo/hof/hof_test.gno b/examples/gno.land/r/demo/hof/hof_test.gno new file mode 100644 index 00000000000..72e8d2159be --- /dev/null +++ b/examples/gno.land/r/demo/hof/hof_test.gno @@ -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 +} diff --git a/examples/gno.land/r/demo/hof/render.gno b/examples/gno.land/r/demo/hof/render.gno new file mode 100644 index 00000000000..6b06ef04051 --- /dev/null +++ b/examples/gno.land/r/demo/hof/render.gno @@ -0,0 +1,113 @@ +package hof + +import ( + "strings" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/fqname" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +const ( + pageSize = 5 +) + +func Render(path string) string { + out := "# Hall of Fame\n\n" + + dashboardEnabled := path == "dashboard" + + if dashboardEnabled { + out += renderDashboard() + } + + out += exhibition.Render(path, dashboardEnabled) + + return out +} + +func (e Exhibition) Render(path string, dashboard bool) string { + out := ufmt.Sprintf("%s\n\n", e.description) + + if e.items.Size() == 0 { + out += "No items in this exhibition currently.\n\n" + return out + } + + out += "
\n\n" + + page := pager.NewPager(e.itemsSorted, pageSize).MustGetPageByPath(path) + + for i := len(page.Items) - 1; i >= 0; i-- { + item := page.Items[i] + + out += "
\n\n" + id, _ := seqid.FromString(item.Key) + out += ufmt.Sprintf("### Submission #%d\n\n", int(id)) + out += item.Value.(*Item).Render(dashboard) + out += "
" + } + + out += "
\n\n" + + out += page.Selector() + + return out +} + +func (i Item) Render(dashboard bool) string { + out := ufmt.Sprintf("\n```\n%s\n```\n\n", i.pkgpath) + out += ufmt.Sprintf("by %s\n\n", strings.Split(i.pkgpath, "/")[2]) + out += ufmt.Sprintf("[View realm](%s)\n\n", strings.TrimPrefix(i.pkgpath, "gno.land")) // gno.land/r/leon/home > /r/leon/home + out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) + + out += ufmt.Sprintf("#### [%dπŸ‘](%s) - [%dπŸ‘Ž](%s)\n\n", + i.upvote.Size(), txlink.URL("Upvote", "pkgpath", i.pkgpath), + i.downvote.Size(), txlink.URL("Downvote", "pkgpath", i.pkgpath), + ) + + if dashboard { + out += ufmt.Sprintf("[Delete](%s)", txlink.URL("Delete", "pkgpath", i.pkgpath)) + } + + return out +} + +func renderDashboard() string { + out := "---\n\n" + out += "## Dashboard\n\n" + out += ufmt.Sprintf("Total submissions: %d\n\n", exhibition.items.Size()) + + out += ufmt.Sprintf("Exhibition admin: %s\n\n", owner.Owner().String()) + + if !exhibition.IsPaused() { + out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.URL("Pause")) + } else { + out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.URL("Unpause")) + } + + out += "---\n\n" + + return out +} + +func RenderExhibWidget(itemsToRender int) string { + if itemsToRender < 1 { + return "" + } + + out := "" + i := 0 + exhibition.items.Iterate("", "", func(key string, value interface{}) bool { + item := value.(*Item) + + out += ufmt.Sprintf("- %s\n", fqname.RenderLink(item.pkgpath, "")) + + i++ + return i >= itemsToRender + }) + + return out +} diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index c208ad421c9..ff52ef4c8b1 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -4,6 +4,7 @@ require ( gno.land/p/demo/ownable v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/ui v0.0.0-latest + gno.land/r/demo/hof v0.0.0-latest gno.land/r/gnoland/blog v0.0.0-latest gno.land/r/gnoland/events v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index c6b3929a16c..ce976923ef5 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" "gno.land/p/demo/ui" + "gno.land/r/demo/hof" blog "gno.land/r/gnoland/blog" events "gno.land/r/gnoland/events" ) @@ -37,7 +38,7 @@ func Render(_ string) string { ui.Columns{3, []ui.Element{ lastBlogposts(4), upcomingEvents(), - lastContributions(4), + latestHOFItems(5), }}, ) @@ -90,6 +91,15 @@ func upcomingEvents() ui.Element { } } +func latestHOFItems(num int) ui.Element { + submissions := hof.RenderExhibWidget(num) + + return ui.Element{ + ui.H3("[Hall of Fame](/r/demo/hof)"), + ui.Text(submissions), + } +} + func introSection() ui.Element { return ui.Element{ ui.H3("We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts."), @@ -270,7 +280,7 @@ func discoverLinks() ui.Element { - [Gnoscan](https://gnoscan.io) - [Portal Loop](https://docs.gno.land/concepts/portal-loop) - [Testnet 4](https://test4.gno.land/) -- Testnet Faucet Hub (soon) +- [Faucet Hub](https://faucet.gno.land) `), diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index b22c22567b3..c587af9b817 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -57,7 +57,7 @@ func main() { // - [Gnoscan](https://gnoscan.io) // - [Portal Loop](https://docs.gno.land/concepts/portal-loop) // - [Testnet 4](https://test4.gno.land/) -// - Testnet Faucet Hub (soon) +// - [Faucet Hub](https://faucet.gno.land) // // // @@ -78,9 +78,9 @@ func main() { // //
// -// ### Latest Contributions +// ### [Hall of Fame](/r/demo/hof) +// // -// [View latest contributions](https://github.com/gnolang/gno/pulls) //
// // diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index 48cf64a9d0a..4649cf4abe6 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -4,5 +4,6 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/r/demo/art/gnoface v0.0.0-latest gno.land/r/demo/art/millipede v0.0.0-latest + gno.land/r/demo/hof v0.0.0-latest gno.land/r/leon/config v0.0.0-latest ) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index ba688792a4c..aea8b43e9cd 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -8,6 +8,7 @@ import ( "gno.land/r/demo/art/gnoface" "gno.land/r/demo/art/millipede" + "gno.land/r/demo/hof" "gno.land/r/leon/config" ) @@ -31,6 +32,8 @@ My contributions to gno.land can mainly be found TODO import r/gh `, } + + hof.Register() } func UpdatePFP(url, caption string) { diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod index 6e7aac70cc7..9885cac19c2 100644 --- a/examples/gno.land/r/manfred/home/gno.mod +++ b/examples/gno.land/r/manfred/home/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/manfred/home -require gno.land/r/manfred/config v0.0.0-latest +require ( + gno.land/r/demo/hof v0.0.0-latest + gno.land/r/manfred/config v0.0.0-latest +) diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno index 720796a2201..4766f54e51f 100644 --- a/examples/gno.land/r/manfred/home/home.gno +++ b/examples/gno.land/r/manfred/home/home.gno @@ -1,6 +1,9 @@ package home -import "gno.land/r/manfred/config" +import ( + "gno.land/r/demo/hof" + "gno.land/r/manfred/config" +) var ( todos []string @@ -12,6 +15,7 @@ func init() { todos = append(todos, "fill this todo list...") status = "Online" // Initial status set to "Online" memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" + hof.Register() } func Render(path string) string { diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod index 573a7e139e7..35e2fbb2119 100644 --- a/examples/gno.land/r/morgan/home/gno.mod +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -1 +1,3 @@ module gno.land/r/morgan/home + +require gno.land/r/demo/hof v0.0.0-latest diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno index 33d7e0b2df7..571f14ed5ec 100644 --- a/examples/gno.land/r/morgan/home/home.gno +++ b/examples/gno.land/r/morgan/home/home.gno @@ -1,10 +1,14 @@ package home +import "gno.land/r/demo/hof" + const staticHome = `# morgan's (gn)home - [πŸ“ sign my guestbook](/r/morgan/guestbook) ` +func init() { hof.Register() } + func Render(path string) string { return staticHome }