diff --git a/cli/smartcontract/testdata/gas/gas.go b/cli/smartcontract/testdata/gas/gas.go index d12bbb8ee..2b4cf920a 100644 --- a/cli/smartcontract/testdata/gas/gas.go +++ b/cli/smartcontract/testdata/gas/gas.go @@ -2,13 +2,25 @@ package gastoken import ( + "errors" + "fmt" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "math/big" ) // Hash contains contract hash. var Hash = util.Uint160{0xcf, 0x76, 0xe2, 0x8b, 0xd0, 0x6, 0x2c, 0x4a, 0x47, 0x8e, 0xe3, 0x55, 0x61, 0x1, 0x13, 0x19, 0xf3, 0xcf, 0xa4, 0xd2} +// TransferEvent represents "Transfer" event emitted by the contract. +type TransferEvent struct { + From util.Uint160 + To util.Uint160 + Amount *big.Int +} + // Invoker is used by ContractReader to call various safe methods. type Invoker interface { nep17.Invoker @@ -44,3 +56,87 @@ func New(actor Actor) *Contract { var nep17t = nep17.New(actor, Hash) return &Contract{ContractReader{nep17t.TokenReader, actor}, nep17t.TokenWriter, actor} } + +// TransferEventsFromApplicationLog retrieves a set of all emitted events +// with "Transfer" name from the provided ApplicationLog. +func TransferEventsFromApplicationLog(log *result.ApplicationLog) ([]*TransferEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*TransferEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "Transfer" { + continue + } + event := new(TransferEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize TransferEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to TransferEvent and +// returns an error if so. +func (e *TransferEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 3 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.From, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field From: %w", err) + } + + index++ + e.To, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field To: %w", err) + } + + index++ + e.Amount, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Amount: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/nameservice/nns.go b/cli/smartcontract/testdata/nameservice/nns.go index c279951ea..8bb311bb9 100644 --- a/cli/smartcontract/testdata/nameservice/nns.go +++ b/cli/smartcontract/testdata/nameservice/nns.go @@ -2,6 +2,8 @@ package nameservice import ( + "errors" + "fmt" "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" @@ -16,6 +18,28 @@ import ( // Hash contains contract hash. var Hash = util.Uint160{0xde, 0x46, 0x5f, 0x5d, 0x50, 0x57, 0xcf, 0x33, 0x28, 0x47, 0x94, 0xc5, 0xcf, 0xc2, 0xc, 0x69, 0x37, 0x1c, 0xac, 0x50} +// TransferEvent represents "Transfer" event emitted by the contract. +type TransferEvent struct { + From util.Uint160 + To util.Uint160 + Amount *big.Int + TokenId []byte +} + +// SetAdminEvent represents "SetAdmin" event emitted by the contract. +type SetAdminEvent struct { + Name string + OldAdmin util.Uint160 + NewAdmin util.Uint160 +} + +// RenewEvent represents "Renew" event emitted by the contract. +type RenewEvent struct { + Name string + OldExpiration *big.Int + NewExpiration *big.Int +} + // Invoker is used by ContractReader to call various safe methods. type Invoker interface { nep11.Invoker @@ -320,3 +344,259 @@ func (c *Contract) DeleteRecordTransaction(name string, typev *big.Int) (*transa func (c *Contract) DeleteRecordUnsigned(name string, typev *big.Int) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, "deleteRecord", nil, name, typev) } + +// TransferEventsFromApplicationLog retrieves a set of all emitted events +// with "Transfer" name from the provided ApplicationLog. +func TransferEventsFromApplicationLog(log *result.ApplicationLog) ([]*TransferEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*TransferEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "Transfer" { + continue + } + event := new(TransferEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize TransferEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to TransferEvent and +// returns an error if so. +func (e *TransferEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 4 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.From, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field From: %w", err) + } + + index++ + e.To, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field To: %w", err) + } + + index++ + e.Amount, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Amount: %w", err) + } + + index++ + e.TokenId, err = arr[index].TryBytes() + if err != nil { + return fmt.Errorf("field TokenId: %w", err) + } + + return nil +} + +// SetAdminEventsFromApplicationLog retrieves a set of all emitted events +// with "SetAdmin" name from the provided ApplicationLog. +func SetAdminEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetAdminEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SetAdminEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SetAdmin" { + continue + } + event := new(SetAdminEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SetAdminEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to SetAdminEvent and +// returns an error if so. +func (e *SetAdminEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 3 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.Name, err = func (item stackitem.Item) (string, error) { + b, err := item.TryBytes() + if err != nil { + return "", err + } + if !utf8.Valid(b) { + return "", errors.New("not a UTF-8 string") + } + return string(b), nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field Name: %w", err) + } + + index++ + e.OldAdmin, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field OldAdmin: %w", err) + } + + index++ + e.NewAdmin, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field NewAdmin: %w", err) + } + + return nil +} + +// RenewEventsFromApplicationLog retrieves a set of all emitted events +// with "Renew" name from the provided ApplicationLog. +func RenewEventsFromApplicationLog(log *result.ApplicationLog) ([]*RenewEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*RenewEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "Renew" { + continue + } + event := new(RenewEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize RenewEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to RenewEvent and +// returns an error if so. +func (e *RenewEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 3 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.Name, err = func (item stackitem.Item) (string, error) { + b, err := item.TryBytes() + if err != nil { + return "", err + } + if !utf8.Valid(b) { + return "", errors.New("not a UTF-8 string") + } + return string(b), nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field Name: %w", err) + } + + index++ + e.OldExpiration, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field OldExpiration: %w", err) + } + + index++ + e.NewExpiration, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field NewExpiration: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/nex/nex.go b/cli/smartcontract/testdata/nex/nex.go index 068fc1987..41de92d02 100644 --- a/cli/smartcontract/testdata/nex/nex.go +++ b/cli/smartcontract/testdata/nex/nex.go @@ -2,17 +2,36 @@ package nextoken import ( + "errors" + "fmt" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "math/big" ) // Hash contains contract hash. var Hash = util.Uint160{0xa8, 0x1a, 0xa1, 0xf0, 0x4b, 0xf, 0xdc, 0x4a, 0xa2, 0xce, 0xd5, 0xbf, 0xc6, 0x22, 0xcf, 0xe8, 0x9, 0x7f, 0xa6, 0xa2} +// TransferEvent represents "Transfer" event emitted by the contract. +type TransferEvent struct { + From util.Uint160 + To util.Uint160 + Amount *big.Int +} + +// OnMintEvent represents "OnMint" event emitted by the contract. +type OnMintEvent struct { + From util.Uint160 + To util.Uint160 + Amount *big.Int + SwapId *big.Int +} + // Invoker is used by ContractReader to call various safe methods. type Invoker interface { nep17.Invoker @@ -229,3 +248,177 @@ func (c *Contract) UpdateCapTransaction(newCap *big.Int) (*transaction.Transacti func (c *Contract) UpdateCapUnsigned(newCap *big.Int) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, "updateCap", nil, newCap) } + +// TransferEventsFromApplicationLog retrieves a set of all emitted events +// with "Transfer" name from the provided ApplicationLog. +func TransferEventsFromApplicationLog(log *result.ApplicationLog) ([]*TransferEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*TransferEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "Transfer" { + continue + } + event := new(TransferEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize TransferEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to TransferEvent and +// returns an error if so. +func (e *TransferEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 3 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.From, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field From: %w", err) + } + + index++ + e.To, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field To: %w", err) + } + + index++ + e.Amount, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Amount: %w", err) + } + + return nil +} + +// OnMintEventsFromApplicationLog retrieves a set of all emitted events +// with "OnMint" name from the provided ApplicationLog. +func OnMintEventsFromApplicationLog(log *result.ApplicationLog) ([]*OnMintEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*OnMintEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "OnMint" { + continue + } + event := new(OnMintEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize OnMintEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to OnMintEvent and +// returns an error if so. +func (e *OnMintEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 4 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.From, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field From: %w", err) + } + + index++ + e.To, err = func (item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field To: %w", err) + } + + index++ + e.Amount, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Amount: %w", err) + } + + index++ + e.SwapId, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field SwapId: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/verifyrpc/verify.go b/cli/smartcontract/testdata/verifyrpc/verify.go index a614c36cf..6835a7e4e 100644 --- a/cli/smartcontract/testdata/verifyrpc/verify.go +++ b/cli/smartcontract/testdata/verifyrpc/verify.go @@ -2,14 +2,23 @@ package verify import ( + "errors" + "fmt" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) // Hash contains contract hash. var Hash = util.Uint160{0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0} +// HelloWorldEvent represents "Hello world!" event emitted by the contract. +type HelloWorldEvent struct { + Args []any +} + // Actor is used by Contract to call state-changing methods. type Actor interface { MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) @@ -67,3 +76,68 @@ func (c *Contract) VerifyUnsigned() (*transaction.Transaction, error) { } return c.actor.MakeUnsignedRun(script, nil) } + +// HelloWorldEventsFromApplicationLog retrieves a set of all emitted events +// with "Hello world!" name from the provided ApplicationLog. +func HelloWorldEventsFromApplicationLog(log *result.ApplicationLog) ([]*HelloWorldEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*HelloWorldEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "Hello world!" { + continue + } + event := new(HelloWorldEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize HelloWorldEvent from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to HelloWorldEvent and +// returns an error if so. +func (e *HelloWorldEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 1 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.Args, err = func (item stackitem.Item) ([]any, error) { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return nil, errors.New("not an array") + } + res := make([]any, len(arr)) + for i := range res { + res[i], err = arr[i].Value(), error(nil) + if err != nil { + return nil, fmt.Errorf("item %d: %w", i, err) + } + } + return res, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field Args: %w", err) + } + + return nil +} diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 14793c8ad..4ee702626 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -110,7 +110,7 @@ type codegen struct { docIndex map[string]int // emittedEvents contains all events emitted by the contract. - emittedEvents map[string][][]string + emittedEvents map[string][]EmittedEventInfo // invokedContracts contains invoked methods of other contracts. invokedContracts map[util.Uint160][]string @@ -2269,7 +2269,7 @@ func newCodegen(info *buildInfo, pkg *packages.Package) *codegen { initEndOffset: -1, deployEndOffset: -1, - emittedEvents: make(map[string][][]string), + emittedEvents: make(map[string][]EmittedEventInfo), invokedContracts: make(map[util.Uint160][]string), sequencePoints: make(map[string][]DebugSeqPoint), } diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 60a45525c..aec697006 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -309,6 +309,22 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { if len(di.NamedTypes) > 0 { cfg.NamedTypes = di.NamedTypes } + if len(di.EmittedEvents) > 0 { + for eventName, eventUsages := range di.EmittedEvents { + for typeName, extType := range eventUsages[0].ExtTypes { + cfg.NamedTypes[typeName] = extType + } + for _, p := range eventUsages[0].Params { + pname := eventName + "." + p.Name + if p.RealType.TypeName != "" { + cfg.Overrides[pname] = p.RealType + } + if p.ExtendedType != nil { + cfg.Types[pname] = *p.ExtendedType + } + } + } + } data, err := yaml.Marshal(&cfg) if err != nil { return nil, fmt.Errorf("can't marshal bindings configuration: %w", err) @@ -366,24 +382,23 @@ func CreateManifest(di *DebugInfo, o *Options) (*manifest.Manifest, error) { } if !o.NoEventsCheck { for name := range di.EmittedEvents { - ev := m.ABI.GetEvent(name) - if ev == nil { + expected := m.ABI.GetEvent(name) + if expected == nil { return nil, fmt.Errorf("event '%s' is emitted but not specified in manifest", name) } - argsList := di.EmittedEvents[name] - for i := range argsList { - if len(argsList[i]) != len(ev.Parameters) { + for _, emitted := range di.EmittedEvents[name] { + if len(emitted.Params) != len(expected.Parameters) { return nil, fmt.Errorf("event '%s' should have %d parameters but has %d", - name, len(ev.Parameters), len(argsList[i])) + name, len(expected.Parameters), len(emitted.Params)) } - for j := range ev.Parameters { - if ev.Parameters[j].Type == smartcontract.AnyType { + for j := range expected.Parameters { + if expected.Parameters[j].Type == smartcontract.AnyType { continue } - expected := ev.Parameters[j].Type.String() - if argsList[i][j] != expected { + expectedT := expected.Parameters[j].Type + if emitted.Params[j].TypeSC != expectedT { return nil, fmt.Errorf("event '%s' should have '%s' as type of %d parameter, "+ - "got: %s", name, expected, j+1, argsList[i][j]) + "got: %s", name, expectedT, j+1, emitted.Params[j].TypeSC) } } } diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 82e2e49f7..88fcbb119 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -29,9 +29,14 @@ type DebugInfo struct { // NamedTypes are exported structured types that have some name (even // if the original structure doesn't) and a number of internal fields. NamedTypes map[string]binding.ExtendedType `json:"-"` - Events []EventDebugInfo `json:"events"` - // EmittedEvents contains events occurring in code. - EmittedEvents map[string][][]string `json:"-"` + // Events are the events that contract is allowed to emit and that have to + // be presented in the resulting contract manifest and debug info file. + Events []EventDebugInfo `json:"events"` + // EmittedEvents contains events occurring in code, i.e. events emitted + // via runtime.Notify(...) call in the contract code if they have constant + // names and doesn't have ellipsis arguments. EmittedEvents are not related + // to the debug info and are aimed to serve bindings generation. + EmittedEvents map[string][]EmittedEventInfo `json:"-"` // InvokedContracts contains foreign contract invocations. InvokedContracts map[util.Uint160][]string `json:"-"` // StaticVariables contains a list of static variable names and types. @@ -112,6 +117,14 @@ type DebugParam struct { TypeSC smartcontract.ParamType `json:"-"` } +// EmittedEventInfo describes information about single emitted event got from +// the contract code. It has the map of extended types used as the parameters to +// runtime.Notify(...) call (if any) and the parameters info itself. +type EmittedEventInfo struct { + ExtTypes map[string]binding.ExtendedType + Params []DebugParam +} + func (c *codegen) saveSequencePoint(n ast.Node) { name := "init" if c.scope != nil { diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 1653d8638..b6cd03d3b 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -7,6 +7,7 @@ import ( "go/types" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/binding" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -172,11 +173,21 @@ func (c *codegen) processNotify(f *funcScope, args []ast.Expr, hasEllipsis bool) return nil } - params := make([]string, 0, len(args[1:])) + params := make([]DebugParam, 0, len(args[1:])) vParams := make([]*stackitem.Type, 0, len(args[1:])) + // extMap holds the extended parameter types used for the given event call. + // It will be unified with the common extMap later during bindings config + // generation. + extMap := make(map[string]binding.ExtendedType) for _, p := range args[1:] { - st, vt, _, _ := c.scAndVMTypeFromExpr(p, nil) - params = append(params, st.String()) + st, vt, over, extT := c.scAndVMTypeFromExpr(p, extMap) + params = append(params, DebugParam{ + Name: "", // Parameter name will be filled in several lines below if the corresponding event exists in the buildinfo.options. + Type: vt.String(), + RealType: over, + ExtendedType: extT, + TypeSC: st, + }) vParams = append(vParams, &vt) } @@ -187,36 +198,43 @@ func (c *codegen) processNotify(f *funcScope, args []ast.Expr, hasEllipsis bool) return nil } var eventFound bool - if c.buildInfo.options != nil && c.buildInfo.options.ContractEvents != nil && !c.buildInfo.options.NoEventsCheck { + if c.buildInfo.options != nil && c.buildInfo.options.ContractEvents != nil { for _, e := range c.buildInfo.options.ContractEvents { if e.Name == name && len(e.Parameters) == len(vParams) { eventFound = true for i, scParam := range e.Parameters { - expectedType := scParam.Type.ConvertToStackitemType() - // No need to cast if the desired type is unknown. - if expectedType == stackitem.AnyT || - // Do not cast if desired type is Interop, the actual type is likely to be Any, leave the resolving to runtime.Notify. - expectedType == stackitem.InteropT || - // No need to cast if actual parameter type matches the desired one. - *vParams[i] == expectedType || - // expectedType doesn't contain Buffer anyway, but if actual variable type is Buffer, - // then runtime.Notify will convert it to ByteArray automatically, thus no need to emit conversion code. - (*vParams[i] == stackitem.BufferT && expectedType == stackitem.ByteArrayT) { - vParams[i] = nil - } else { - // For other cases the conversion code will be emitted using vParams... - vParams[i] = &expectedType - // ...thus, update emitted notification info in advance. - params[i] = scParam.Type.String() + params[i].Name = scParam.Name + if !c.buildInfo.options.NoEventsCheck { + expectedType := scParam.Type.ConvertToStackitemType() + // No need to cast if the desired type is unknown. + if expectedType == stackitem.AnyT || + // Do not cast if desired type is Interop, the actual type is likely to be Any, leave the resolving to runtime.Notify. + expectedType == stackitem.InteropT || + // No need to cast if actual parameter type matches the desired one. + *vParams[i] == expectedType || + // expectedType doesn't contain Buffer anyway, but if actual variable type is Buffer, + // then runtime.Notify will convert it to ByteArray automatically, thus no need to emit conversion code. + (*vParams[i] == stackitem.BufferT && expectedType == stackitem.ByteArrayT) { + vParams[i] = nil + } else { + // For other cases the conversion code will be emitted using vParams... + vParams[i] = &expectedType + // ...thus, update emitted notification info in advance. + params[i].Type = scParam.Type.String() + params[i].TypeSC = scParam.Type + } } } } } } - c.emittedEvents[name] = append(c.emittedEvents[name], params) + c.emittedEvents[name] = append(c.emittedEvents[name], EmittedEventInfo{ + ExtTypes: extMap, + Params: params, + }) // Do not enforce perfect expected/actual events match on this step, the final // check wil be performed after compilation if --no-events option is off. - if eventFound { + if eventFound && !c.buildInfo.options.NoEventsCheck { return vParams } return nil diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index 3f9bca863..bf3e3d9a1 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -62,6 +62,7 @@ type ( // smartcontract. The map key has one of the following forms: // - `methodName` for method return value; // - `mathodName.paramName` for method's parameter value. + // - `eventName.paramName` for event's parameter value. Types map[string]ExtendedType `yaml:"types,omitempty"` Output io.Writer `yaml:"-"` } diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index 58ed76240..973264c78 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -5,6 +5,7 @@ import ( "sort" "strings" "text/template" + "unicode" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/binding" @@ -17,6 +18,15 @@ import ( // start and ends with a new line. On adding new block of code to the template, please, // ensure that this block has new line at the start and in the end of the block. const ( + eventDefinition = `{{ define "EVENT" }} +// {{.Name}}Event represents "{{.ManifestName}}" event emitted by the contract. +type {{.Name}}Event struct { + {{- range $index, $arg := .Parameters}} + {{toPascalCase .Name}} {{.Type}} + {{- end}} +} +{{ end }}` + safemethodDefinition = `{{ define "SAFEMETHOD" }} // {{.Name}} {{.Comment}} func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}} @@ -118,6 +128,7 @@ type {{toTypeName $name}} struct { {{- end}} } {{end}} +{{- range $e := .CustomEvents }}{{template "EVENT" $e }}{{ end -}} {{- if .HasReader}} // Invoker is used by ContractReader to call various safe methods. type Invoker interface { @@ -249,9 +260,65 @@ func (res *{{toTypeName $name}}) FromStackItem(item stackitem.Item) error { {{- end}} return nil } +{{ end -}} +{{- range $e := .CustomEvents }} +// {{$e.Name}}EventsFromApplicationLog retrieves a set of all emitted events +// with "{{$e.ManifestName}}" name from the provided ApplicationLog. +func {{$e.Name}}EventsFromApplicationLog(log *result.ApplicationLog) ([]*{{$e.Name}}Event, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*{{$e.Name}}Event + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "{{$e.ManifestName}}" { + continue + } + event := new({{$e.Name}}Event) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize {{$e.Name}}Event from stackitem (execution %d, event %d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided stackitem.Array to {{$e.Name}}Event and +// returns an error if so. +func (e *{{$e.Name}}Event) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != {{len $e.Parameters}} { + return errors.New("wrong number of structure elements") + } + + {{if len $e.Parameters}}var ( + index = -1 + err error + ) + {{- range $p := $e.Parameters}} + index++ + e.{{toPascalCase .Name}}, err = {{etTypeConverter .ExtType "arr[index]"}} + if err != nil { + return fmt.Errorf("field {{toPascalCase .Name}}: %w", err) + } +{{end}} +{{- end}} + return nil +} {{end -}}` srcTmpl = bindingDefinition + + eventDefinition + safemethodDefinition + methodDefinition ) @@ -260,8 +327,9 @@ type ( ContractTmpl struct { binding.ContractTmpl - SafeMethods []SafeMethodTmpl - NamedTypes map[string]binding.ExtendedType + SafeMethods []SafeMethodTmpl + CustomEvents []CustomEventTemplate + NamedTypes map[string]binding.ExtendedType IsNep11D bool IsNep11ND bool @@ -278,6 +346,25 @@ type ( ItemTo string ExtendedReturn binding.ExtendedType } + + CustomEventTemplate struct { + // Name is the event's name that will be used as the event structure name in + // the resulting RPC binding. It is a valid go structure name and may differ + // from ManifestName. + Name string + // ManifestName is the event's name declared in the contract manifest. + // It may contain any UTF8 character. + ManifestName string + Parameters []EventParamTmpl + } + + EventParamTmpl struct { + binding.ParamTmpl + + // ExtType holds the event parameter's type information provided by Manifest, + // i.e. simple types only. + ExtType binding.ExtendedType + } ) // NewConfig initializes and returns a new config instance. @@ -328,7 +415,7 @@ func Generate(cfg binding.Config) error { } ctr.ContractTmpl = binding.TemplateFromManifest(cfg, scTypeToGo) - ctr = scTemplateToRPC(cfg, ctr, imports) + ctr = scTemplateToRPC(cfg, ctr, imports, scTypeToGo) ctr.NamedTypes = cfg.NamedTypes var srcTemplate = template.Must(template.New("generate").Funcs(template.FuncMap{ @@ -338,8 +425,9 @@ func Generate(cfg binding.Config) error { r, _ := extendedTypeToGo(et, ctr.NamedTypes) return r }, - "toTypeName": toTypeName, - "cutPointer": cutPointer, + "toTypeName": toTypeName, + "cutPointer": cutPointer, + "toPascalCase": toPascalCase, }).Parse(srcTmpl)) return srcTemplate.Execute(cfg.Output, ctr) @@ -533,7 +621,7 @@ func scTypeToGo(name string, typ smartcontract.ParamType, cfg *binding.Config) ( return extendedTypeToGo(et, cfg.NamedTypes) } -func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]struct{}) ContractTmpl { +func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]struct{}, scTypeConverter func(string, smartcontract.ParamType, *binding.Config) (string, string)) ContractTmpl { for i := range ctr.Imports { imports[ctr.Imports[i]] = struct{}{} } @@ -564,6 +652,51 @@ func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]st if len(cfg.NamedTypes) > 0 { imports["errors"] = struct{}{} } + for _, abiEvent := range cfg.Manifest.ABI.Events { + eTmp := CustomEventTemplate{ + // TODO: proper event name is better to be set right into config binding in normal form. + Name: toPascalCase(abiEvent.Name), + ManifestName: abiEvent.Name, + } + var varnames = make(map[string]bool) + for i := range abiEvent.Parameters { + name := abiEvent.Parameters[i].Name + fullPName := abiEvent.Name + "." + name + typeStr, pkg := scTypeConverter(fullPName, abiEvent.Parameters[i].Type, &cfg) + if pkg != "" { + imports[pkg] = struct{}{} + } + for varnames[name] { + name = name + "_" + } + varnames[name] = true + + var ( + extType binding.ExtendedType + ok bool + ) + if extType, ok = cfg.Types[fullPName]; !ok { + extType = binding.ExtendedType{ + Base: abiEvent.Parameters[i].Type, + } + } + eTmp.Parameters = append(eTmp.Parameters, EventParamTmpl{ + ParamTmpl: binding.ParamTmpl{ + Name: name, + Type: typeStr, + }, + ExtType: extType, + }) + } + ctr.CustomEvents = append(ctr.CustomEvents, eTmp) + } + + if len(ctr.CustomEvents) > 0 { + imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{} + imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} + imports["fmt"] = struct{}{} + imports["errors"] = struct{}{} + } for i := range ctr.SafeMethods { switch ctr.SafeMethods[i].ReturnType { @@ -693,3 +826,32 @@ func toTypeName(s string) string { func addIndent(str string, ind string) string { return strings.ReplaceAll(str, "\n", "\n"+ind) } + +// toPascalCase removes all non-unicode characters from the provided string and +// converts it to pascal case using space as delimiter. +func toPascalCase(s string) string { + var res string + ss := strings.Split(s, " ") + for i := range ss { // TODO: use DecodeRuneInString instead. + var word string + for _, ch := range ss[i] { + var ok bool + if len(res) == 0 && len(word) == 0 { + ok = unicode.IsLetter(ch) + } else { + ok = unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' + } + if ok { + word += string(ch) + } + } + if len(word) > 0 { + res += upperFirst(word) + } + } + return res +} + +func upperFirst(s string) string { + return strings.ToUpper(s[0:1]) + s[1:] +}