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/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,17 +357,19 @@ 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{
{
Parameter: manifest.Parameter{
Name: "args",
Type: smartcontract.ArrayType,
},
},
},
},
},
Permissions: []permission{permission(*manifest.NewPermission(manifest.PermissionWildcard))},
}
b, err := yaml.Marshal(m)
@ -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"`
NamedTypes map[string]binding.ExtendedType `yaml:"namedtypes,omitempty"`
}
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).
```
$ ./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

View file

@ -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 {

View file

@ -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,22 +338,46 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
if len(di.NamedTypes) > 0 {
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 {
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
}
}
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
}
}
}
}
}
}
data, err := yaml.Marshal(&cfg)
if err != nil {
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) {
_, 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) {

View file

@ -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)

View file

@ -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,

View file

@ -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 {