diff --git a/examples/gno.land/p/demo/ownable/errors.gno b/examples/gno.land/p/demo/ownable/errors.gno
index ffbf6ab3f6f..89776a6cf12 100644
--- a/examples/gno.land/p/demo/ownable/errors.gno
+++ b/examples/gno.land/p/demo/ownable/errors.gno
@@ -3,6 +3,6 @@ package ownable
import "errors"
var (
- ErrUnauthorized = errors.New("unauthorized; caller is not owner")
- ErrInvalidAddress = errors.New("new owner address is invalid")
+ ErrUnauthorized = errors.New("ownable: caller is not owner")
+ ErrInvalidAddress = errors.New("ownable: new owner address is invalid")
)
diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno
new file mode 100644
index 00000000000..f9f0ea15dd9
--- /dev/null
+++ b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno
@@ -0,0 +1,90 @@
+// Package authorizable is an extension of p/demo/ownable;
+// It allows the user to instantiate an Authorizable struct, which extends
+// p/demo/ownable with a list of users that are authorized for something.
+// By using authorizable, you have a superuser (ownable), as well as another
+// authorization level, which can be used for adding moderators or similar to your realm.
+package authorizable
+
+import (
+ "std"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ownable"
+ "gno.land/p/demo/ufmt"
+)
+
+type Authorizable struct {
+ *ownable.Ownable // owner in ownable is superuser
+ authorized *avl.Tree // std.Addr > struct{}{}
+}
+
+func NewAuthorizable() *Authorizable {
+ a := &Authorizable{
+ ownable.New(),
+ avl.NewTree(),
+ }
+
+ // Add owner to auth list
+ a.authorized.Set(a.Owner().String(), struct{}{})
+ return a
+}
+
+func NewAuthorizableWithAddress(addr std.Address) *Authorizable {
+ a := &Authorizable{
+ ownable.NewWithAddress(addr),
+ avl.NewTree(),
+ }
+
+ // Add owner to auth list
+ a.authorized.Set(a.Owner().String(), struct{}{})
+ return a
+}
+
+func (a *Authorizable) AddToAuthList(addr std.Address) error {
+ if err := a.CallerIsOwner(); err != nil {
+ return ErrNotSuperuser
+ }
+
+ if _, exists := a.authorized.Get(addr.String()); exists {
+ return ErrAlreadyInList
+ }
+
+ a.authorized.Set(addr.String(), struct{}{})
+
+ return nil
+}
+
+func (a *Authorizable) DeleteFromAuthList(addr std.Address) error {
+ if err := a.CallerIsOwner(); err != nil {
+ return ErrNotSuperuser
+ }
+
+ if !a.authorized.Has(addr.String()) {
+ return ErrNotInAuthList
+ }
+
+ if _, removed := a.authorized.Remove(addr.String()); !removed {
+ str := ufmt.Sprintf("authorizable: could not remove %s from auth list", addr.String())
+ panic(str)
+ }
+
+ return nil
+}
+
+func (a Authorizable) CallerOnAuthList() error {
+ caller := std.PrevRealm().Addr()
+
+ if !a.authorized.Has(caller.String()) {
+ return ErrNotInAuthList
+ }
+
+ return nil
+}
+
+func (a Authorizable) AssertOnAuthList() {
+ caller := std.PrevRealm().Addr()
+
+ if !a.authorized.Has(caller.String()) {
+ panic(ErrNotInAuthList)
+ }
+}
diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable_test.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable_test.gno
new file mode 100644
index 00000000000..10a5e411bdb
--- /dev/null
+++ b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable_test.gno
@@ -0,0 +1,116 @@
+package authorizable
+
+import (
+ "std"
+ "testing"
+
+ "gno.land/p/demo/testutils"
+ "gno.land/p/demo/uassert"
+)
+
+var (
+ alice = testutils.TestAddress("alice")
+ bob = testutils.TestAddress("bob")
+ charlie = testutils.TestAddress("charlie")
+)
+
+func TestNewAuthorizable(t *testing.T) {
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice) // TODO(bug, issue #2371): should not be needed
+
+ a := NewAuthorizable()
+ got := a.Owner()
+
+ if alice != got {
+ t.Fatalf("Expected %s, got: %s", alice, got)
+ }
+}
+
+func TestNewAuthorizableWithAddress(t *testing.T) {
+ a := NewAuthorizableWithAddress(alice)
+
+ got := a.Owner()
+
+ if alice != got {
+ t.Fatalf("Expected %s, got: %s", alice, got)
+ }
+}
+
+func TestCallerOnAuthList(t *testing.T) {
+ a := NewAuthorizableWithAddress(alice)
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice)
+
+ if err := a.CallerOnAuthList(); err == ErrNotInAuthList {
+ t.Fatalf("expected alice to be on the list")
+ }
+}
+
+func TestNotCallerOnAuthList(t *testing.T) {
+ a := NewAuthorizableWithAddress(alice)
+ std.TestSetRealm(std.NewUserRealm(bob))
+ std.TestSetOrigCaller(bob)
+
+ if err := a.CallerOnAuthList(); err == nil {
+ t.Fatalf("expected bob to not be on the list")
+ }
+}
+
+func TestAddToAuthList(t *testing.T) {
+ a := NewAuthorizableWithAddress(alice)
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice)
+
+ if err := a.AddToAuthList(bob); err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ std.TestSetRealm(std.NewUserRealm(bob))
+ std.TestSetOrigCaller(bob)
+
+ if err := a.AddToAuthList(bob); err == nil {
+ t.Fatalf("Expected AddToAuth to error while bob called it, but it didn't")
+ }
+}
+
+func TestDeleteFromList(t *testing.T) {
+ a := NewAuthorizableWithAddress(alice)
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice)
+
+ if err := a.AddToAuthList(bob); err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ if err := a.AddToAuthList(charlie); err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ std.TestSetRealm(std.NewUserRealm(bob))
+ std.TestSetOrigCaller(bob)
+
+ // Try an unauthorized deletion
+ if err := a.DeleteFromAuthList(alice); err == nil {
+ t.Fatalf("Expected DelFromAuth to error with %v", err)
+ }
+
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice)
+
+ if err := a.DeleteFromAuthList(charlie); err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+}
+
+func TestAssertOnList(t *testing.T) {
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice)
+ a := NewAuthorizableWithAddress(alice)
+
+ std.TestSetRealm(std.NewUserRealm(bob))
+ std.TestSetOrigCaller(bob)
+
+ uassert.PanicsWithMessage(t, ErrNotInAuthList.Error(), func() {
+ a.AssertOnAuthList()
+ })
+}
diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/errors.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/errors.gno
new file mode 100644
index 00000000000..4ba5983bccb
--- /dev/null
+++ b/examples/gno.land/p/demo/ownable/exts/authorizable/errors.gno
@@ -0,0 +1,9 @@
+package authorizable
+
+import "errors"
+
+var (
+ ErrNotInAuthList = errors.New("authorizable: caller is not in authorized list")
+ ErrNotSuperuser = errors.New("authorizable: caller is not superuser")
+ ErrAlreadyInList = errors.New("authorizable: address is already in authorized list")
+)
diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod
new file mode 100644
index 00000000000..f36823f3f71
--- /dev/null
+++ b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod
@@ -0,0 +1,9 @@
+module gno.land/p/demo/ownable/exts/authorizable
+
+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
+ gno.land/p/demo/uassert v0.0.0-latest
+ gno.land/p/demo/ufmt v0.0.0-latest
+)
diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno
index 75ebcde0a28..a77b22461a9 100644
--- a/examples/gno.land/p/demo/ownable/ownable.gno
+++ b/examples/gno.land/p/demo/ownable/ownable.gno
@@ -1,8 +1,6 @@
package ownable
-import (
- "std"
-)
+import "std"
const OwnershipTransferEvent = "OwnershipTransfer"
@@ -19,7 +17,9 @@ func New() *Ownable {
}
func NewWithAddress(addr std.Address) *Ownable {
- return &Ownable{owner: addr}
+ return &Ownable{
+ owner: addr,
+ }
}
// TransferOwnership transfers ownership of the Ownable struct to a new address
@@ -40,6 +40,7 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error {
"from", string(prevOwner),
"to", string(newOwner),
)
+
return nil
}
@@ -64,6 +65,7 @@ func (o *Ownable) DropOwnership() error {
return nil
}
+// Owner returns the owner address from Ownable
func (o Ownable) Owner() std.Address {
return o.owner
}
@@ -73,9 +75,11 @@ func (o Ownable) CallerIsOwner() error {
if std.PrevRealm().Addr() == o.owner {
return nil
}
+
return ErrUnauthorized
}
+// AssertCallerIsOwner panics if the caller is not the owner
func (o Ownable) AssertCallerIsOwner() {
if std.PrevRealm().Addr() != o.owner {
panic(ErrUnauthorized)
diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno
index 6217948d587..a9d97154f45 100644
--- a/examples/gno.land/p/demo/ownable/ownable_test.gno
+++ b/examples/gno.land/p/demo/ownable/ownable_test.gno
@@ -9,52 +9,60 @@ import (
)
var (
- firstCaller = testutils.TestAddress("first")
- secondCaller = testutils.TestAddress("second")
+ alice = testutils.TestAddress("alice")
+ bob = testutils.TestAddress("bob")
)
func TestNew(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
- std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice) // TODO(bug): should not be needed
o := New()
got := o.Owner()
- uassert.Equal(t, firstCaller, got)
+ if alice != got {
+ t.Fatalf("Expected %s, got: %s", alice, got)
+ }
}
func TestNewWithAddress(t *testing.T) {
- o := NewWithAddress(firstCaller)
+ o := NewWithAddress(alice)
got := o.Owner()
- uassert.Equal(t, firstCaller, got)
+ if alice != got {
+ t.Fatalf("Expected %s, got: %s", alice, got)
+ }
}
func TestOwner(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
+ std.TestSetRealm(std.NewUserRealm(alice))
o := New()
- expected := firstCaller
+ expected := alice
got := o.Owner()
uassert.Equal(t, expected, got)
}
func TestTransferOwnership(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
+ std.TestSetRealm(std.NewUserRealm(alice))
o := New()
- err := o.TransferOwnership(secondCaller)
- uassert.NoError(t, err, "TransferOwnership failed")
+ err := o.TransferOwnership(bob)
+ if err != nil {
+ t.Fatalf("TransferOwnership failed, %v", err)
+ }
got := o.Owner()
- uassert.Equal(t, secondCaller, got)
+ if bob != got {
+ t.Fatalf("Expected: %s, got: %s", bob, got)
+ }
}
func TestCallerIsOwner(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
+ std.TestSetRealm(std.NewUserRealm(alice))
o := New()
- unauthorizedCaller := secondCaller
+ unauthorizedCaller := bob
std.TestSetRealm(std.NewUserRealm(unauthorizedCaller))
std.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed
@@ -64,7 +72,7 @@ func TestCallerIsOwner(t *testing.T) {
}
func TestDropOwnership(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
+ std.TestSetRealm(std.NewUserRealm(alice))
o := New()
@@ -78,23 +86,25 @@ func TestDropOwnership(t *testing.T) {
// Errors
func TestErrUnauthorized(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
- std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed
+ std.TestSetRealm(std.NewUserRealm(alice))
+ std.TestSetOrigCaller(alice) // TODO(bug): should not be needed
o := New()
- std.TestSetRealm(std.NewUserRealm(secondCaller))
- std.TestSetOrigCaller(secondCaller) // TODO(bug): should not be needed
+ std.TestSetRealm(std.NewUserRealm(bob))
+ std.TestSetOrigCaller(bob) // TODO(bug): should not be needed
- err := o.TransferOwnership(firstCaller)
- uassert.ErrorContains(t, err, ErrUnauthorized.Error())
+ err := o.TransferOwnership(alice)
+ if err != ErrUnauthorized {
+ t.Fatalf("Should've been ErrUnauthorized, was %v", err)
+ }
err = o.DropOwnership()
uassert.ErrorContains(t, err, ErrUnauthorized.Error())
}
func TestErrInvalidAddress(t *testing.T) {
- std.TestSetRealm(std.NewUserRealm(firstCaller))
+ std.TestSetRealm(std.NewUserRealm(alice))
o := New()
diff --git a/examples/gno.land/r/gnoland/events/administration.gno b/examples/gno.land/r/gnoland/events/administration.gno
new file mode 100644
index 00000000000..02914adee69
--- /dev/null
+++ b/examples/gno.land/r/gnoland/events/administration.gno
@@ -0,0 +1,26 @@
+package events
+
+import (
+ "std"
+
+ "gno.land/p/demo/ownable/exts/authorizable"
+)
+
+var (
+ su = std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn
+ auth = authorizable.NewAuthorizableWithAddress(su)
+)
+
+// GetOwner gets the owner of the events realm
+func GetOwner() std.Address {
+ return auth.Owner()
+}
+
+// AddModerator adds a moderator to the events realm
+func AddModerator(mod std.Address) {
+ auth.AssertCallerIsOwner()
+
+ if err := auth.AddToAuthList(mod); err != nil {
+ panic(err)
+ }
+}
diff --git a/examples/gno.land/r/gnoland/events/errors.gno b/examples/gno.land/r/gnoland/events/errors.gno
new file mode 100644
index 00000000000..788115015a4
--- /dev/null
+++ b/examples/gno.land/r/gnoland/events/errors.gno
@@ -0,0 +1,18 @@
+package events
+
+import (
+ "errors"
+ "strconv"
+)
+
+var (
+ ErrEmptyName = errors.New("event name cannot be empty")
+ ErrNoSuchID = errors.New("event with specified ID does not exist")
+ ErrWidgetMinAmt = errors.New("you need to request at least 1 event to render")
+ ErrWidgetMaxAmt = errors.New("maximum number of events in widget is" + strconv.Itoa(MaxWidgetSize))
+ ErrDescriptionTooLong = errors.New("event description is too long")
+ ErrInvalidStartTime = errors.New("invalid start time format")
+ ErrInvalidEndTime = errors.New("invalid end time format")
+ ErrEndBeforeStart = errors.New("end time cannot be before start time")
+ ErrStartEndTimezonemMismatch = errors.New("start and end timezones are not the same")
+)
diff --git a/examples/gno.land/r/gnoland/events/events.gno b/examples/gno.land/r/gnoland/events/events.gno
index 9c2708a112e..0984edf75a9 100644
--- a/examples/gno.land/r/gnoland/events/events.gno
+++ b/examples/gno.land/r/gnoland/events/events.gno
@@ -1,240 +1,199 @@
+// Package events allows you to upload data about specific IRL/online events
+// It includes dynamic support for updating rendering events based on their
+// status, ie if they are upcoming, in progress, or in the past.
package events
import (
- "gno.land/p/demo/ui"
-)
-
-// XXX: p/demo/ui API is crappy, we need to make it more idiomatic
-// XXX: use an updatable block system to update content from a DAO
-// XXX: var blocks avl.Tree
-
-func Render(_ string) string {
- dom := ui.DOM{Prefix: "r/gnoland/events:"}
- dom.Title = "Gno.land Core Team Attends Industry Events & Meetups"
- dom.Classes = []string{"gno-tmpl-section"}
+ "sort"
+ "std"
+ "strings"
+ "time"
- // body
- dom.Body.Append(introSection()...)
- dom.Body.Append(ui.HR{})
- dom.Body.Append(upcomingEvents()...)
- dom.Body.Append(ui.HR{})
- dom.Body.Append(pastEvents()...)
-
- return dom.String()
-}
+ "gno.land/p/demo/seqid"
+ "gno.land/p/demo/ufmt"
+)
-func introSection() ui.Element {
- return ui.Element{
- ui.Paragraph("If you’re interested in building web3 with us, catch up with gno.land in person at one of our industry events. We’re looking to connect with developers and like-minded thinkers who can contribute to the growth of our platform."),
+type (
+ Event struct {
+ id string
+ name string // name of event
+ description string // short description of event
+ link string // link to auth corresponding web2 page, ie eventbrite/luma or conference page
+ location string // location of the event
+ startTime time.Time // given in RFC3339
+ endTime time.Time // end time of the event, given in RFC3339
}
-}
-
-func upcomingEvents() ui.Element {
- return ui.Element{
- ui.H2("Upcoming Events"),
- ui.Text(`
-
-
-### GopherCon EU
-- Come Meet Us at our Booth
-- Berlin, June 17 - 20, 2024
-
-[Learn More](https://gophercon.eu/)
-
-
-
-### GopherCon US
-- Come Meet Us at our Booth
-- Chicago, July 7 - 10, 2024
-
-[Learn More](https://www.gophercon.com/)
-
-
-
-
-
-### Nebular Summit
-- Join our workshop
-- Brussels, July 12 - 13, 2024
+ eventsSlice []*Event
+)
-[Learn More](https://nebular.builders/)
-
+var (
+ events = make(eventsSlice, 0) // sorted
+ idCounter seqid.ID
+)
-
+const (
+ maxDescLength = 100
+ EventAdded = "EventAdded"
+ EventDeleted = "EventDeleted"
+ EventEdited = "EventEdited"
+)
-
-
+// AddEvent adds auth new event
+// Start time & end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00
+func AddEvent(name, description, link, location, startTime, endTime string) (string, error) {
+ auth.AssertOnAuthList()
-
-
`),
+ if strings.TrimSpace(name) == "" {
+ return "", ErrEmptyName
}
-}
-
-func pastEvents() ui.Element {
- return ui.Element{
- ui.H2("Past Events"),
- ui.Text(`
-
-
-
-### Gno @ Golang Serbia
-
-- **Join the meetup**
-- Belgrade, May 23, 2024
-
-[Learn more](https://gno.land/r/gnoland/blog:p/gnomes-in-serbia)
-
-
-
-
-
-### Intro to Gno Tokyo
-
-- **Join the meetup**
-- Tokyo, April 11, 2024
-
-[Learn more](https://gno.land/r/gnoland/blog:p/gno-tokyo)
-
-
-
-
-
-### Go to Gno Seoul
-
-- **Join the workshop**
-- Seoul, March 23, 2024
-
-[Learn more](https://medium.com/onbloc/go-to-gno-recap-intro-to-the-gno-stack-with-memeland-284a43d7f620)
-
-
-
-
-
-### GopherCon US
-
-- **Come Meet Us at our Booth**
-- San Diego, September 26 - 29, 2023
-
-[Learn more](https://www.gophercon.com/)
-
-
-
-
-### GopherCon EU
-
-- **Come Meet Us at our Booth**
-- Berlin, July 26 - 29, 2023
-
-[Learn more](https://gophercon.eu/)
-
-
-
-
-
-### Nebular Summit Gno.land for Developers
-
-- Paris, July 24 - 25, 2023
-- Manfred Touron
-
-[Learn more](https://www.nebular.builders/)
-
-
-
-
-
-### EthCC
-
-- **Come Meet Us at our Booth**
-- Paris, July 17 - 20, 2023
-- Manfred Touron
-
-[Learn more](https://www.ethcc.io/)
-
-
-
-
-
-### Eth Seoul
-
-- **The Evolution of Smart Contracts: A Journey into Gno.land**
-- Seoul, June 3, 2023
-- Manfred Touron
-
-[Learn more](https://2023.ethseoul.org/)
+ if len(description) > maxDescLength {
+ return "", ufmt.Errorf("%s: provided length is %d, maximum is %d", ErrDescriptionTooLong, len(description), maxDescLength)
+ }
-
-
+ // Parse times
+ st, et, err := parseTimes(startTime, endTime)
+ if err != nil {
+ return "", err
+ }
-### BUIDL Asia
+ id := idCounter.Next().String()
+ e := &Event{
+ id: id,
+ name: name,
+ description: description,
+ link: link,
+ location: location,
+ startTime: st,
+ endTime: et,
+ }
-- **Proof of Contribution in Gno.land**
-- Seoul, June 6, 2023
-- Manfred Touron
+ events = append(events, e)
+ sort.Sort(events)
-[Learn more](https://www.buidl.asia/)
+ std.Emit(EventAdded,
+ "id",
+ e.id,
+ )
-
-
+ return id, nil
+}
-### Game Developer Conference
+// DeleteEvent deletes an event with auth given ID
+func DeleteEvent(id string) {
+ auth.AssertOnAuthList()
-- **Side Event: Web3 Gaming Apps Powered by Gno**
-- San Francisco, Mach 23, 2023
-- Jae Kwon
+ e, idx, err := GetEventByID(id)
+ if err != nil {
+ panic(err)
+ }
-[Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c)
+ events = append(events[:idx], events[idx+1:]...)
-
-
+ std.Emit(EventDeleted,
+ "id",
+ e.id,
+ )
+}
-### EthDenver
+// EditEvent edits an event with auth given ID
+// It only updates values corresponding to non-empty arguments sent with the call
+// Note: if you need to update the start time or end time, you need to provide both every time
+func EditEvent(id string, name, description, link, location, startTime, endTime string) {
+ auth.AssertOnAuthList()
-- **Side Event: Discover Gno.land**
-- Denver, Feb 24 - Mar 5, 2023
-- Jae Kwon
+ e, _, err := GetEventByID(id)
+ if err != nil {
+ panic(err)
+ }
-[Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c)
+ // Set only valid values
+ if strings.TrimSpace(name) != "" {
+ e.name = name
+ }
-
-
+ if strings.TrimSpace(description) != "" {
+ e.description = description
+ }
-### Istanbul Blockchain Week
+ if strings.TrimSpace(link) != "" {
+ e.link = link
+ }
-- Istanbul, Nov 14 - 17, 2022
-- Manfred Touron
+ if strings.TrimSpace(location) != "" {
+ e.location = location
+ }
-[Watch the talk](https://www.youtube.com/watch?v=JX0gdWT0Cg4)
+ if strings.TrimSpace(startTime) != "" || strings.TrimSpace(endTime) != "" {
+ st, et, err := parseTimes(startTime, endTime)
+ if err != nil {
+ panic(err) // need to also revert other state changes
+ }
-
-
+ oldStartTime := e.startTime
+ e.startTime = st
+ e.endTime = et
-### Web Summit Buckle Up and Build with Cosmos
+ // If sort order was disrupted, sort again
+ if oldStartTime != e.startTime {
+ sort.Sort(events)
+ }
+ }
-- Lisbon, Nov 1 - 4, 2022
-- Manfred Touron
+ std.Emit(EventEdited,
+ "id",
+ e.id,
+ )
+}
-
-
+func GetEventByID(id string) (*Event, int, error) {
+ for i, event := range events {
+ if event.id == id {
+ return event, i, nil
+ }
+ }
-### Cosmoverse
+ return nil, -1, ErrNoSuchID
+}
-- Medallin, Sept 26 - 28, 2022
-- Manfred Touron
+// Len returns the length of the slice
+func (m eventsSlice) Len() int {
+ return len(m)
+}
-[Watch the talk](https://www.youtube.com/watch?v=6s1zG7hgxMk)
+// Less compares the startTime fields of two elements
+// In this case, events will be sorted by largest startTime first (upcoming > past)
+func (m eventsSlice) Less(i, j int) bool {
+ return m[i].startTime.After(m[j].startTime)
+}
-
-
+// Swap swaps two elements in the slice
+func (m eventsSlice) Swap(i, j int) {
+ m[i], m[j] = m[j], m[i]
+}
-### Berlin Blockchain Week Buckle Up and Build with Cosmos
+// parseTimes parses the start and end time for an event and checks for possible errors
+func parseTimes(startTime, endTime string) (time.Time, time.Time, error) {
+ st, err := time.Parse(time.RFC3339, startTime)
+ if err != nil {
+ return time.Time{}, time.Time{}, ufmt.Errorf("%s: %s", ErrInvalidStartTime, err.Error())
+ }
-- Berlin, Sept 11 - 18, 2022
+ et, err := time.Parse(time.RFC3339, endTime)
+ if err != nil {
+ return time.Time{}, time.Time{}, ufmt.Errorf("%s: %s", ErrInvalidEndTime, err.Error())
+ }
-[Watch the talk](https://www.youtube.com/watch?v=hCLErPgnavI)
+ if et.Before(st) {
+ return time.Time{}, time.Time{}, ErrEndBeforeStart
+ }
-
-
`),
+ _, stOffset := st.Zone()
+ _, etOffset := et.Zone()
+ if stOffset != etOffset {
+ return time.Time{}, time.Time{}, ErrStartEndTimezonemMismatch
}
+
+ return st, et, nil
}
diff --git a/examples/gno.land/r/gnoland/events/events_filetest.gno b/examples/gno.land/r/gnoland/events/events_filetest.gno
deleted file mode 100644
index 46ee273414d..00000000000
--- a/examples/gno.land/r/gnoland/events/events_filetest.gno
+++ /dev/null
@@ -1,226 +0,0 @@
-package main
-
-import "gno.land/r/gnoland/events"
-
-func main() {
- println(events.Render(""))
-}
-
-// Output:
-//
-//
-// # Gno.land Core Team Attends Industry Events & Meetups
-//
-//
-// If you’re interested in building web3 with us, catch up with gno.land in person at one of our industry events. We’re looking to connect with developers and like-minded thinkers who can contribute to the growth of our platform.
-//
-//
-// ---
-//
-// ## Upcoming Events
-//
-//
-//
-//
-// ### GopherCon EU
-// - Come Meet Us at our Booth
-// - Berlin, June 17 - 20, 2024
-//
-// [Learn More](https://gophercon.eu/)
-//
-//
-//
-//
-// ### GopherCon US
-// - Come Meet Us at our Booth
-// - Chicago, July 7 - 10, 2024
-//
-// [Learn More](https://www.gophercon.com/)
-//
-//
-//
-//
-//
-// ### Nebular Summit
-// - Join our workshop
-// - Brussels, July 12 - 13, 2024
-//
-// [Learn More](https://nebular.builders/)
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// ---
-//
-// ## Past Events
-//
-//
-//
-//
-//
-// ### Gno @ Golang Serbia
-//
-// - **Join the meetup**
-// - Belgrade, May 23, 2024
-//
-// [Learn more](https://gno.land/r/gnoland/blog:p/gnomes-in-serbia)
-//
-//
-//
-//
-//
-// ### Intro to Gno Tokyo
-//
-// - **Join the meetup**
-// - Tokyo, April 11, 2024
-//
-// [Learn more](https://gno.land/r/gnoland/blog:p/gno-tokyo)
-//
-//
-//
-//
-//
-// ### Go to Gno Seoul
-//
-// - **Join the workshop**
-// - Seoul, March 23, 2024
-//
-// [Learn more](https://medium.com/onbloc/go-to-gno-recap-intro-to-the-gno-stack-with-memeland-284a43d7f620)
-//
-//
-//
-//
-//
-// ### GopherCon US
-//
-// - **Come Meet Us at our Booth**
-// - San Diego, September 26 - 29, 2023
-//
-// [Learn more](https://www.gophercon.com/)
-//
-//
-//
-//
-//
-// ### GopherCon EU
-//
-// - **Come Meet Us at our Booth**
-// - Berlin, July 26 - 29, 2023
-//
-// [Learn more](https://gophercon.eu/)
-//
-//
-//
-//
-//
-// ### Nebular Summit Gno.land for Developers
-//
-// - Paris, July 24 - 25, 2023
-// - Manfred Touron
-//
-// [Learn more](https://www.nebular.builders/)
-//
-//
-//
-//
-//
-// ### EthCC
-//
-// - **Come Meet Us at our Booth**
-// - Paris, July 17 - 20, 2023
-// - Manfred Touron
-//
-// [Learn more](https://www.ethcc.io/)
-//
-//
-//
-//
-//
-// ### Eth Seoul
-//
-// - **The Evolution of Smart Contracts: A Journey into Gno.land**
-// - Seoul, June 3, 2023
-// - Manfred Touron
-//
-// [Learn more](https://2023.ethseoul.org/)
-//
-//
-//
-//
-// ### BUIDL Asia
-//
-// - **Proof of Contribution in Gno.land**
-// - Seoul, June 6, 2023
-// - Manfred Touron
-//
-// [Learn more](https://www.buidl.asia/)
-//
-//
-//
-//
-// ### Game Developer Conference
-//
-// - **Side Event: Web3 Gaming Apps Powered by Gno**
-// - San Francisco, Mach 23, 2023
-// - Jae Kwon
-//
-// [Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c)
-//
-//
-//
-//
-// ### EthDenver
-//
-// - **Side Event: Discover Gno.land**
-// - Denver, Feb 24 - Mar 5, 2023
-// - Jae Kwon
-//
-// [Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c)
-//
-//
-//
-//
-// ### Istanbul Blockchain Week
-//
-// - Istanbul, Nov 14 - 17, 2022
-// - Manfred Touron
-//
-// [Watch the talk](https://www.youtube.com/watch?v=JX0gdWT0Cg4)
-//
-//
-//
-//
-// ### Web Summit Buckle Up and Build with Cosmos
-//
-// - Lisbon, Nov 1 - 4, 2022
-// - Manfred Touron
-//
-//
-//
-//
-// ### Cosmoverse
-//
-// - Medallin, Sept 26 - 28, 2022
-// - Manfred Touron
-//
-// [Watch the talk](https://www.youtube.com/watch?v=6s1zG7hgxMk)
-//
-//
-//
-//
-// ### Berlin Blockchain Week Buckle Up and Build with Cosmos
-//
-// - Berlin, Sept 11 - 18, 2022
-//
-// [Watch the talk](https://www.youtube.com/watch?v=hCLErPgnavI)
-//
-//
-//
-//
-//
diff --git a/examples/gno.land/r/gnoland/events/events_test.gno b/examples/gno.land/r/gnoland/events/events_test.gno
new file mode 100644
index 00000000000..1e5625e0b2c
--- /dev/null
+++ b/examples/gno.land/r/gnoland/events/events_test.gno
@@ -0,0 +1,161 @@
+package events
+
+import (
+ "std"
+ "strings"
+ "testing"
+ "time"
+
+ "gno.land/p/demo/uassert"
+)
+
+var (
+ suRealm = std.NewUserRealm(su)
+
+ now = "2009-02-13T23:31:30Z" // time.Now() is hardcoded to this value in the gno test machine currently
+ parsedTimeNow, _ = time.Parse(time.RFC3339, now)
+)
+
+func TestAddEvent(t *testing.T) {
+ std.TestSetOrigCaller(su)
+ std.TestSetRealm(suRealm)
+
+ e1Start := parsedTimeNow.Add(time.Hour * 24 * 5)
+ e1End := e1Start.Add(time.Hour * 4)
+
+ AddEvent("Event 1", "this event is upcoming", "gno.land", "gnome land", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))
+
+ got := renderHome(false)
+
+ if !strings.Contains(got, "Event 1") {
+ t.Fatalf("Expected to find Event 1 in render")
+ }
+
+ e2Start := parsedTimeNow.Add(-time.Hour * 24 * 5)
+ e2End := e2Start.Add(time.Hour * 4)
+
+ AddEvent("Event 2", "this event is in the past", "gno.land", "gnome land", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))
+
+ got = renderHome(false)
+
+ upcomingPos := strings.Index(got, "## Upcoming events")
+ pastPos := strings.Index(got, "## Past events")
+
+ e1Pos := strings.Index(got, "Event 1")
+ e2Pos := strings.Index(got, "Event 2")
+
+ // expected index ordering: upcoming < e1 < past < e2
+ if e1Pos < upcomingPos || e1Pos > pastPos {
+ t.Fatalf("Expected to find Event 1 in Upcoming events")
+ }
+
+ if e2Pos < upcomingPos || e2Pos < pastPos || e2Pos < e1Pos {
+ t.Fatalf("Expected to find Event 2 on auth different pos")
+ }
+
+ // larger index => smaller startTime (future => past)
+ if events[0].startTime.Unix() < events[1].startTime.Unix() {
+ t.Fatalf("expected ordering to be different")
+ }
+}
+
+func TestAddEventErrors(t *testing.T) {
+ std.TestSetOrigCaller(su)
+ std.TestSetRealm(suRealm)
+
+ _, err := AddEvent("", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31Z", "2009-02-13T23:33:31Z")
+ uassert.ErrorIs(t, err, ErrEmptyName)
+
+ _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "", "2009-02-13T23:33:31Z")
+ uassert.ErrorContains(t, err, ErrInvalidStartTime.Error())
+
+ _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31Z", "")
+ uassert.ErrorContains(t, err, ErrInvalidEndTime.Error())
+
+ _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31Z", "2009-02-13T23:30:31Z")
+ uassert.ErrorIs(t, err, ErrEndBeforeStart)
+
+ _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31+06:00", "2009-02-13T23:33:31+02:00")
+ uassert.ErrorIs(t, err, ErrStartEndTimezonemMismatch)
+
+ tooLongDesc := `Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean ma`
+ _, err = AddEvent("sample name", tooLongDesc, "gno.land", "gnome land", "2009-02-13T23:31:31Z", "2009-02-13T23:33:31Z")
+ uassert.ErrorContains(t, err, ErrDescriptionTooLong.Error())
+}
+
+func TestDeleteEvent(t *testing.T) {
+ events = nil // remove elements from previous tests - see issue #1982
+
+ e1Start := parsedTimeNow.Add(time.Hour * 24 * 5)
+ e1End := e1Start.Add(time.Hour * 4)
+
+ id, _ := AddEvent("ToDelete", "description", "gno.land", "gnome land", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))
+
+ got := renderHome(false)
+
+ if !strings.Contains(got, "ToDelete") {
+ t.Fatalf("Expected to find ToDelete event in render")
+ }
+
+ DeleteEvent(id)
+ got = renderHome(false)
+
+ if strings.Contains(got, "ToDelete") {
+ t.Fatalf("Did not expect to find ToDelete event in render")
+ }
+}
+
+func TestEditEvent(t *testing.T) {
+ events = nil // remove elements from previous tests - see issue #1982
+
+ e1Start := parsedTimeNow.Add(time.Hour * 24 * 5)
+ e1End := e1Start.Add(time.Hour * 4)
+ loc := "gnome land"
+
+ id, _ := AddEvent("ToDelete", "description", "gno.land", loc, e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))
+
+ newName := "New Name"
+ newDesc := "Normal description"
+ newLink := "new Link"
+ newST := e1Start.Add(time.Hour)
+ newET := newST.Add(time.Hour)
+
+ EditEvent(id, newName, newDesc, newLink, "", newST.Format(time.RFC3339), newET.Format(time.RFC3339))
+ edited, _, _ := GetEventByID(id)
+
+ // Check updated values
+ uassert.Equal(t, edited.name, newName)
+ uassert.Equal(t, edited.description, newDesc)
+ uassert.Equal(t, edited.link, newLink)
+ uassert.True(t, edited.startTime.Equal(newST))
+ uassert.True(t, edited.endTime.Equal(newET))
+
+ // Check if the old values are the same
+ uassert.Equal(t, edited.location, loc)
+}
+
+func TestInvalidEdit(t *testing.T) {
+ events = nil // remove elements from previous tests - see issue #1982
+
+ uassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() {
+ EditEvent("123123", "", "", "", "", "", "")
+ })
+}
+
+func TestParseTimes(t *testing.T) {
+ // times not provided
+ // end time before start time
+ // timezone Missmatch
+
+ _, _, err := parseTimes("", "")
+ uassert.ErrorContains(t, err, ErrInvalidStartTime.Error())
+
+ _, _, err = parseTimes(now, "")
+ uassert.ErrorContains(t, err, ErrInvalidEndTime.Error())
+
+ _, _, err = parseTimes("2009-02-13T23:30:30Z", "2009-02-13T21:30:30Z")
+ uassert.ErrorContains(t, err, ErrEndBeforeStart.Error())
+
+ _, _, err = parseTimes("2009-02-10T23:30:30+02:00", "2009-02-13T21:30:33+05:00")
+ uassert.ErrorContains(t, err, ErrStartEndTimezonemMismatch.Error())
+}
diff --git a/examples/gno.land/r/gnoland/events/gno.mod b/examples/gno.land/r/gnoland/events/gno.mod
index ec781c7cf10..5a4c6ac56f3 100644
--- a/examples/gno.land/r/gnoland/events/gno.mod
+++ b/examples/gno.land/r/gnoland/events/gno.mod
@@ -1,3 +1,8 @@
module gno.land/r/gnoland/events
-require gno.land/p/demo/ui v0.0.0-latest
+require (
+ gno.land/p/demo/ownable/exts/authorizable v0.0.0-latest
+ gno.land/p/demo/seqid v0.0.0-latest
+ gno.land/p/demo/uassert v0.0.0-latest
+ gno.land/p/demo/ufmt v0.0.0-latest
+)
diff --git a/examples/gno.land/r/gnoland/events/rendering.gno b/examples/gno.land/r/gnoland/events/rendering.gno
new file mode 100644
index 00000000000..bde32065d27
--- /dev/null
+++ b/examples/gno.land/r/gnoland/events/rendering.gno
@@ -0,0 +1,145 @@
+package events
+
+import (
+ "bytes"
+ "time"
+
+ "gno.land/p/demo/ufmt"
+)
+
+const (
+ MaxWidgetSize = 5
+)
+
+// RenderEventWidget shows up to eventsToRender of the latest events to a caller
+func RenderEventWidget(eventsToRender int) (string, error) {
+ numOfEvents := len(events)
+ if numOfEvents == 0 {
+ return "No events.", nil
+ }
+
+ if eventsToRender > MaxWidgetSize {
+ return "", ErrWidgetMaxAmt
+ }
+
+ if eventsToRender < 1 {
+ return "", ErrWidgetMinAmt
+ }
+
+ if eventsToRender > numOfEvents {
+ eventsToRender = numOfEvents
+ }
+
+ output := ""
+
+ for _, event := range events[eventsToRender:] {
+ output += ufmt.Sprintf("- [%s](%s)\n", event.name, event.link)
+ }
+
+ return output, nil
+}
+
+// renderHome renders the home page of the events realm
+func renderHome(admin bool) string {
+ output := "# gno.land events\n\n"
+
+ if len(events) == 0 {
+ output += "No upcoming or past events."
+ return output
+ }
+
+ output += "Below is a list of all gno.land events, including in progress, upcoming, and past ones.\n\n"
+ output += "---\n\n"
+
+ var (
+ inProgress = ""
+ upcoming = ""
+ past = ""
+ now = time.Now()
+ )
+
+ for _, e := range events {
+ if now.Before(e.startTime) {
+ upcoming += e.Render(admin)
+ } else if now.After(e.endTime) {
+ past += e.Render(admin)
+ } else {
+ inProgress += e.Render(admin)
+ }
+ }
+
+ if upcoming != "" {
+ // Add upcoming events
+ output += "## Upcoming events\n\n"
+ output += ""
+
+ output += upcoming
+
+ output += "
\n\n"
+ output += "---\n\n"
+ }
+
+ if inProgress != "" {
+ output += "## Currently in progress\n\n"
+ output += ""
+
+ output += inProgress
+
+ output += "
\n\n"
+ output += "---\n\n"
+ }
+
+ if past != "" {
+ // Add past events
+ output += "## Past events\n\n"
+ output += ""
+
+ output += past
+
+ output += "
\n\n"
+ }
+
+ return output
+}
+
+// Render returns the markdown representation of a single event instance
+func (e Event) Render(admin bool) string {
+ var buf bytes.Buffer
+
+ buf.WriteString("\n\n")
+ buf.WriteString(ufmt.Sprintf("### %s\n\n", e.name))
+ buf.WriteString(ufmt.Sprintf("%s\n\n", e.description))
+ buf.WriteString(ufmt.Sprintf("**Location:** %s\n\n", e.location))
+
+ _, offset := e.startTime.Zone() // offset is in seconds
+ hoursOffset := offset / (60 * 60)
+ sign := ""
+ if offset >= 0 {
+ sign = "+"
+ }
+
+ buf.WriteString(ufmt.Sprintf("**Starts:** %s UTC%s%d\n\n", e.startTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset))
+ buf.WriteString(ufmt.Sprintf("**Ends:** %s UTC%s%d\n\n", e.endTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset))
+
+ if admin {
+ buf.WriteString(ufmt.Sprintf("[EDIT](/r/gnoland/events?help&__func=EditEvent&id=%s)\n\n", e.id))
+ buf.WriteString(ufmt.Sprintf("[DELETE](/r/gnoland/events?help&__func=DeleteEvent&id=%s)\n\n", e.id))
+ }
+
+ if e.link != "" {
+ buf.WriteString(ufmt.Sprintf("[See more](%s)\n\n", e.link))
+ }
+
+ buf.WriteString("
")
+
+ return buf.String()
+}
+
+// Render is the main rendering entry point
+func Render(path string) string {
+ if path == "admin" {
+ return renderHome(true)
+ }
+
+ return renderHome(false)
+}
diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod
index cb2ec58b665..c208ad421c9 100644
--- a/examples/gno.land/r/gnoland/home/gno.mod
+++ b/examples/gno.land/r/gnoland/home/gno.mod
@@ -5,4 +5,5 @@ require (
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/ui 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 62984711d79..e5f24f87135 100644
--- a/examples/gno.land/r/gnoland/home/home.gno
+++ b/examples/gno.land/r/gnoland/home/home.gno
@@ -7,6 +7,7 @@ import (
"gno.land/p/demo/ufmt"
"gno.land/p/demo/ui"
blog "gno.land/r/gnoland/blog"
+ events "gno.land/r/gnoland/events"
)
// XXX: p/demo/ui API is crappy, we need to make it more idiomatic
@@ -35,7 +36,7 @@ func Render(_ string) string {
dom.Body.Append(
ui.Columns{3, []ui.Element{
lastBlogposts(4),
- upcomingEvents(4),
+ upcomingEvents(),
lastContributions(4),
}},
)
@@ -68,7 +69,7 @@ func Render(_ string) string {
func lastBlogposts(limit int) ui.Element {
posts := blog.RenderLastPostsWidget(limit)
return ui.Element{
- ui.H3("Latest Blogposts"),
+ ui.H3("[Latest Blogposts](/r/gnoland/blog)"),
ui.Text(posts),
}
}
@@ -81,11 +82,11 @@ func lastContributions(limit int) ui.Element {
}
}
-func upcomingEvents(limit int) ui.Element {
+func upcomingEvents() ui.Element {
+ out, _ := events.RenderEventWidget(events.MaxWidgetSize)
return ui.Element{
- ui.H3("Upcoming Events"),
- // TODO: replace with r/gnoland/events
- ui.Text("[View upcoming events](/events)"),
+ ui.H3("[Latest Events](/r/gnoland/events)"),
+ ui.Text(out),
}
}
diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno
index 2b0a802718f..15f329683f4 100644
--- a/examples/gno.land/r/gnoland/home/home_filetest.gno
+++ b/examples/gno.land/r/gnoland/home/home_filetest.gno
@@ -68,15 +68,15 @@ func main() {
//
//
//
-// ### Latest Blogposts
+// ### [Latest Blogposts](/r/gnoland/blog)
//
// No posts.
//
//
//
-// ### Upcoming Events
+// ### [Latest Events](/r/gnoland/events)
//
-// [View upcoming events](/events)
+// No events.
//
//
//
diff --git a/examples/gno.land/r/gnoland/home/overide_filetest.gno b/examples/gno.land/r/gnoland/home/overide_filetest.gno
index 34356b93349..4f21b90a3c2 100644
--- a/examples/gno.land/r/gnoland/home/overide_filetest.gno
+++ b/examples/gno.land/r/gnoland/home/overide_filetest.gno
@@ -21,4 +21,4 @@ func main() {
// Output:
// Hello World!
-// r: unauthorized; caller is not owner
+// r: ownable: caller is not owner