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:
parent
194639bb15
commit
e2580187a1
8 changed files with 216 additions and 36 deletions
|
@ -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 {
|
||||
|
|
105
docs/compiler.md
105
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue