Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new govdao pattern with context #2380

Merged
merged 21 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions examples/gno.land/p/demo/context/context.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Package context provides a minimal implementation of Go context with support
// for Value and WithValue.
//
// Adapted from https://github.com/golang/go/tree/master/src/context/.
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package context

type Context interface {
// Value returns the value associated with this context for key, or nil
// if no value is associated with key.
Value(key interface{}) interface{}
}

// Empty returns a non-nil, empty context, similar with context.Background and
// context.TODO in Go.
func Empty() Context {
return &emptyCtx{}
}

type emptyCtx struct{}

func (ctx emptyCtx) Value(key interface{}) interface{} {
return nil
}

func (ctx emptyCtx) String() string {
return "context.Empty"
}

type valueCtx struct {
parent Context
key, val interface{}
}

func (ctx *valueCtx) Value(key interface{}) interface{} {
if ctx.key == key {
return ctx.val
}
return ctx.parent.Value(key)
}

func stringify(v interface{}) string {
switch s := v.(type) {
case stringer:
return s.String()
case string:
return s
}
return "non-stringer"
}

type stringer interface {
String() string
}

func (c *valueCtx) String() string {
return stringify(c.parent) + ".WithValue(" +
stringify(c.key) + ", " +
stringify(c.val) + ")"
}

// WithValue returns a copy of parent in which the value associated with key is
// val.
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
// XXX: if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") }
return &valueCtx{parent, key, val}
}
96 changes: 96 additions & 0 deletions examples/gno.land/p/demo/context/context_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package context

import "testing"

func TestContextExample(t *testing.T) {
type favContextKey string

k := favContextKey("language")
ctx := WithValue(Empty(), k, "Gno")

if v := ctx.Value(k); v != nil {
if string(v) != "Gno" {
t.Errorf("language value should be Gno, but is %s", v)
}
} else {
t.Errorf("language key value was not found")
}

if v := ctx.Value(favContextKey("color")); v != nil {
t.Errorf("color key was found")
}
}

// otherContext is a Context that's not one of the types defined in context.go.
// This lets us test code paths that differ based on the underlying type of the
// Context.
type otherContext struct {
Context
}

type (
key1 int
key2 int
)

// func (k key2) String() string { return fmt.Sprintf("%[1]T(%[1]d)", k) }

var (
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
k1 = key1(1)
k2 = key2(1) // same int as k1, different type
k3 = key2(3) // same type as k2, different int
)

func TestValues(t *testing.T) {
check := func(c Context, nm, v1, v2, v3 string) {
if v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {
t.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)
}
if v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {
t.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)
}
if v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {
t.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)
}
}

c0 := Empty()
check(c0, "c0", "", "", "")

t.Skip() // XXX: depends on https://github.com/gnolang/gno/issues/2386

c1 := WithValue(Empty(), k1, "c1k1")
check(c1, "c1", "c1k1", "", "")

/*if got, want := c1.String(), `context.Empty.WithValue(context_test.key1, c1k1)`; got != want {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("c.String() = %q want %q", got, want)
}*/

c2 := WithValue(c1, k2, "c2k2")
check(c2, "c2", "c1k1", "c2k2", "")

/*if got, want := fmt.Sprint(c2), `context.Empty.WithValue(context_test.key1, c1k1).WithValue(context_test.key2(1), c2k2)`; got != want {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("c.String() = %q want %q", got, want)
}*/

c3 := WithValue(c2, k3, "c3k3")
check(c3, "c2", "c1k1", "c2k2", "c3k3")

c4 := WithValue(c3, k1, nil)
check(c4, "c4", "", "c2k2", "c3k3")

o0 := otherContext{Empty()}
check(o0, "o0", "", "", "")

o1 := otherContext{WithValue(Empty(), k1, "c1k1")}
check(o1, "o1", "c1k1", "", "")

o2 := WithValue(o1, k2, "o2k2")
check(o2, "o2", "c1k1", "o2k2", "")

o3 := otherContext{c4}
check(o3, "o3", "", "c2k2", "c3k3")

o4 := WithValue(o3, k3, nil)
check(o4, "o4", "", "c2k2", "")
}
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/context/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/context
5 changes: 4 additions & 1 deletion examples/gno.land/p/gov/proposal/gno.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
module gno.land/p/gov/proposal

require gno.land/p/demo/uassert v0.0.0-latest
require (
gno.land/p/demo/context v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
)
51 changes: 45 additions & 6 deletions examples/gno.land/p/gov/proposal/proposal.gno
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package proposal
import (
"errors"
"std"

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

var errNotGovDAO = errors.New("only r/gov/dao can be the caller")
Expand All @@ -16,11 +18,20 @@ func NewExecutor(callback func() error) Executor {
}
}

// NewCtxExecutor creates a new executor with the provided callback function.
func NewCtxExecutor(callback func(ctx context.Context) error) Executor {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
return &executorImpl{
callbackCtx: callback,
done: false,
}
}

