From 9993c0598022b5fc0c3d9240a97537bc63b080ef Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Tue, 30 Apr 2024 23:08:31 +0900 Subject: [PATCH] feat(gnovm, tm2): implement event emission with `std.Emit` (#1653) # 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 Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/cmd/gnoland/testdata/addpkg.txtar | 32 +- gno.land/cmd/gnoland/testdata/event.txtar | 59 ++++ .../cmd/gnoland/testdata/event_callback.txtar | 51 +++ .../testdata/event_defer_callback_loop.txtar | 64 ++++ .../testdata/event_for_statement.txtar | 64 ++++ gno.land/pkg/sdk/vm/keeper.go | 5 + gnovm/stdlibs/native.go | 25 ++ gnovm/stdlibs/std/context.go | 1 + gnovm/stdlibs/std/emit_event.gno | 19 ++ gnovm/stdlibs/std/emit_event.go | 60 ++++ gnovm/stdlibs/std/emit_event_test.go | 314 ++++++++++++++++++ gnovm/stdlibs/std/native.go | 31 ++ gnovm/stdlibs/std/package.go | 19 ++ gnovm/tests/file.go | 2 + gnovm/tests/files/zrealm_natbind0.gno | 2 +- tm2/pkg/bft/abci/types/types.go | 12 + tm2/pkg/crypto/keys/client/maketx.go | 2 + tm2/pkg/sdk/baseapp.go | 4 + tm2/pkg/sdk/sdk.proto | 2 +- 19 files changed, 744 insertions(+), 24 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/event.txtar create mode 100644 gno.land/cmd/gnoland/testdata/event_callback.txtar create mode 100644 gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar create mode 100644 gno.land/cmd/gnoland/testdata/event_for_statement.txtar create mode 100644 gnovm/stdlibs/std/emit_event.gno create mode 100644 gnovm/stdlibs/std/emit_event.go create mode 100644 gnovm/stdlibs/std/emit_event_test.go create mode 100644 gnovm/stdlibs/std/package.go diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar index 071096cb49d..e62bce0c59f 100644 --- a/gno.land/cmd/gnoland/testdata/addpkg.txtar +++ b/gno.land/cmd/gnoland/testdata/addpkg.txtar @@ -1,21 +1,21 @@ # 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 @@ -23,15 +23,3 @@ 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 diff --git a/gno.land/cmd/gnoland/testdata/event.txtar b/gno.land/cmd/gnoland/testdata/event.txtar new file mode 100644 index 00000000000..45f2ceaf772 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/event.txtar @@ -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", + ) +} \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/event_callback.txtar b/gno.land/cmd/gnoland/testdata/event_callback.txtar new file mode 100644 index 00000000000..a0366df1346 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/event_callback.txtar @@ -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() + }) +} \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar b/gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar new file mode 100644 index 00000000000..9e7fac9abfe --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar @@ -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() +} \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/event_for_statement.txtar b/gno.land/cmd/gnoland/testdata/event_for_statement.txtar new file mode 100644 index 00000000000..a05a614f985 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/event_for_statement.txtar @@ -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() + }) + } +} \ No newline at end of file diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index cbeee69d938..c032ca4b7db 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -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( @@ -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( @@ -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) @@ -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{ @@ -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{ diff --git a/gnovm/stdlibs/native.go b/gnovm/stdlibs/native.go index 0b56eeb7399..3b1ab719e72 100644 --- a/gnovm/stdlibs/native.go +++ b/gnovm/stdlibs/native.go @@ -370,6 +370,31 @@ var nativeFuncs = [...]nativeFunc{ p0, p1, p2, p3) }, }, + { + "std", + "emit", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("[]string")}, + }, + []gno.FieldTypeExpr{}, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 []string + rp1 = reflect.ValueOf(&p1).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + + libs_std.X_emit( + m, + p0, p1) + }, + }, { "std", "AssertOriginCall", diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index 72ca7445aef..35aef7fbd62 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -17,4 +17,5 @@ type ExecContext struct { OrigSend std.Coins OrigSendSpent *std.Coins // mutable Banker BankerInterface + EventLogger *sdk.EventLogger } diff --git a/gnovm/stdlibs/std/emit_event.gno b/gnovm/stdlibs/std/emit_event.gno new file mode 100644 index 00000000000..147513e962b --- /dev/null +++ b/gnovm/stdlibs/std/emit_event.gno @@ -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) diff --git a/gnovm/stdlibs/std/emit_event.go b/gnovm/stdlibs/std/emit_event.go new file mode 100644 index 00000000000..46fea79d43c --- /dev/null +++ b/gnovm/stdlibs/std/emit_event.go @@ -0,0 +1,60 @@ +package std + +// ref: https://github.com/gnolang/gno/pull/575 +// ref: https://github.com/gnolang/gno/pull/1833 + +import ( + "errors" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" +) + +var errInvalidGnoEventAttrs = errors.New("cannot pair attributes due to odd count") + +func X_emit(m *gno.Machine, typ string, attrs []string) { + eventAttrs, err := attrKeysAndValues(attrs) + if err != nil { + m.Panic(typedString(err.Error())) + } + + pkgPath := CurrentRealmPath(m) + fnIdent := getPrevFunctionNameFromTarget(m, "Emit") + + evt := gnoEvent{ + Type: typ, + PkgPath: pkgPath, + Func: fnIdent, + Attributes: eventAttrs, + } + ctx := m.Context.(ExecContext) + ctx.EventLogger.EmitEvent(evt) +} + +func attrKeysAndValues(attrs []string) ([]gnoEventAttribute, error) { + attrLen := len(attrs) + if attrLen%2 != 0 { + return nil, errInvalidGnoEventAttrs + } + eventAttrs := make([]gnoEventAttribute, attrLen/2) + for i := 0; i < attrLen-1; i += 2 { + eventAttrs[i/2] = gnoEventAttribute{ + Key: attrs[i], + Value: attrs[i+1], + } + } + return eventAttrs, nil +} + +type gnoEvent struct { + Type string `json:"type"` + PkgPath string `json:"pkg_path"` + Func string `json:"func"` + Attributes []gnoEventAttribute `json:"attrs"` +} + +func (e gnoEvent) AssertABCIEvent() {} + +type gnoEventAttribute struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/gnovm/stdlibs/std/emit_event_test.go b/gnovm/stdlibs/std/emit_event_test.go new file mode 100644 index 00000000000..10bd8ecacd9 --- /dev/null +++ b/gnovm/stdlibs/std/emit_event_test.go @@ -0,0 +1,314 @@ +package std + +import ( + "encoding/json" + "strconv" + "strings" + "testing" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/stretchr/testify/assert" +) + +func TestEmit(t *testing.T) { + m := gno.NewMachine("emit", nil) + pkgPath := CurrentRealmPath(m) + tests := []struct { + name string + eventType string + attrs []string + expectedEvents []gnoEvent + expectPanic bool + }{ + { + name: "SimpleValid", + eventType: "test", + attrs: []string{"key1", "value1", "key2", "value2"}, + expectedEvents: []gnoEvent{ + { + Type: "test", + PkgPath: pkgPath, + Func: "", + Attributes: []gnoEventAttribute{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + }, + expectPanic: false, + }, + { + name: "InvalidAttributes", + eventType: "test", + attrs: []string{"key1", "value1", "key2"}, + expectPanic: true, + }, + { + name: "EmptyAttribute", + eventType: "test", + attrs: []string{"key1", "", "key2", "value2"}, + expectedEvents: []gnoEvent{ + { + Type: "test", + PkgPath: pkgPath, + Func: "", + Attributes: []gnoEventAttribute{ + {Key: "key1", Value: ""}, + {Key: "key2", Value: "value2"}, + }, + }, + }, + expectPanic: false, + }, + { + name: "EmptyType", + eventType: "", + attrs: []string{"key1", "value1", "key2", "value2"}, + expectedEvents: []gnoEvent{ + { + Type: "", + PkgPath: pkgPath, + Func: "", + Attributes: []gnoEventAttribute{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + }, + expectPanic: false, + }, + { + name: "EmptyAttributeKey", + eventType: "test", + attrs: []string{"", "value1", "key2", "value2"}, + expectedEvents: []gnoEvent{ + { + Type: "test", + PkgPath: pkgPath, + Func: "", + Attributes: []gnoEventAttribute{ + {Key: "", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + }, + expectPanic: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + elgs := sdk.NewEventLogger() + m.Context = ExecContext{EventLogger: elgs} + + if tt.expectPanic { + assert.Panics(t, func() { + X_emit(m, tt.eventType, tt.attrs) + }) + } else { + X_emit(m, tt.eventType, tt.attrs) + assert.Equal(t, len(tt.expectedEvents), len(elgs.Events())) + + res, err := json.Marshal(elgs.Events()) + if err != nil { + t.Fatal(err) + } + + expectRes, err := json.Marshal(tt.expectedEvents) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, string(expectRes), string(res)) + } + }) + } +} + +func TestEmit_MultipleEvents(t *testing.T) { + t.Parallel() + m := gno.NewMachine("emit", nil) + + elgs := sdk.NewEventLogger() + m.Context = ExecContext{EventLogger: elgs} + + attrs1 := []string{"key1", "value1", "key2", "value2"} + attrs2 := []string{"key3", "value3", "key4", "value4"} + X_emit(m, "test1", attrs1) + X_emit(m, "test2", attrs2) + + assert.Equal(t, 2, len(elgs.Events())) + + res, err := json.Marshal(elgs.Events()) + if err != nil { + t.Fatal(err) + } + + expect := []gnoEvent{ + { + Type: "test1", + PkgPath: "", + Func: "", + Attributes: []gnoEventAttribute{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + { + Type: "test2", + PkgPath: "", + Func: "", + Attributes: []gnoEventAttribute{ + {Key: "key3", Value: "value3"}, + {Key: "key4", Value: "value4"}, + }, + }, + } + + expectRes, err := json.Marshal(expect) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, string(expectRes), string(res)) +} + +func TestEmit_ContractInteraction(t *testing.T) { + const ( + testFoo = "foo" + testQux = "qux" + ) + + type ( + contractA struct { + foo func(*gno.Machine, func()) + } + + contractB struct { + qux func(m *gno.Machine) + } + ) + + t.Parallel() + m := gno.NewMachine("emit", nil) + elgs := sdk.NewEventLogger() + m.Context = ExecContext{EventLogger: elgs} + + baz := func(m *gno.Machine) { + X_emit(m, testFoo, []string{"k1", "v1", "k2", "v2"}) + } + + a := &contractA{ + foo: func(m *gno.Machine, cb func()) { + baz(m) + cb() + }, + } + b := &contractB{ + qux: func(m *gno.Machine) { + X_emit(m, testQux, []string{"bar", "baz"}) + }, + } + + a.foo(m, func() { + b.qux(m) + }) + + assert.Equal(t, 2, len(elgs.Events())) + + res, err := json.Marshal(elgs.Events()) + if err != nil { + t.Fatal(err) + } + + expected := `[{"type":"foo","pkg_path":"","func":"","attrs":[{"key":"k1","value":"v1"},{"key":"k2","value":"v2"}]},{"type":"qux","pkg_path":"","func":"","attrs":[{"key":"bar","value":"baz"}]}]` + + assert.Equal(t, expected, string(res)) +} + +func TestEmit_Iteration(t *testing.T) { + const testBar = "bar" + m := gno.NewMachine("emit", nil) + + elgs := sdk.NewEventLogger() + m.Context = ExecContext{EventLogger: elgs} + + iterEvent := func(m *gno.Machine) { + for i := 0; i < 10; i++ { + X_emit(m, testBar, []string{"qux", "value1"}) + } + } + iterEvent(m) + assert.Equal(t, 10, len(elgs.Events())) + + res, err := json.Marshal(elgs.Events()) + if err != nil { + t.Fatal(err) + } + + var builder strings.Builder + builder.WriteString("[") + for i := 0; i < 10; i++ { + builder.WriteString(`{"type":"bar","pkg_path":"","func":"","attrs":[{"key":"qux","value":"value1"}]},`) + } + expected := builder.String()[:builder.Len()-1] + "]" + + assert.Equal(t, expected, string(res)) +} + +func complexInteraction(m *gno.Machine) { + deferEmitExample(m) +} + +func deferEmitExample(m *gno.Machine) { + defer func() { + X_emit(m, "DeferEvent", []string{"key1", "value1", "key2", "value2"}) + }() + + forLoopEmitExample(m, 3, func(i int) { + X_emit(m, "ForLoopEvent", []string{"iteration", strconv.Itoa(i), "key", "value"}) + }) + + callbackEmitExample(m, func() { + X_emit(m, "CallbackEvent", []string{"key1", "value1", "key2", "value2"}) + }) +} + +func forLoopEmitExample(m *gno.Machine, count int, callback func(int)) { + defer func() { + X_emit(m, "ForLoopCompletionEvent", []string{"count", strconv.Itoa(count)}) + }() + + for i := 0; i < count; i++ { + callback(i) + } +} + +func callbackEmitExample(m *gno.Machine, callback func()) { + defer func() { + X_emit(m, "CallbackCompletionEvent", []string{"key", "value"}) + }() + + callback() +} + +func TestEmit_ComplexInteraction(t *testing.T) { + m := gno.NewMachine("emit", nil) + + elgs := sdk.NewEventLogger() + m.Context = ExecContext{EventLogger: elgs} + + complexInteraction(m) + + assert.Equal(t, 7, len(elgs.Events())) + + res, err := json.Marshal(elgs.Events()) + if err != nil { + t.Fatal(err) + } + + expected := `[{"type":"ForLoopEvent","pkg_path":"","func":"","attrs":[{"key":"iteration","value":"0"},{"key":"key","value":"value"}]},{"type":"ForLoopEvent","pkg_path":"","func":"","attrs":[{"key":"iteration","value":"1"},{"key":"key","value":"value"}]},{"type":"ForLoopEvent","pkg_path":"","func":"","attrs":[{"key":"iteration","value":"2"},{"key":"key","value":"value"}]},{"type":"ForLoopCompletionEvent","pkg_path":"","func":"","attrs":[{"key":"count","value":"3"}]},{"type":"CallbackEvent","pkg_path":"","func":"","attrs":[{"key":"key1","value":"value1"},{"key":"key2","value":"value2"}]},{"type":"CallbackCompletionEvent","pkg_path":"","func":"","attrs":[{"key":"key","value":"value"}]},{"type":"DeferEvent","pkg_path":"","func":"","attrs":[{"key":"key1","value":"value1"},{"key":"key2","value":"value2"}]}]` + + assert.Equal(t, expected, string(res)) +} diff --git a/gnovm/stdlibs/std/native.go b/gnovm/stdlibs/std/native.go index 26bfe433858..deb0f1268d2 100644 --- a/gnovm/stdlibs/std/native.go +++ b/gnovm/stdlibs/std/native.go @@ -32,6 +32,37 @@ func GetHeight(m *gno.Machine) int64 { return m.Context.(ExecContext).Height } +// getPrevFunctionNameFromTarget returns the last called function name (identifier) from the call stack. +func getPrevFunctionNameFromTarget(m *gno.Machine, targetFunc string) string { + targetIndex := findTargetFuncIndex(m, targetFunc) + if targetIndex == -1 { + return "" + } + return findPrevFuncName(m, targetIndex) +} + +// findTargetFuncIndex finds and returns the index of the target function in the call stack. +func findTargetFuncIndex(m *gno.Machine, targetFunc string) int { + for i := len(m.Frames) - 1; i >= 0; i-- { + currFunc := m.Frames[i].Func + if currFunc != nil && currFunc.Name == gno.Name(targetFunc) { + return i + } + } + return -1 +} + +// findPrevFuncName returns the function name before the given index in the call stack. +func findPrevFuncName(m *gno.Machine, targetIndex int) string { + for i := targetIndex - 1; i >= 0; i-- { + currFunc := m.Frames[i].Func + if currFunc != nil { + return string(currFunc.Name) + } + } + panic("function name not found") +} + func X_origSend(m *gno.Machine) (denoms []string, amounts []int64) { os := m.Context.(ExecContext).OrigSend return ExpandCoins(os) diff --git a/gnovm/stdlibs/std/package.go b/gnovm/stdlibs/std/package.go new file mode 100644 index 00000000000..219f196b2d9 --- /dev/null +++ b/gnovm/stdlibs/std/package.go @@ -0,0 +1,19 @@ +package std + +import ( + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" +) + +var Package = amino.RegisterPackage(amino.NewPackage( + "github.com/gnolang/gno/gnovm/stdlibs/std", + "tm", + amino.GetCallersDirname(), +). + WithDependencies( + abci.Package, + ). + WithTypes( + gnoEventAttribute{}, + gnoEvent{}, + )) diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 70bed4eda50..9e4bdaf9e9a 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -17,6 +17,7 @@ import ( "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" "github.com/pmezard/go-difflib/difflib" ) @@ -50,6 +51,7 @@ func testMachineCustom(store gno.Store, pkgPath string, stdout io.Writer, maxAll OrigSend: send, OrigSendSpent: new(std.Coins), Banker: banker, + EventLogger: sdk.NewEventLogger(), } m := gno.NewMachineWithOptions(gno.MachineOptions{ PkgPath: "", // set later. diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index 0d614f5e8a1..084ddd3d18f 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -141,7 +141,7 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:6" +// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:7" // }, // "FileName": "native.gno", // "IsMethod": false, diff --git a/tm2/pkg/bft/abci/types/types.go b/tm2/pkg/bft/abci/types/types.go index 637d09b92a3..8c2764cb1bd 100644 --- a/tm2/pkg/bft/abci/types/types.go +++ b/tm2/pkg/bft/abci/types/types.go @@ -1,6 +1,7 @@ package abci import ( + "encoding/json" "time" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -116,6 +117,17 @@ func (r ResponseBase) IsErr() bool { return r.Error != nil } +func (r ResponseBase) EncodeEvents() []byte { + if len(r.Events) == 0 { + return []byte("[]") + } + res, err := json.Marshal(r.Events) + if err != nil { + panic(err) + } + return res +} + // nondeterministic type ResponseException struct { ResponseBase diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index f5cd596f2c0..603be59396c 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -182,6 +182,8 @@ func ExecSignAndBroadcast( io.Println("OK!") io.Println("GAS WANTED:", bres.DeliverTx.GasWanted) io.Println("GAS USED: ", bres.DeliverTx.GasUsed) + io.Println("HEIGHT: ", bres.Height) + io.Println("EVENTS: ", string(bres.DeliverTx.EncodeEvents())) return nil } diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 1f62f53f81a..1801a0af35f 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -653,6 +653,10 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res // each result. data = append(data, msgResult.Data...) events = append(events, msgResult.Events...) + defer func() { + events = append(events, ctx.EventLogger().Events()...) + result.Events = events + }() // TODO append msgevent from ctx. XXX XXX // stop execution and return on first failed message diff --git a/tm2/pkg/sdk/sdk.proto b/tm2/pkg/sdk/sdk.proto index 62fbfc19758..828b17950cf 100644 --- a/tm2/pkg/sdk/sdk.proto +++ b/tm2/pkg/sdk/sdk.proto @@ -12,4 +12,4 @@ message Result { abci.ResponseBase response_base = 1 [json_name = "ResponseBase"]; sint64 gas_wanted = 2 [json_name = "GasWanted"]; sint64 gas_used = 3 [json_name = "GasUsed"]; -} \ No newline at end of file +}