cli: fetch extended evet types from contract config

The user should specify it via parameter's `extendedtype` field and
via upper-level `namedtypes` field of the contract configuration YAML.

Also, as we have proper event structure source, make the `--guess-eventtype`
compilation option and make event types guess optional.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2023-05-08 21:58:28 +03:00
parent 194639bb15
commit e2580187a1
8 changed files with 216 additions and 36 deletions

View file

@ -24,6 +24,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "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/rpcclient/management"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "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/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -125,7 +126,7 @@ func NewCommands() []cli.Command {
{ {
Name: "compile", Name: "compile",
Usage: "compile a smart contract to a .nef file", 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 Description: `Compiles given smart contract to a .nef file and emits other associated
information (manifest, bindings configuration, debug information files) if information (manifest, bindings configuration, debug information files) if
asked to. If none of --out, --manifest, --config, --bindings flags are specified, asked to. If none of --out, --manifest, --config, --bindings flags are specified,
@ -171,6 +172,10 @@ func NewCommands() []cli.Command {
Name: "no-permissions", Name: "no-permissions",
Usage: "do not check if invoked contracts are allowed in manifest", 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{ cli.StringFlag{
Name: "bindings", Name: "bindings",
Usage: "output file for smart-contract bindings configuration", Usage: "output file for smart-contract bindings configuration",
@ -352,17 +357,19 @@ func initSmartContract(ctx *cli.Context) error {
SourceURL: "http://example.com/", SourceURL: "http://example.com/",
SupportedStandards: []string{}, SupportedStandards: []string{},
SafeMethods: []string{}, SafeMethods: []string{},
Events: []manifest.Event{ Events: []compiler.HybridEvent{
{ {
Name: "Hello world!", Name: "Hello world!",
Parameters: []manifest.Parameter{ Parameters: []compiler.HybridParameter{
{ {
Parameter: manifest.Parameter{
Name: "args", Name: "args",
Type: smartcontract.ArrayType, Type: smartcontract.ArrayType,
}, },
}, },
}, },
}, },
},
Permissions: []permission{permission(*manifest.NewPermission(manifest.PermissionWildcard))}, Permissions: []permission{permission(*manifest.NewPermission(manifest.PermissionWildcard))},
} }
b, err := yaml.Marshal(m) b, err := yaml.Marshal(m)
@ -447,6 +454,8 @@ func contractCompile(ctx *cli.Context) error {
NoStandardCheck: ctx.Bool("no-standards"), NoStandardCheck: ctx.Bool("no-standards"),
NoEventsCheck: ctx.Bool("no-events"), NoEventsCheck: ctx.Bool("no-events"),
NoPermissionsCheck: ctx.Bool("no-permissions"), NoPermissionsCheck: ctx.Bool("no-permissions"),
GuessEventTypes: ctx.Bool("guess-eventtypes"),
} }
if len(confFile) != 0 { if len(confFile) != 0 {
@ -457,6 +466,7 @@ func contractCompile(ctx *cli.Context) error {
o.Name = conf.Name o.Name = conf.Name
o.SourceURL = conf.SourceURL o.SourceURL = conf.SourceURL
o.ContractEvents = conf.Events o.ContractEvents = conf.Events
o.DeclaredNamedTypes = conf.NamedTypes
o.ContractSupportedStandards = conf.SupportedStandards o.ContractSupportedStandards = conf.SupportedStandards
o.Permissions = make([]manifest.Permission, len(conf.Permissions)) o.Permissions = make([]manifest.Permission, len(conf.Permissions))
for i := range conf.Permissions { for i := range conf.Permissions {
@ -705,9 +715,10 @@ type ProjectConfig struct {
SourceURL string SourceURL string
SafeMethods []string SafeMethods []string
SupportedStandards []string SupportedStandards []string
Events []manifest.Event Events []compiler.HybridEvent
Permissions []permission 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 { func inspect(ctx *cli.Context) error {

View file

@ -472,10 +472,113 @@ and structures. Notice that structured types returned by methods can't be Null
at the moment (see #2795). 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 $ ./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 ## Smart contract examples
Some examples are provided in the [examples directory](../examples). For more Some examples are provided in the [examples directory](../examples). For more

View file

@ -76,6 +76,7 @@ func NewDeployTx(bc Ledger, name string, sender util.Uint160, r gio.Reader, conf
o.Name = conf.Name o.Name = conf.Name
o.SourceURL = conf.SourceURL o.SourceURL = conf.SourceURL
o.ContractEvents = conf.Events o.ContractEvents = conf.Events
o.DeclaredNamedTypes = conf.NamedTypes
o.ContractSupportedStandards = conf.SupportedStandards o.ContractSupportedStandards = conf.SupportedStandards
o.Permissions = make([]manifest.Permission, len(conf.Permissions)) o.Permissions = make([]manifest.Permission, len(conf.Permissions))
for i := range conf.Permissions { for i := range conf.Permissions {

View file

@ -52,14 +52,26 @@ type Options struct {
// This setting has effect only if manifest is emitted. // This setting has effect only if manifest is emitted.
NoPermissionsCheck bool 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 is a contract's name to be written to manifest.
Name string Name string
// SourceURL is a contract's source URL to be written to manifest. // SourceURL is a contract's source URL to be written to manifest.
SourceURL string SourceURL string
// Runtime notifications. // Runtime notifications declared in the contract configuration file.
ContractEvents []manifest.Event 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. // The list of standards supported by the contract.
ContractSupportedStandards []string ContractSupportedStandards []string
@ -78,6 +90,23 @@ type Options struct {
BindingsFile string 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 { type buildInfo struct {
config *packages.Config config *packages.Config
program []*packages.Package program []*packages.Package
@ -309,22 +338,46 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
if len(di.NamedTypes) > 0 { if len(di.NamedTypes) > 0 {
cfg.NamedTypes = di.NamedTypes cfg.NamedTypes = di.NamedTypes
} }
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
}
}
}
if o.GuessEventTypes {
if len(di.EmittedEvents) > 0 { if len(di.EmittedEvents) > 0 {
for eventName, eventUsages := range di.EmittedEvents { 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 { for typeName, extType := range eventUsages[0].ExtTypes {
if _, ok := cfg.NamedTypes[typeName]; !ok {
cfg.NamedTypes[typeName] = extType cfg.NamedTypes[typeName] = extType
} }
}
for _, p := range eventUsages[0].Params { for _, p := range eventUsages[0].Params {
// TODO: prettify notification name in-place.
pname := eventName + "." + p.Name pname := eventName + "." + p.Name
if p.RealType.TypeName != "" { if p.RealType.TypeName != "" {
if _, ok := cfg.Overrides[pname]; !ok {
cfg.Overrides[pname] = p.RealType cfg.Overrides[pname] = p.RealType
} }
}
if p.ExtendedType != nil { if p.ExtendedType != nil {
if _, ok := cfg.Types[pname]; !ok {
cfg.Types[pname] = *p.ExtendedType cfg.Types[pname] = *p.ExtendedType
} }
} }
} }
} }
}
}
data, err := yaml.Marshal(&cfg) data, err := yaml.Marshal(&cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't marshal bindings configuration: %w", err) return nil, fmt.Errorf("can't marshal bindings configuration: %w", err)

View file

@ -159,16 +159,16 @@ func TestEventWarnings(t *testing.T) {
}) })
t.Run("wrong parameter number", func(t *testing.T) { t.Run("wrong parameter number", func(t *testing.T) {
_, err = compiler.CreateManifest(di, &compiler.Options{ _, err = compiler.CreateManifest(di, &compiler.Options{
ContractEvents: []manifest.Event{{Name: "Event"}}, ContractEvents: []compiler.HybridEvent{{Name: "Event"}},
Name: "payable", Name: "payable",
}) })
require.Error(t, err) require.Error(t, err)
}) })
t.Run("wrong parameter type", func(t *testing.T) { t.Run("wrong parameter type", func(t *testing.T) {
_, err = compiler.CreateManifest(di, &compiler.Options{ _, err = compiler.CreateManifest(di, &compiler.Options{
ContractEvents: []manifest.Event{{ ContractEvents: []compiler.HybridEvent{{
Name: "Event", Name: "Event",
Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.StringType)}, Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.StringType)}},
}}, }},
Name: "payable", Name: "payable",
}) })
@ -176,9 +176,9 @@ func TestEventWarnings(t *testing.T) {
}) })
t.Run("any parameter type", func(t *testing.T) { t.Run("any parameter type", func(t *testing.T) {
_, err = compiler.CreateManifest(di, &compiler.Options{ _, err = compiler.CreateManifest(di, &compiler.Options{
ContractEvents: []manifest.Event{{ ContractEvents: []compiler.HybridEvent{{
Name: "Event", Name: "Event",
Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.AnyType)}, Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.AnyType)}},
}}, }},
Name: "payable", Name: "payable",
}) })
@ -186,9 +186,9 @@ func TestEventWarnings(t *testing.T) {
}) })
t.Run("good", func(t *testing.T) { t.Run("good", func(t *testing.T) {
_, err = compiler.CreateManifest(di, &compiler.Options{ _, err = compiler.CreateManifest(di, &compiler.Options{
ContractEvents: []manifest.Event{{ ContractEvents: []compiler.HybridEvent{{
Name: "Event", Name: "Event",
Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.IntegerType)}, Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.IntegerType)}},
}}, }},
Name: "payable", Name: "payable",
}) })
@ -224,7 +224,7 @@ func TestEventWarnings(t *testing.T) {
require.Error(t, err) require.Error(t, err)
_, err = compiler.CreateManifest(di, &compiler.Options{ _, err = compiler.CreateManifest(di, &compiler.Options{
ContractEvents: []manifest.Event{{Name: "Event"}}, ContractEvents: []compiler.HybridEvent{{Name: "Event"}},
Name: "eventTest", Name: "eventTest",
}) })
require.NoError(t, err) require.NoError(t, err)
@ -242,9 +242,9 @@ func TestEventWarnings(t *testing.T) {
_, err = compiler.CreateManifest(di, &compiler.Options{ _, err = compiler.CreateManifest(di, &compiler.Options{
Name: "eventTest", Name: "eventTest",
ContractEvents: []manifest.Event{{ ContractEvents: []compiler.HybridEvent{{
Name: "Event", Name: "Event",
Parameters: []manifest.Parameter{manifest.NewParameter("number", smartcontract.IntegerType)}, Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.IntegerType)}},
}}, }},
}) })
require.NoError(t, err) require.NoError(t, err)
@ -260,7 +260,7 @@ func TestNotifyInVerify(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
src := fmt.Sprintf(srcTmpl, name) src := fmt.Sprintf(srcTmpl, name)
_, _, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), _, _, 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) require.Error(t, err)
t.Run("suppress", func(t *testing.T) { t.Run("suppress", func(t *testing.T) {

View file

@ -593,9 +593,20 @@ func (di *DebugInfo) ConvertToManifest(o *Options) (*manifest.Manifest, error) {
if o.ContractSupportedStandards != nil { if o.ContractSupportedStandards != nil {
result.SupportedStandards = o.ContractSupportedStandards 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{ result.ABI = manifest.ABI{
Methods: methods, Methods: methods,
Events: o.ContractEvents, Events: events,
} }
if result.ABI.Events == nil { if result.ABI.Events == nil {
result.ABI.Events = make([]manifest.Event, 0) result.ABI.Events = make([]manifest.Event, 0)

View file

@ -542,13 +542,13 @@ func TestForcedNotifyArgumentsConversion(t *testing.T) {
if count != len(expectedVMParamTypes) { if count != len(expectedVMParamTypes) {
t.Fatalf("parameters count mismatch: %d vs %d", 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)) vmParams := make([]stackitem.Item, len(expectedVMParamTypes))
for i := range scParams { for i := range scParams {
scParams[i] = manifest.Parameter{ scParams[i] = compiler.HybridParameter{Parameter: manifest.Parameter{
Name: strconv.Itoa(i), Name: strconv.Itoa(i),
Type: targetSCParamTypes[i], Type: targetSCParamTypes[i],
} }}
defaultValue := stackitem.NewBigInteger(big.NewInt(int64(i))) defaultValue := stackitem.NewBigInteger(big.NewInt(int64(i)))
var ( var (
val stackitem.Item val stackitem.Item
@ -564,7 +564,7 @@ func TestForcedNotifyArgumentsConversion(t *testing.T) {
} }
ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{ ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{
Name: "Helper", Name: "Helper",
ContractEvents: []manifest.Event{ ContractEvents: []compiler.HybridEvent{
{ {
Name: methodWithoutEllipsis, Name: methodWithoutEllipsis,
Parameters: scParams, Parameters: scParams,

View file

@ -60,6 +60,7 @@ func CompileFile(t testing.TB, sender util.Uint160, srcPath string, configPath s
o := &compiler.Options{} o := &compiler.Options{}
o.Name = conf.Name o.Name = conf.Name
o.ContractEvents = conf.Events o.ContractEvents = conf.Events
o.DeclaredNamedTypes = conf.NamedTypes
o.ContractSupportedStandards = conf.SupportedStandards o.ContractSupportedStandards = conf.SupportedStandards
o.Permissions = make([]manifest.Permission, len(conf.Permissions)) o.Permissions = make([]manifest.Permission, len(conf.Permissions))
for i := range conf.Permissions { for i := range conf.Permissions {