diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 5a2af4685..38dea5f13 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/binding" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" @@ -125,7 +126,7 @@ func NewCommands() []cli.Command { { Name: "compile", Usage: "compile a smart contract to a .nef file", - UsageText: "neo-go contract compile -i path [-o nef] [-v] [-d] [-m manifest] [-c yaml] [--bindings file] [--no-standards] [--no-events] [--no-permissions]", + UsageText: "neo-go contract compile -i path [-o nef] [-v] [-d] [-m manifest] [-c yaml] [--bindings file] [--no-standards] [--no-events] [--no-permissions] [--guess-eventtypes]", Description: `Compiles given smart contract to a .nef file and emits other associated information (manifest, bindings configuration, debug information files) if asked to. If none of --out, --manifest, --config, --bindings flags are specified, @@ -171,6 +172,10 @@ func NewCommands() []cli.Command { Name: "no-permissions", Usage: "do not check if invoked contracts are allowed in manifest", }, + cli.BoolFlag{ + Name: "guess-eventtypes", + Usage: "guess event types for smart-contract bindings configuration from the code usages", + }, cli.StringFlag{ Name: "bindings", Usage: "output file for smart-contract bindings configuration", @@ -352,13 +357,15 @@ func initSmartContract(ctx *cli.Context) error { SourceURL: "http://example.com/", SupportedStandards: []string{}, SafeMethods: []string{}, - Events: []manifest.Event{ + Events: []compiler.HybridEvent{ { Name: "Hello world!", - Parameters: []manifest.Parameter{ + Parameters: []compiler.HybridParameter{ { - Name: "args", - Type: smartcontract.ArrayType, + Parameter: manifest.Parameter{ + Name: "args", + Type: smartcontract.ArrayType, + }, }, }, }, @@ -447,6 +454,8 @@ func contractCompile(ctx *cli.Context) error { NoStandardCheck: ctx.Bool("no-standards"), NoEventsCheck: ctx.Bool("no-events"), NoPermissionsCheck: ctx.Bool("no-permissions"), + + GuessEventTypes: ctx.Bool("guess-eventtypes"), } if len(confFile) != 0 { @@ -457,6 +466,7 @@ func contractCompile(ctx *cli.Context) error { o.Name = conf.Name o.SourceURL = conf.SourceURL o.ContractEvents = conf.Events + o.DeclaredNamedTypes = conf.NamedTypes o.ContractSupportedStandards = conf.SupportedStandards o.Permissions = make([]manifest.Permission, len(conf.Permissions)) for i := range conf.Permissions { @@ -705,9 +715,10 @@ type ProjectConfig struct { SourceURL string SafeMethods []string SupportedStandards []string - Events []manifest.Event + Events []compiler.HybridEvent Permissions []permission - Overloads map[string]string `yaml:"overloads,omitempty"` + Overloads map[string]string `yaml:"overloads,omitempty"` + NamedTypes map[string]binding.ExtendedType `yaml:"namedtypes,omitempty"` } func inspect(ctx *cli.Context) error { diff --git a/docs/compiler.md b/docs/compiler.md index d4d17efb4..36c37409a 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -472,10 +472,113 @@ and structures. Notice that structured types returned by methods can't be Null at the moment (see #2795). ``` -$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --manifest manifest.json --bindings contract.bindings.yml +$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --manifest manifest.json --bindings contract.bindings.yml --guess-eventtypes $ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --config contract.bindings.yml --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176 ``` +Contract-specific RPC-bindings generated by "generate-rpcwrapper" command include +structure wrappers for each event declared in the contract manifest as far as the +set of helpers that allow to retrieve emitted event from the application log or +from stackitem. By default, event wrappers builder use event structure that was +described in the manifest. Since the type data available in the manifest is +limited, in some cases the resulting generated event structure may use generic +go types. Go contracts can make use of additional type data from bindings +configuration file generated during compilation. Like for any other contract +types, this can cover arrays, maps and structures. To reach the maximum +resemblance between the emitted events and the generated event wrappers, we +recommend either to fill in the extended events type information in the contract +configuration file before the compilation or to use `--guess-eventtypes` +compilation option. + +If using `--guess-eventtypes` compilation option, event parameter types will be +guessed from the arguments of `runtime.Notify` calls for each emitted event. If +multiple calls of `runtime.Notify` are found, then argument types will be checked +for matching (guessed types must be the same across the particular event usages). +After that, the extended types binding configuration will be generated according +to the emitted events parameter types. `--guess-eventtypes` compilation option +is able to recognize those events that has a constant name known at a compilation +time and do not include variadic arguments usage. Thus, use this option if your +contract suites these requirements. Otherwise, we recommend to manually specify +extended event parameter types information in the contract configuration file. + +Extended event parameter type information can be provided manually via contract +configuration file under the `events` section. Each event parameter specified in +this section may be supplied with additional parameter type information specified +under `extendedtype` subsection. The extended type information (`ExtendedType`) +has the following structure: + +| Field | Type | Required | Meaning | +|-------------|---------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| `base` | Any valid [NEP-14 parameter type](https://github.com/neo-project/proposals/blob/master/nep-14.mediawiki#parametertype) except `Void`. | Always required. | The base type of a parameter, e.g. `Array` for go structures and any nested arrays, `Map` for nested maps, `Hash160` for 160-bits integers, etc. | +| `name` | `string` | Required for structures, omitted for arrays, interfaces and maps. | Name of a structure that will be used in the resulting RPC binding. | +| `interface` | `string` | Required for `InteropInterface`-based types, currently `iterator` only is supported. | Underlying value of the `InteropInterface`. | +| `key` | Any simple [NEP-14 parameter type](https://github.com/neo-project/proposals/blob/master/nep-14.mediawiki#parametertype). | Required for `Map`-based types. | Key type for maps. | +| `value` | `ExtendedType`. | Required for iterators, arrays and maps. | Value type of iterators, arrays and maps. | +| `fields` | Array of `FieldExtendedType`. | Required for structures. | Ordered type data for structure fields. | + +The structure's field extended information (`FieldExtendedType`) has the following structure: + +| Field | Type | Required | Meaning | +|------------------------|----------------|------------------|-----------------------------------------------------------------------------| +| `field` | `string` | Always required. | Name of the structure field that will be used in the resulting RPC binding. | +| Inlined `ExtendedType` | `ExtendedType` | Always required. | The extended type information about structure field. | + + +Any named structures used in the `ExtendedType` description must be manually +specified in the contract configuration file under top-level `namedtypes` section +in the form of `map[string]ExtendedType`, where the map key is a name of the +described named structure that matches the one provided in the `name` field of +the event parameter's extended type. + +Here's the example of manually-created contract configuration file that uses +extended types for event parameters description: + +``` +name: "HelloWorld contract" +supportedstandards: [] +events: + - name: Some simple notification + parameters: + - name: intP + type: Integer + - name: boolP + type: Boolean + - name: stringP + type: String + - name: Structure notification + parameters: + - name: structure parameter + type: Array + extendedtype: + base: Array + name: transferData + - name: Map of structures notification + parameters: + - name: map parameter + type: Map + extendedtype: + base: Map + key: Integer + value: + base: Array + name: transferData + - name: Iterator notification + parameters: + - name: data + type: InteropInterface + extendedtype: + base: InteropInterface + interface: iterator +namedtypes: + transferData: + base: Array + fields: + - field: IntField + base: Integer + - field: BoolField + base: Boolean +``` + ## Smart contract examples Some examples are provided in the [examples directory](../examples). For more diff --git a/internal/testchain/transaction.go b/internal/testchain/transaction.go index 630902ac8..cce8dd49c 100644 --- a/internal/testchain/transaction.go +++ b/internal/testchain/transaction.go @@ -76,6 +76,7 @@ func NewDeployTx(bc Ledger, name string, sender util.Uint160, r gio.Reader, conf o.Name = conf.Name o.SourceURL = conf.SourceURL o.ContractEvents = conf.Events + o.DeclaredNamedTypes = conf.NamedTypes o.ContractSupportedStandards = conf.SupportedStandards o.Permissions = make([]manifest.Permission, len(conf.Permissions)) for i := range conf.Permissions { diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index aec697006..e44aead0a 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -52,14 +52,26 @@ type Options struct { // This setting has effect only if manifest is emitted. NoPermissionsCheck bool + // GuessEventTypes specifies if types of runtime notifications need to be guessed + // from the usage context. These types are used for RPC binding generation only and + // can be defined for events with name known at the compilation time and without + // variadic args usages. If some type is specified via config file, then the config's + // one is preferable. Currently, event's parameter type is defined from the first + // occurrence of event call. + GuessEventTypes bool + // Name is a contract's name to be written to manifest. Name string // SourceURL is a contract's source URL to be written to manifest. SourceURL string - // Runtime notifications. - ContractEvents []manifest.Event + // Runtime notifications declared in the contract configuration file. + ContractEvents []HybridEvent + + // DeclaredNamedTypes is the set of named types that were declared in the + // contract configuration type and are the part of manifest events. + DeclaredNamedTypes map[string]binding.ExtendedType // The list of standards supported by the contract. ContractSupportedStandards []string @@ -78,6 +90,23 @@ type Options struct { BindingsFile string } +// HybridEvent represents the description of event emitted by the contract squashed +// with extended event's parameters description. We have it as a separate type for +// the user's convenience. It is applied for the smart contract configuration file +// only. +type HybridEvent struct { + Name string `json:"name"` + Parameters []HybridParameter `json:"parameters"` +} + +// HybridParameter contains the manifest's event parameter description united with +// the extended type description for this parameter. It is applied for the smart +// contract configuration file only. +type HybridParameter struct { + manifest.Parameter `yaml:",inline"` + ExtendedType *binding.ExtendedType `yaml:"extendedtype,omitempty"` +} + type buildInfo struct { config *packages.Config program []*packages.Package @@ -309,18 +338,42 @@ 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 name, et := range o.DeclaredNamedTypes { + // TODO: handle name conflict + cfg.NamedTypes[name] = et + } + for _, e := range o.ContractEvents { + for _, p := range e.Parameters { + // TODO: proper imports handling during bindings generation (see utf8 example). + if p.ExtendedType != nil { + pName := e.Name + "." + p.Name + cfg.Types[pName] = *p.ExtendedType } - for _, p := range eventUsages[0].Params { - pname := eventName + "." + p.Name - if p.RealType.TypeName != "" { - cfg.Overrides[pname] = p.RealType + } + } + if o.GuessEventTypes { + if len(di.EmittedEvents) > 0 { + for eventName, eventUsages := range di.EmittedEvents { + // Take into account the first usage only. + // TODO: extend it to the rest of invocations. + for typeName, extType := range eventUsages[0].ExtTypes { + if _, ok := cfg.NamedTypes[typeName]; !ok { + cfg.NamedTypes[typeName] = extType + } } - if p.ExtendedType != nil { - cfg.Types[pname] = *p.ExtendedType + for _, p := range eventUsages[0].Params { + // TODO: prettify notification name in-place. + pname := eventName + "." + p.Name + if p.RealType.TypeName != "" { + if _, ok := cfg.Overrides[pname]; !ok { + cfg.Overrides[pname] = p.RealType + } + } + if p.ExtendedType != nil { + if _, ok := cfg.Types[pname]; !ok { + cfg.Types[pname] = *p.ExtendedType + } + } } } } diff --git a/pkg/compiler/compiler_test.go b/pkg/compiler/compiler_test.go index 8a9ab207a..288aaf3c7 100644 --- a/pkg/compiler/compiler_test.go +++ b/pkg/compiler/compiler_test.go @@ -159,16 +159,16 @@ func TestEventWarnings(t *testing.T) { }) t.Run("wrong parameter number", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ - ContractEvents: []manifest.Event{{Name: "Event"}}, + ContractEvents: []compiler.HybridEvent{{Name: "Event"}}, Name: "payable", }) require.Error(t, err) }) t.Run("wrong parameter type", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ - ContractEvents: []manifest.Event{{ + ContractEvents: []compiler.HybridEvent{{ Name: "Event", - Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.StringType)}, + Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.StringType)}}, }}, Name: "payable", }) @@ -176,9 +176,9 @@ func TestEventWarnings(t *testing.T) { }) t.Run("any parameter type", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ - ContractEvents: []manifest.Event{{ + ContractEvents: []compiler.HybridEvent{{ Name: "Event", - Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.AnyType)}, + Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.AnyType)}}, }}, Name: "payable", }) @@ -186,9 +186,9 @@ func TestEventWarnings(t *testing.T) { }) t.Run("good", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ - ContractEvents: []manifest.Event{{ + ContractEvents: []compiler.HybridEvent{{ Name: "Event", - Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.IntegerType)}, + Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.IntegerType)}}, }}, Name: "payable", }) @@ -224,7 +224,7 @@ func TestEventWarnings(t *testing.T) { require.Error(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{ - ContractEvents: []manifest.Event{{Name: "Event"}}, + ContractEvents: []compiler.HybridEvent{{Name: "Event"}}, Name: "eventTest", }) require.NoError(t, err) @@ -242,9 +242,9 @@ func TestEventWarnings(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ Name: "eventTest", - ContractEvents: []manifest.Event{{ + ContractEvents: []compiler.HybridEvent{{ Name: "Event", - Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.IntegerType)}, + Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.IntegerType)}}, }}, }) require.NoError(t, err) @@ -260,7 +260,7 @@ func TestNotifyInVerify(t *testing.T) { t.Run(name, func(t *testing.T) { src := fmt.Sprintf(srcTmpl, name) _, _, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), - &compiler.Options{ContractEvents: []manifest.Event{{Name: "Event"}}}) + &compiler.Options{ContractEvents: []compiler.HybridEvent{{Name: "Event"}}}) require.Error(t, err) t.Run("suppress", func(t *testing.T) { diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index c720fcbc5..0eb18bb5a 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -593,9 +593,20 @@ func (di *DebugInfo) ConvertToManifest(o *Options) (*manifest.Manifest, error) { if o.ContractSupportedStandards != nil { result.SupportedStandards = o.ContractSupportedStandards } + events := make([]manifest.Event, len(o.ContractEvents)) + for i, e := range o.ContractEvents { + params := make([]manifest.Parameter, len(e.Parameters)) + for j, p := range e.Parameters { + params[j] = p.Parameter + } + events[i] = manifest.Event{ + Name: o.ContractEvents[i].Name, + Parameters: params, + } + } result.ABI = manifest.ABI{ Methods: methods, - Events: o.ContractEvents, + Events: events, } if result.ABI.Events == nil { result.ABI.Events = make([]manifest.Event, 0) diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 1cd1c88f3..c8f77ce24 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -542,13 +542,13 @@ func TestForcedNotifyArgumentsConversion(t *testing.T) { if count != len(expectedVMParamTypes) { t.Fatalf("parameters count mismatch: %d vs %d", count, len(expectedVMParamTypes)) } - scParams := make([]manifest.Parameter, len(targetSCParamTypes)) + scParams := make([]compiler.HybridParameter, len(targetSCParamTypes)) vmParams := make([]stackitem.Item, len(expectedVMParamTypes)) for i := range scParams { - scParams[i] = manifest.Parameter{ + scParams[i] = compiler.HybridParameter{Parameter: manifest.Parameter{ Name: strconv.Itoa(i), Type: targetSCParamTypes[i], - } + }} defaultValue := stackitem.NewBigInteger(big.NewInt(int64(i))) var ( val stackitem.Item @@ -564,7 +564,7 @@ func TestForcedNotifyArgumentsConversion(t *testing.T) { } ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{ Name: "Helper", - ContractEvents: []manifest.Event{ + ContractEvents: []compiler.HybridEvent{ { Name: methodWithoutEllipsis, Parameters: scParams, diff --git a/pkg/neotest/compile.go b/pkg/neotest/compile.go index fee77c17e..70645c11c 100644 --- a/pkg/neotest/compile.go +++ b/pkg/neotest/compile.go @@ -60,6 +60,7 @@ func CompileFile(t testing.TB, sender util.Uint160, srcPath string, configPath s o := &compiler.Options{} o.Name = conf.Name o.ContractEvents = conf.Events + o.DeclaredNamedTypes = conf.NamedTypes o.ContractSupportedStandards = conf.SupportedStandards o.Permissions = make([]manifest.Permission, len(conf.Permissions)) for i := range conf.Permissions {