// executorImpl is an implementation of the Executor interface.
type executorImpl struct {
callback func() error
done bool
success bool
callback func() error
callbackCtx func(ctx context.Context) error
done bool
success bool
}

// Execute runs the executor's callback function.
Expand All @@ -32,9 +43,13 @@ func (exec *executorImpl) Execute() error {
// Verify the executor is r/gov/dao
assertCalledByGovdao()

// Run the callback
err := exec.callback()

var err error
if exec.callback != nil {
err = exec.callback()
} else if exec.callbackCtx != nil {
ctx := context.WithValue(context.Empty(), statusContextKey, approvedStatus)
err = exec.callbackCtx(ctx)
}
exec.done = true
exec.success = err == nil

Expand Down Expand Up @@ -62,6 +77,21 @@ func (exec executorImpl) GetStatus() Status {
}
}

func IsApprovedByGovdaoContext(ctx context.Context) bool {
v := ctx.Value(statusContextKey)
if v == nil {
return false
}
vs, ok := v.(string)
return ok && vs == approvedStatus
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
}

func AssertContextApprovedByGovDAO(ctx context.Context) {
if !IsApprovedByGovdaoContext(ctx) {
panic("not approved by govdao")
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
}
}

// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao
func assertCalledByGovdao() {
caller := std.CurrentRealm().PkgPath()
Expand All @@ -70,3 +100,12 @@ func assertCalledByGovdao() {
panic(errNotGovDAO)
}
}

type propContextKey string

func (k propContextKey) String() string { return string(k) }

const (
statusContextKey = propContextKey("govdao-prop-status")
approvedStatus = "approved"
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
)
12 changes: 11 additions & 1 deletion examples/gno.land/r/gnoland/blog/admin.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strings"

"gno.land/p/demo/avl"
"gno.land/p/demo/context"
"gno.land/p/gov/proposal"
)

var (
Expand Down Expand Up @@ -39,11 +41,19 @@ func AdminRemoveModerator(addr std.Address) {
moderatorList.Set(addr.String(), false) // FIXME: delete instead?
}

func DaoAddPost(ctx context.Context, slug, title, body, publicationDate, authors, tags string) {
proposal.AssertContextApprovedByGovDAO(ctx)
caller := std.DerivePkgAddr("gno.land/r/gov/dao")
addPost(caller, slug, title, body, publicationDate, authors, tags)
}

func ModAddPost(slug, title, body, publicationDate, authors, tags string) {
assertIsModerator()

caller := std.GetOrigCaller()
addPost(caller, slug, title, body, publicationDate, authors, tags)
}

func addPost(caller std.Address, slug, title, body, publicationDate, authors, tags string) {
var tagList []string
if tags != "" {
tagList = strings.Split(tags, ",")
Expand Down
2 changes: 2 additions & 0 deletions examples/gno.land/r/gnoland/blog/gno.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ module gno.land/r/gnoland/blog
require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/blog v0.0.0-latest
gno.land/p/demo/context v0.0.0-latest
gno.land/p/gov/proposal v0.0.0-latest
)
12 changes: 7 additions & 5 deletions examples/gno.land/r/gov/dao/prop1_filetest.gno
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// PKGPATH: gno.land/r/foo/prop01
//
// Please note that this package is intended for demonstration purposes only.
// You could execute this code (the init part) by running a `maketx run` command
// or by uploading a similar package to a personal namespace.
//
// For the specific case of validators, a `r/gnoland/valopers` will be used to
// organize the lifecycle of validators (register, etc), and this more complex
// contract will be responsible to generate proposals.
package main
package prop01
ajnavarro marked this conversation as resolved.
Show resolved Hide resolved

import (
"std"
Expand Down Expand Up @@ -67,20 +69,20 @@ func main() {

// Output:
// --
// - [/r/gov/dao:0](0) - manual valset changes proposal example (by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
// - [/r/gov/dao:0](0) - manual valset changes proposal example (by g1acws3q8u0hjl9pndjy8rqz58q3x05dnd6d99qa)
// --
// # Prop#0
//
// manual valset changes proposal example
// Status: active
// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
// Author: g1acws3q8u0hjl9pndjy8rqz58q3x05dnd6d99qa
// --
// --
// # Prop#0
//
// manual valset changes proposal example
// Status: accepted
// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
// Author: g1acws3q8u0hjl9pndjy8rqz58q3x05dnd6d99qa
// --
// No valset changes to apply.
// --
Expand All @@ -89,7 +91,7 @@ func main() {
//
// manual valset changes proposal example
// Status: succeeded
// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
// Author: g1acws3q8u0hjl9pndjy8rqz58q3x05dnd6d99qa
// --
// Valset changes to apply:
// - g12345678 (10)
Expand Down
Loading
Loading