Skip to content

Commit

Permalink
feat(gnovm, tm2): implement event emission with std.Emit (#1653)
Browse files Browse the repository at this point in the history
# Description

Succeed in my predecessor's legacy.

I have implemented the output to show the path where the event occurred,
as discussed in #1833. Also made it print the timestamp or block height
together.

## Key Changes

In this change, event emission functionality has been added to the Gno.
The main changes include:

1. Introducing of the `emitEvent` function:
- The `emitEvent` function emits an event based on the given type and
attributes
    - Attributes are passed as an even-length of array key-value pairs
- When emitting an event, the current _package path_, _timestamp_, and
_block height_ information are recorded along with the event(discussed
in #1833). This metadata provides additional context about where the
event occured.

2. Additional of event-related types and functions in the `sdk`
packages:
- `NewEvent` creates a new `Event` object based on the provided
information.
- `ArributedEvent` struct contains informations such as event type,
package path, block height, timestamp and attributes. But I'm not sure
how to utilize the `eventType` yet. So, I've just put it as a
placeholder which will be a disscussion for another time.
- `EventArribute` represents an attribute of an event and consists of a
key-value pair
- `NewEventArribute` creates a new `EventAttribute` object based on the
given key-value pair.

## Example

```go
package ee

import (
	"std"
)

const (
	EventSender = "sender"
	EventReceiver = "receiver"
)

func Sender(){
    SubSender()
    SubReceiver()
}

func SubSender() {
    std.Emit(
        EventSender,
		"key1", "value1",
		"key2", "value2",
		"key3", "value3",
    )  
}

func SubReceiver() {
    std.Emit(
        EventReceiver,
        "bar", "baz",
    )
}

func Receiver() {
    std.Emit(
        EventReceiver,
        "foo", "bar",
    )
}
```

### Result

```json
[
  "{\"type\":\"sender\",\"pkg_path\":\"gno.land/r/demo/ee\",\"identifier\":\"SubSender\",\"timestamp\":1713846501,\"attributes\":[{\"key\":\"key1\",\"value\":\"value1\"},{\"key\":\"key2\",\"value\":\"value2\"},{\"key\":\"key3\",\"value\":\"value3\"}]}",
  "{\"type\":\"receiver\",\"pkg_path\":\"gno.land/r/demo/ee\",\"identifier\":\"SubReceiver\",\"timestamp\":1713846501,\"attributes\":[{\"key\":\"bar\",\"value\":\"baz\"}]}"
]
```

## Related Issue/PR

#575 emit & event built-in functions (@r3v4s)
#853 feat: event & emit in gno (@anarcher) <- previous work.
#975 [META] Gno Wishlist / Feature Request Dump (@zivkovicmilos)

---------

Co-authored-by: n3wbie <r3v4@onbloc.xyz>
Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 30, 2024
1 parent de2fc45 commit 5429655
Show file tree
Hide file tree
Showing 19 changed files with 744 additions and 24 deletions.
32 changes: 10 additions & 22 deletions gno.land/cmd/gnoland/testdata/addpkg.txtar
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
# test for add package

# load hello.gno package located in $WORK directory as gno.land/r/hello
loadpkg gno.land/r/hello $WORK

## start a new node
gnoland start

## add hello.gno package located in $WORK directory as gno.land/r/hello
gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1

## compare AddPkg
cmp stdout stdout.addpkg.success


## execute SayHello
gnokey maketx call -pkgpath gno.land/r/hello -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1

gnokey maketx call -pkgpath gno.land/r/hello -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1

## compare SayHello
cmp stdout stdout.call.success
stdout '\("hello world!" string\)'
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: \d+'
stdout 'HEIGHT: \d+'
stdout 'EVENTS: \[\]'

-- hello.gno --
package hello

func SayHello() string {
return "hello world!"
}


-- stdout.addpkg.success --

OK!
GAS WANTED: 2000000
GAS USED: 119829
-- stdout.call.success --
("hello world!" string)
OK!
GAS WANTED: 2000000
GAS USED: 52801
59 changes: 59 additions & 0 deletions gno.land/cmd/gnoland/testdata/event.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# load the package from $WORK directory
loadpkg gno.land/r/demo/ee $WORK

# start a new node
gnoland start

gnokey maketx call -pkgpath gno.land/r/demo/ee -func Foo -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: \d+'
stdout 'HEIGHT: \d+'
stdout 'EVENTS: \[{\"type\":\"foo\",\"pkg_path\":\"gno.land\/r\/demo\/ee\",\"func\":\"SubFoo\",\"attrs\":\[{\"key\":\"key1\",\"value\":\"value1\"},{\"key\":\"key2\",\"value\":\"value2\"},{\"key\":\"key3\",\"value\":\"value3\"}\]},{\"type\":\"bar\",\"pkg_path\":\"gno.land\/r\/demo\/ee\",\"func\":\"SubBar\",\"attrs\":\[{\"key\":\"bar\",\"value\":\"baz\"}\]}\]'

gnokey maketx call -pkgpath gno.land/r/demo/ee -func Bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: \d+'
stdout 'HEIGHT: \d+'
stdout 'EVENTS: \[{\"type\":\"bar\",\"pkg_path\":\"gno.land\/r\/demo\/ee\",\"func\":\"Bar\",\"attrs\":\[{\"key\":\"foo\",\"value\":\"bar\"}\]}\]'

-- ee.gno --
package ee

import (
"std"
)

const (
EventFoo = "foo"
EventBar = "bar"
)

func Foo(){
SubFoo()
SubBar()
}

func SubFoo() {
std.Emit(
EventFoo,
"key1", "value1",
"key2", "value2",
"key3", "value3",
)
}

func SubBar() {
std.Emit(
EventBar,
"bar", "baz",
)
}

func Bar() {
std.Emit(
EventBar,
"foo", "bar",
)
}
51 changes: 51 additions & 0 deletions gno.land/cmd/gnoland/testdata/event_callback.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# load the package from $WORK directory
loadpkg gno.land/r/demo/cbee $WORK

# start a new node
gnoland start

gnokey maketx call -pkgpath gno.land/r/demo/cbee -func Foo -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: [0-9]+'
stdout 'HEIGHT: [0-9]+'
stdout 'EVENTS: \[{\"type\":\"foo\",\"pkg_path\":\"gno\.land\/r\/demo\/cbee\",\"func\":\"subFoo\",\"attrs\":\[{\"key\":\"k1\",\"value\":\"v1\"},{\"key\":\"k2\",\"value\":\"v2\"}\]},{\"type\":\"bar\",\"pkg_path\":\"gno\.land\/r\/demo\/cbee\",\"func\":\"subBar\",\"attrs\":\[{\"key\":\"bar\",\"value\":\"baz\"}\]}\]'


-- cbee.gno --
package cbee

import (
"std"
)

const (
foo = "foo"
bar = "bar"
)

type contractA struct{}

func (c *contractA) foo(cb func()) {
subFoo()
cb()
}

func subFoo() {
std.Emit(foo, "k1", "v1", "k2", "v2")
}

type contractB struct{}

func (c *contractB) subBar() {
std.Emit(bar, "bar", "baz")
}

func Foo() {
a := &contractA{}
b := &contractB{}

a.foo(func() {
b.subBar()
})
}
64 changes: 64 additions & 0 deletions gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# load the package from $WORK directory
loadpkg gno.land/r/demo/edcl $WORK

# start a new node
gnoland start

gnokey maketx call -pkgpath gno.land/r/demo/edcl -func Main -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: [0-9]+'
stdout 'HEIGHT: [0-9]+'
stdout 'EVENTS: \[{\"type\":\"ForLoopEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"\",\"attrs\":\[{\"key\":\"iteration\",\"value\":\"0\"},{\"key\":\"key\",\"value\":\"value\"}\]},{\"type\":\"ForLoopEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"\",\"attrs\":\[{\"key\":\"iteration\",\"value\":\"1\"},{\"key\":\"key\",\"value\":\"value\"}\]},{\"type\":\"ForLoopEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"\",\"attrs\":\[{\"key\":\"iteration\",\"value\":\"2\"},{\"key\":\"key\",\"value\":\"value\"}\]},{\"type\":\"ForLoopCompletionEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"forLoopEmitExample\",\"attrs\":\[{\"key\":\"count\",\"value\":\"3\"}\]},{\"type\":\"CallbackEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"\",\"attrs\":\[{\"key\":\"key1\",\"value\":\"value1\"},{\"key\":\"key2\",\"value\":\"value2\"}\]},{\"type\":\"CallbackCompletionEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"callbackEmitExample\",\"attrs\":\[{\"key\":\"key\",\"value\":\"value\"}\]},{\"type\":\"DeferEvent\",\"pkg_path\":\"gno.land\/r\/demo\/edcl\",\"func\":\"deferEmitExample\",\"attrs\":\[{\"key\":\"key1\",\"value\":\"value1\"},{\"key\":\"key2\",\"value\":\"value2\"}\]}\]'

-- edcl.gno --

package edcl

import (
"std"
"strconv"
)

func Main() {
deferEmitExample()
}

func deferEmitExample() {
defer func() {
std.Emit("DeferEvent", "key1", "value1", "key2", "value2")
println("Defer emit executed")
}()

forLoopEmitExample(3, func(i int) {
std.Emit("ForLoopEvent", "iteration", strconv.Itoa(i), "key", "value")
println("For loop emit executed: iteration ", i)
})

callbackEmitExample(func() {
std.Emit("CallbackEvent", "key1", "value1", "key2", "value2")
println("Callback emit executed")
})

println("deferEmitExample completed")
}

func forLoopEmitExample(count int, callback func(int)) {
defer func() {
std.Emit("ForLoopCompletionEvent", "count", strconv.Itoa(count))
println("For loop completion emit executed ", count)
}()

for i := 0; i < count; i++ {
callback(i)
}
}

func callbackEmitExample(callback func()) {
defer func() {
std.Emit("CallbackCompletionEvent", "key", "value")
println("Callback completion emit executed")
}()

callback()
}
64 changes: 64 additions & 0 deletions gno.land/cmd/gnoland/testdata/event_for_statement.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# load the package from $WORK directory
loadpkg gno.land/r/demo/foree $WORK

# start a new node
gnoland start

gnokey maketx call -pkgpath gno.land/r/demo/foree -func Foo -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: [0-9]+'
stdout 'HEIGHT: [0-9]+'
stdout 'EVENTS: \[{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]},{"type":"testing","pkg_path":"gno.land\/r\/demo\/foree","func":"Foo","attrs":\[{"key":"foo","value":"bar"}\]}\]'

gnokey maketx call -pkgpath gno.land/r/demo/foree -func Bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: [0-9]+'
stdout 'HEIGHT: [0-9]+'
stdout 'EVENTS: \[{"type":"Foo","pkg_path":"gno.land\/r\/demo\/foree","func":"subFoo","attrs":\[{"key":"k1","value":"v1"},{"key":"k2","value":"v2"}\]},{"type":"Bar","pkg_path":"gno.land\/r\/demo\/foree","func":"subBar","attrs":\[{"key":"bar","value":"baz"}\]},{"type":"Foo","pkg_path":"gno.land\/r\/demo\/foree","func":"subFoo","attrs":\[{"key":"k1","value":"v1"},{"key":"k2","value":"v2"}\]},{"type":"Bar","pkg_path":"gno.land\/r\/demo\/foree","func":"subBar","attrs":\[{"key":"bar","value":"baz"}\]}\]'


-- foree.gno --

package foree

import "std"

func Foo() {
for i := 0; i < 10; i++ {
std.Emit("testing", "foo", "bar")
}
}

const (
eventFoo = "Foo"
eventBar = "Bar"
)

type contractA struct{}

func (c *contractA) Foo(cb func()) {
subFoo()
cb()
}

func subFoo() {
std.Emit(eventFoo, "k1", "v1", "k2", "v2")
}

type contractB struct{}

func (c *contractB) subBar() {
std.Emit(eventBar, "bar", "baz")
}

func Bar() {
a := &contractA{}
b := &contractB{}
for i := 0; i < 2; i++ {
a.Foo(func() {
b.subBar()
})
}
}
5 changes: 5 additions & 0 deletions gno.land/pkg/sdk/vm/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
OrigSendSpent: new(std.Coins),
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx),
EventLogger: ctx.EventLogger(),
}
// Parse and run the files, construct *PV.
m2 := gno.NewMachineWithOptions(
Expand Down Expand Up @@ -275,6 +276,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
OrigSendSpent: new(std.Coins),
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx),
EventLogger: ctx.EventLogger(),
}
// Construct machine and evaluate.
m := gno.NewMachineWithOptions(
Expand Down Expand Up @@ -351,6 +353,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) {
OrigSendSpent: new(std.Coins),
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx),
EventLogger: ctx.EventLogger(),
}
// Parse and run the files, construct *PV.
buf := new(bytes.Buffer)
Expand Down Expand Up @@ -500,6 +503,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res
// OrigSendSpent: nil,
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded.
EventLogger: ctx.EventLogger(),
}
m := gno.NewMachineWithOptions(
gno.MachineOptions{
Expand Down Expand Up @@ -566,6 +570,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string
// OrigSendSpent: nil,
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded.
EventLogger: ctx.EventLogger(),
}
m := gno.NewMachineWithOptions(
gno.MachineOptions{
Expand Down
25 changes: 25 additions & 0 deletions gnovm/stdlibs/native.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gnovm/stdlibs/std/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ type ExecContext struct {
OrigSend std.Coins
OrigSendSpent *std.Coins // mutable
Banker BankerInterface
EventLogger *sdk.EventLogger
}
19 changes: 19 additions & 0 deletions gnovm/stdlibs/std/emit_event.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package std

// Emit is a function that constructs a gnoEvent with a specified type and attributes.
// It then forwards this event to the event logger. Each emitted event carries metadata
// such as the event type, the initializing realm, and the provided attributes.
//
// The function takes type and attribute strings. typ (or type) is an arbitrary string that represents
// the type of the event. It plays a role of indexing what kind of event occurred. For example,
// a name like "Foo" or "Bar" can be used to indicating the purpose (nature) of the event.
//
// And the attrs (attributes) accepts an even number of strings and sets them as key-value pairs
// according to the order they are passed. For example, if the attr strings "key1", "value1" are
// passed in, the key is set to "key1" and the value is set to "value1".
//
// The event is dispatched to the EventLogger, which resides in the tm2/pkg/sdk/events.go file.
//
// For more details about the GnoEvent data structure, refer to its definition in the emit_event.go file.
func Emit(typ string, attrs ...string) { emit(typ, attrs) }
func emit(typ string, attrs []string)

0 comments on commit 5429655

Please sign in to comment.