diff --git a/examples/gno.land/p/demo/context/context.gno b/examples/gno.land/p/demo/context/context.gno new file mode 100644 index 00000000000..92d191012eb --- /dev/null +++ b/examples/gno.land/p/demo/context/context.gno @@ -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} +} diff --git a/examples/gno.land/p/demo/context/context_test.gno b/examples/gno.land/p/demo/context/context_test.gno new file mode 100644 index 00000000000..0059f0d2a25 --- /dev/null +++ b/examples/gno.land/p/demo/context/context_test.gno @@ -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 ( + 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 { + 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 { + 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", "") +} diff --git a/examples/gno.land/p/demo/context/gno.mod b/examples/gno.land/p/demo/context/gno.mod new file mode 100644 index 00000000000..a04ae1f91f8 --- /dev/null +++ b/examples/gno.land/p/demo/context/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/context diff --git a/examples/gno.land/p/gov/proposal/gno.mod b/examples/gno.land/p/gov/proposal/gno.mod index 38c297435bf..8604740f898 100644 --- a/examples/gno.land/p/gov/proposal/gno.mod +++ b/examples/gno.land/p/gov/proposal/gno.mod @@ -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 +) diff --git a/examples/gno.land/p/gov/proposal/proposal.gno b/examples/gno.land/p/gov/proposal/proposal.gno index c347ca16cc6..79303b62a82 100644 --- a/examples/gno.land/p/gov/proposal/proposal.gno +++ b/examples/gno.land/p/gov/proposal/proposal.gno @@ -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") @@ -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 { + 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. @@ -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 @@ -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 +} + +func AssertContextApprovedByGovDAO(ctx context.Context) { + if !IsApprovedByGovdaoContext(ctx) { + panic("not approved by govdao") + } +} + // assertCalledByGovdao asserts that the calling Realm is /r/gov/dao func assertCalledByGovdao() { caller := std.CurrentRealm().PkgPath() @@ -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" +) diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 3becb7022dd..08b0911cf24 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -5,6 +5,8 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/context" + "gno.land/p/gov/proposal" ) var ( @@ -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, ",") diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 1d64238cdc8..17c17e0cfa6 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -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 ) diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno index cca7aa91ff2..5778d9e4e58 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno @@ -1,3 +1,5 @@ +// 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. @@ -5,7 +7,7 @@ // 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 import ( "std" @@ -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. // -- @@ -89,7 +91,7 @@ func main() { // // manual valset changes proposal example // Status: succeeded -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Author: g1acws3q8u0hjl9pndjy8rqz58q3x05dnd6d99qa // -- // Valset changes: // - #123: g12345678 (10) diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/prop2_filetest.gno new file mode 100644 index 00000000000..8391630e21b --- /dev/null +++ b/examples/gno.land/r/gov/dao/prop2_filetest.gno @@ -0,0 +1,86 @@ +// PKGPATH: gno.land/r/foo/prop02 +package prop02 + +import ( + "time" + + "gno.land/p/demo/context" + "gno.land/p/gov/proposal" + gnoblog "gno.land/r/gnoland/blog" + govdao "gno.land/r/gov/dao" +) + +func init() { + executor := proposal.NewCtxExecutor(func(ctx context.Context) error { + gnoblog.DaoAddPost( + ctx, + "hello-from-govdao", // slug + "Hello from GovDAO!", // title + "This post was published by a GovDAO proposal.", // body + time.Now().Format(time.RFC3339), // publidation date + "moul", // authors + "govdao,example", // tags + ) + return nil + }) + + // Create a proposal. + // XXX: payment + comment := "post a new blogpost about govdao" + govdao.Propose(comment, executor) +} + +func main() { + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("1")) + println("--") + govdao.VoteOnProposal(1, "YES") + println("--") + println(govdao.Render("1")) + println("--") + println(gnoblog.Render("")) + println("--") + govdao.ExecuteProposal(1) + println("--") + println(govdao.Render("1")) + println("--") + println(gnoblog.Render("")) +} + +// Output: +// -- +// - [/r/gov/dao:0](0) - post a new blogpost about govdao (by g1qzu77x7g9z0klhnavmewmhxcz4jcckflhl7sce) +// -- +// # Prop#0 +// +// post a new blogpost about govdao +// Status: active +// Author: g1qzu77x7g9z0klhnavmewmhxcz4jcckflhl7sce +// -- +// -- +// # Prop#0 +// +// post a new blogpost about govdao +// Status: accepted +// Author: g1qzu77x7g9z0klhnavmewmhxcz4jcckflhl7sce +// -- +// # Gnoland's Blog +// +// No posts. +// -- +// -- +// # Prop#0 +// +// post a new blogpost about govdao +// Status: succeeded +// Author: g1qzu77x7g9z0klhnavmewmhxcz4jcckflhl7sce +// -- +// # Gnoland's Blog +// +//
+// +// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao) +// 13 Feb 2009 +//