Merge pull request #3008 from nspcc-dev/event-gen

Close #2891.
This commit is contained in:
Roman Khimov 2023-05-31 23:56:51 +03:00 committed by GitHub
commit 772e723e8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 5313 additions and 324 deletions

4
.gitignore vendored
View file

@ -7,9 +7,6 @@
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Added by CoZ developers
vendor/
bin/
@ -54,6 +51,7 @@ testdata/
!pkg/services/notary/testdata
!pkg/services/oracle/testdata
!pkg/smartcontract/testdata
!cli/smartcontract/testdata
pkg/vm/testdata/fuzz
!pkg/vm/testdata
!pkg/wallet/testdata

View file

@ -316,7 +316,6 @@ func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{invoker}
}
// Get invokes `+"`get`"+` method of contract.
func (c *ContractReader) Get() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "get"))
@ -324,6 +323,10 @@ func (c *ContractReader) Get() (*big.Int, error) {
`, string(data))
}
// rewriteExpectedOutputs denotes whether expected output files should be rewritten
// for TestGenerateRPCBindings and TestAssistedRPCBindings.
const rewriteExpectedOutputs = false
func TestGenerateRPCBindings(t *testing.T) {
tmpDir := t.TempDir()
app := cli.NewApp()
@ -341,10 +344,14 @@ func TestGenerateRPCBindings(t *testing.T) {
data, err := os.ReadFile(outFile)
require.NoError(t, err)
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
if rewriteExpectedOutputs {
require.NoError(t, os.WriteFile(good, data, os.ModePerm))
} else {
expected, err := os.ReadFile(good)
require.NoError(t, err)
expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
require.Equal(t, string(expected), string(data))
}
})
}
@ -363,6 +370,8 @@ func TestGenerateRPCBindings(t *testing.T) {
checkBinding(filepath.Join("testdata", "nonepiter", "iter.manifest.json"),
"0x00112233445566778899aabbccddeeff00112233",
filepath.Join("testdata", "nonepiter", "iter.go"))
require.False(t, rewriteExpectedOutputs)
}
func TestAssistedRPCBindings(t *testing.T) {
@ -370,18 +379,32 @@ func TestAssistedRPCBindings(t *testing.T) {
app := cli.NewApp()
app.Commands = NewCommands()
var checkBinding = func(source string) {
t.Run(source, func(t *testing.T) {
var checkBinding = func(source string, guessEventTypes bool, suffix ...string) {
testName := source
if len(suffix) != 0 {
testName += suffix[0]
}
t.Run(testName, func(t *testing.T) {
configFile := filepath.Join(source, "config.yml")
expectedFile := filepath.Join(source, "rpcbindings.out")
if len(suffix) != 0 {
configFile = filepath.Join(source, "config"+suffix[0]+".yml")
expectedFile = filepath.Join(source, "rpcbindings"+suffix[0]+".out")
}
manifestF := filepath.Join(tmpDir, "manifest.json")
bindingF := filepath.Join(tmpDir, "binding.yml")
nefF := filepath.Join(tmpDir, "out.nef")
require.NoError(t, app.Run([]string{"", "contract", "compile",
cmd := []string{"", "contract", "compile",
"--in", source,
"--config", filepath.Join(source, "config.yml"),
"--config", configFile,
"--manifest", manifestF,
"--bindings", bindingF,
"--out", nefF,
}))
}
if guessEventTypes {
cmd = append(cmd, "--guess-eventtypes")
}
require.NoError(t, app.Run(cmd))
outFile := filepath.Join(tmpDir, "out.go")
require.NoError(t, app.Run([]string{"", "contract", "generate-rpcwrapper",
"--config", bindingF,
@ -393,15 +416,24 @@ func TestAssistedRPCBindings(t *testing.T) {
data, err := os.ReadFile(outFile)
require.NoError(t, err)
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
expected, err := os.ReadFile(filepath.Join(source, "rpcbindings.out"))
if rewriteExpectedOutputs {
require.NoError(t, os.WriteFile(expectedFile, data, os.ModePerm))
} else {
expected, err := os.ReadFile(expectedFile)
require.NoError(t, err)
expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
require.Equal(t, string(expected), string(data))
}
})
}
checkBinding(filepath.Join("testdata", "types"))
checkBinding(filepath.Join("testdata", "structs"))
checkBinding(filepath.Join("testdata", "types"), false)
checkBinding(filepath.Join("testdata", "structs"), false)
checkBinding(filepath.Join("testdata", "notifications"), false)
checkBinding(filepath.Join("testdata", "notifications"), false, "_extended")
checkBinding(filepath.Join("testdata", "notifications"), true, "_guessed")
require.False(t, rewriteExpectedOutputs)
}
func TestGenerate_Errors(t *testing.T) {
@ -467,3 +499,55 @@ callflags:
"--config", cfgPath, "--out", "zzz")
})
}
func TestCompile_GuessEventTypes(t *testing.T) {
app := cli.NewApp()
app.Commands = NewCommands()
app.ExitErrHandler = func(*cli.Context, error) {}
checkError := func(t *testing.T, msg string, args ...string) {
// cli.ExitError doesn't implement wraping properly, so we check for an error message.
err := app.Run(args)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), msg), "got: %v", err)
}
check := func(t *testing.T, source string, expectedErrText string) {
tmpDir := t.TempDir()
configFile := filepath.Join(source, "invalid.yml")
manifestF := filepath.Join(tmpDir, "invalid.manifest.json")
bindingF := filepath.Join(tmpDir, "invalid.binding.yml")
nefF := filepath.Join(tmpDir, "invalid.out.nef")
cmd := []string{"", "contract", "compile",
"--in", source,
"--config", configFile,
"--manifest", manifestF,
"--bindings", bindingF,
"--out", nefF,
"--guess-eventtypes",
}
checkError(t, expectedErrText, cmd...)
}
t.Run("not declared in manifest", func(t *testing.T) {
check(t, filepath.Join("testdata", "invalid5"), "inconsistent usages of event `Non declared event`: not declared in the contract config")
})
t.Run("invalid number of params", func(t *testing.T) {
check(t, filepath.Join("testdata", "invalid6"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1")
})
/*
// TODO: this on is a controversial one. If event information is provided in the config file, then conversion code
// will be emitted by the compiler according to the parameter type provided via config. Thus, we can be sure that
// either event parameter has the type specified in the config file or the execution of the contract will fail.
// Thus, this testcase is always failing (no compilation error occures).
// Question: do we want to compare `RealType` of the emitted parameter with the one expected in the manifest?
t.Run("SC parameter type mismatch", func(t *testing.T) {
check(t, filepath.Join("testdata", "invalid7"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1")
})
*/
t.Run("extended types mismatch", func(t *testing.T) {
check(t, filepath.Join("testdata", "invalid8"), "inconsistent usages of event `SomeEvent`: extended type of param #0 mismatch")
})
t.Run("named types redeclare", func(t *testing.T) {
check(t, filepath.Join("testdata", "invalid9"), "configured declared named type intersects with the contract's one: `invalid9.NamedStruct`")
})
}

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

@ -44,4 +44,3 @@ func New(actor Actor) *Contract {
var nep17t = nep17.New(actor, Hash)
return &Contract{ContractReader{nep17t.TokenReader, actor}, nep17t.TokenWriter, actor}
}

View file

@ -0,0 +1,7 @@
package invalid5
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main() {
runtime.Notify("Non declared event")
}

View file

@ -0,0 +1 @@
name: Test undeclared event

View file

@ -0,0 +1,7 @@
package invalid6
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main() {
runtime.Notify("SomeEvent", "p1", "p2")
}

View file

@ -0,0 +1,6 @@
name: Test undeclared event
events:
- name: SomeEvent
parameters:
- name: p1
type: String

View file

@ -0,0 +1,7 @@
package invalid7
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main() {
runtime.Notify("SomeEvent", "p1", 5)
}

View file

@ -0,0 +1,8 @@
name: Test undeclared event
events:
- name: SomeEvent
parameters:
- name: p1
type: String
- name: p2
type: String

View file

@ -0,0 +1,17 @@
package invalid8
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
type SomeStruct1 struct {
Field1 int
}
type SomeStruct2 struct {
Field2 string
}
func Main() {
// Inconsistent event params usages (different named types throughout the usages).
runtime.Notify("SomeEvent", SomeStruct1{Field1: 123})
runtime.Notify("SomeEvent", SomeStruct2{Field2: "str"})
}

View file

@ -0,0 +1,6 @@
name: Test undeclared event
events:
- name: SomeEvent
parameters:
- name: p1
type: Array

View file

@ -0,0 +1,12 @@
package invalid9
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
type NamedStruct struct {
SomeInt int
}
func Main() NamedStruct {
runtime.Notify("SomeEvent", []interface{}{123})
return NamedStruct{SomeInt: 123}
}

View file

@ -0,0 +1,16 @@
name: Test undeclared event
events:
- name: SomeEvent
parameters:
- name: p1
type: Array
extendedtype:
base: Array
name: invalid9.NamedStruct
namedtypes:
invalid9.NamedStruct:
base: Array
name: invalid9.NamedStruct
fields:
- field: SomeInt
base: Integer

View file

@ -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"
@ -11,11 +13,26 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"math/big"
"unicode/utf8"
)
// 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}
// 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
@ -59,7 +76,6 @@ func New(actor Actor) *Contract {
return &Contract{ContractReader{nep11ndt.NonDivisibleReader, actor}, nep11ndt.BaseWriter, actor}
}
// Roots invokes `roots` method of contract.
func (c *ContractReader) Roots() (uuid.UUID, result.Iterator, error) {
return unwrap.SessionIterator(c.invoker.Call(Hash, "roots"))
@ -321,3 +337,169 @@ 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)
}
// SetAdminEventsFromApplicationLog retrieves a set of all emitted events
// with "SetAdmin" name from the provided [result.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 or
// returns an error if it's not possible to do to 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 [result.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 or
// returns an error if it's not possible to do to 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
}

View file

@ -2,17 +2,29 @@
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}
// 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
@ -56,7 +68,6 @@ func New(actor Actor) *Contract {
return &Contract{ContractReader{nep17t.TokenReader, actor}, nep17t.TokenWriter, actor}
}
// Cap invokes `cap` method of contract.
func (c *ContractReader) Cap() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "cap"))
@ -230,3 +241,93 @@ 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)
}
// OnMintEventsFromApplicationLog retrieves a set of all emitted events
// with "OnMint" name from the provided [result.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 or
// returns an error if it's not possible to do to 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
}

View file

@ -30,7 +30,6 @@ func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{invoker}
}
// Tokens invokes `tokens` method of contract.
func (c *ContractReader) Tokens() (uuid.UUID, result.Iterator, error) {
return unwrap.SessionIterator(c.invoker.Call(Hash, "tokens"))

View file

@ -0,0 +1,19 @@
name: "Notifications"
sourceurl: https://github.com/nspcc-dev/neo-go/
events:
- name: "! complicated name %$#"
parameters:
- name: ! complicated param @#$%
type: String
- name: "SomeMap"
parameters:
- name: m
type: Map
- name: "SomeStruct"
parameters:
- name: s
type: Struct
- name: "SomeArray"
parameters:
- name: a
type: Array

View file

@ -0,0 +1,47 @@
name: "Notifications"
sourceurl: https://github.com/nspcc-dev/neo-go/
events:
- name: "! complicated name %$#"
parameters:
- name: ! complicated param @#$%
type: String
- name: "SomeMap"
parameters:
- name: m
type: Map
extendedtype:
base: Map
key: Integer
value:
base: Map
key: String
value:
base: Array
value:
base: Hash160
- name: "SomeStruct"
parameters:
- name: s
type: Struct
extendedtype:
base: Struct
name: crazyStruct
- name: "SomeArray"
parameters:
- name: a
type: Array
extendedtype:
base: Array
value:
base: Array
value:
base: Integer
namedtypes:
crazyStruct:
base: Struct
name: crazyStruct
fields:
- field: I
base: Integer
- field: B
base: Boolean

View file

@ -0,0 +1,19 @@
name: "Notifications"
sourceurl: https://github.com/nspcc-dev/neo-go/
events:
- name: "! complicated name %$#"
parameters:
- name: ! complicated param @#$%
type: String
- name: "SomeMap"
parameters:
- name: m
type: Map
- name: "SomeStruct"
parameters:
- name: s
type: Struct
- name: "SomeArray"
parameters:
- name: a
type: Array

View file

@ -0,0 +1,25 @@
package structs
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
func Main() {
runtime.Notify("! complicated name %$#", "str1")
}
func CrazyMap() {
runtime.Notify("SomeMap", map[int][]map[string][]interop.Hash160{})
}
func Struct() {
runtime.Notify("SomeStruct", struct {
I int
B bool
}{I: 123, B: true})
}
func Array() {
runtime.Notify("SomeArray", [][]int{})
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
name: "Types"
sourceurl: https://github.com/nspcc-dev/neo-go/
safemethods: ["bool", "int", "bytes", "string", "any", "hash160", "hash256", "publicKey", "signature", "bools", "ints", "bytess", "strings", "hash160s", "hash256s", "publicKeys", "signatures", "aAAStrings", "maps", "crazyMaps"]
safemethods: ["bool", "int", "bytes", "string", "any", "hash160", "hash256", "publicKey", "signature", "bools", "ints", "bytess", "strings", "hash160s", "hash256s", "publicKeys", "signatures", "aAAStrings", "maps", "crazyMaps", "anyMaps"]

View file

@ -31,7 +31,6 @@ func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{invoker}
}
// AAAStrings invokes `aAAStrings` method of contract.
func (c *ContractReader) AAAStrings(s [][][]string) ([][][]string, error) {
return func (item stackitem.Item, err error) ([][][]string, error) {
@ -96,10 +95,38 @@ func (c *ContractReader) Any(a any) (any, error) {
if err != nil {
return nil, err
}
return item.Value(), nil
return item.Value(), error(nil)
} (unwrap.Item(c.invoker.Call(Hash, "any", a)))
}
// AnyMaps invokes `anyMaps` method of contract.
func (c *ContractReader) AnyMaps(m map[*big.Int]any) (map[*big.Int]any, error) {
return func (item stackitem.Item, err error) (map[*big.Int]any, error) {
if err != nil {
return nil, err
}
return func (item stackitem.Item) (map[*big.Int]any, error) {
m, ok := item.Value().([]stackitem.MapElement)
if !ok {
return nil, fmt.Errorf("%s is not a map", item.Type().String())
}
res := make(map[*big.Int]any)
for i := range m {
k, err := m[i].Key.TryInteger()
if err != nil {
return nil, fmt.Errorf("key %d: %w", i, err)
}
v, err := m[i].Value.Value(), error(nil)
if err != nil {
return nil, fmt.Errorf("value %d: %w", i, err)
}
res[k] = v
}
return res, nil
} (item)
} (unwrap.Item(c.invoker.Call(Hash, "anyMaps", m)))
}
// Bool invokes `bool` method of contract.
func (c *ContractReader) Bool(b bool) (bool, error) {
return unwrap.Bool(c.invoker.Call(Hash, "bool", b))

View file

@ -83,3 +83,7 @@ func Maps(m map[string]string) map[string]string {
func CrazyMaps(m map[int][]map[string][]interop.Hash160) map[int][]map[string][]interop.Hash160 {
return m
}
func AnyMaps(m map[int]any) map[int]any {
return m
}

View file

@ -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)
@ -30,7 +39,6 @@ func New(actor Actor) *Contract {
return &Contract{actor}
}
func scriptForVerify() ([]byte, error) {
return smartcontract.CreateCallWithAssertScript(Hash, "verify")
}
@ -68,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 [result.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 or
// returns an error if it's not possible to do to 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
}

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

@ -1,6 +1,7 @@
package events
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
@ -24,6 +25,11 @@ func NotifySomeMap(arg map[string]int) {
runtime.Notify("SomeMap", arg)
}
// NotifySomeCrazyMap emits notification with complicated Map.
func NotifySomeCrazyMap(arg map[int][]map[string][]interop.Hash160) {
runtime.Notify("SomeCrazyMap", arg)
}
// NotifySomeArray emits notification with Array.
func NotifySomeArray(arg []int) {
runtime.Notify("SomeArray", arg)

View file

@ -18,6 +18,10 @@ events:
parameters:
- name: m
type: Map
- name: SomeCrazyMap
parameters:
- name: m
type: Map
- name: SomeArray
parameters:
- name: a

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

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

View file

@ -18,6 +18,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding"
"github.com/nspcc-dev/neo-go/pkg/util"
"golang.org/x/tools/go/packages"
"gopkg.in/yaml.v3"
@ -52,14 +53,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 +91,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,6 +339,78 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
if len(di.NamedTypes) > 0 {
cfg.NamedTypes = di.NamedTypes
}
for name, et := range o.DeclaredNamedTypes {
if _, ok := cfg.NamedTypes[name]; ok {
return nil, fmt.Errorf("configured declared named type intersects with the contract's one: `%s`", name)
}
cfg.NamedTypes[name] = et
}
for _, e := range o.ContractEvents {
eStructName := rpcbinding.ToEventBindingName(e.Name)
for _, p := range e.Parameters {
pStructName := rpcbinding.ToParameterBindingName(p.Name)
if p.ExtendedType != nil {
pName := eStructName + "." + pStructName
cfg.Types[pName] = *p.ExtendedType
}
}
}
if o.GuessEventTypes {
if len(di.EmittedEvents) > 0 {
for eventName, eventUsages := range di.EmittedEvents {
var manifestEvent HybridEvent
for _, e := range o.ContractEvents {
if e.Name == eventName {
manifestEvent = e
break
}
}
if len(manifestEvent.Name) == 0 {
return nil, fmt.Errorf("inconsistent usages of event `%s`: not declared in the contract config", eventName)
}
exampleUsage := eventUsages[0]
for _, usage := range eventUsages {
if len(usage.Params) != len(manifestEvent.Parameters) {
return nil, fmt.Errorf("inconsistent usages of event `%s` against config: number of params mismatch: %d vs %d", eventName, len(exampleUsage.Params), len(manifestEvent.Parameters))
}
for i, actual := range usage.Params {
mParam := manifestEvent.Parameters[i]
// TODO: see the TestCompile_GuessEventTypes, "SC parameter type mismatch" section,
// do we want to compare with actual.RealType? The conversion code is emitted by the
// compiler for it, so we expect the parameter to be of the proper type.
if !(mParam.Type == smartcontract.AnyType || actual.TypeSC == mParam.Type) {
return nil, fmt.Errorf("inconsistent usages of event `%s` against config: SC type of param #%d mismatch: %s vs %s", eventName, i, actual.TypeSC, mParam.Type)
}
expected := exampleUsage.Params[i]
if !actual.ExtendedType.Equals(expected.ExtendedType) {
return nil, fmt.Errorf("inconsistent usages of event `%s`: extended type of param #%d mismatch", eventName, i)
}
}
}
eBindingName := rpcbinding.ToEventBindingName(eventName)
for typeName, extType := range exampleUsage.ExtTypes {
if _, ok := cfg.NamedTypes[typeName]; !ok {
cfg.NamedTypes[typeName] = extType
}
}
for _, p := range exampleUsage.Params {
pBindingName := rpcbinding.ToParameterBindingName(p.Name)
pname := eBindingName + "." + pBindingName
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)
@ -366,24 +468,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)
}
}
}

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

@ -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 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.
EmittedEvents map[string][][]string `json:"-"`
// 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.
@ -103,7 +108,7 @@ type DebugRange struct {
End uint16
}
// DebugParam represents the variables's name and type.
// DebugParam represents the variable's name and type.
type DebugParam struct {
Name string `json:"name"`
Type string `json:"type"`
@ -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 {
@ -373,10 +386,12 @@ func (c *codegen) scAndVMTypeFromType(t types.Type, exts map[string]binding.Exte
over.TypeName = "map[" + t.Key().String() + "]" + over.TypeName
return smartcontract.MapType, stackitem.MapT, over, et
case *types.Struct:
var extName string
if isNamed {
over.Package = named.Obj().Pkg().Path()
over.TypeName = named.Obj().Pkg().Name() + "." + named.Obj().Name()
_ = c.genStructExtended(t, over.TypeName, exts)
extName = over.TypeName
} else {
name := "unnamed"
if exts != nil {
@ -385,11 +400,14 @@ func (c *codegen) scAndVMTypeFromType(t types.Type, exts map[string]binding.Exte
}
_ = c.genStructExtended(t, name, exts)
}
// For bindings configurator this structure becomes named in fact. Its name
// is "unnamed[X...X]".
extName = name
}
return smartcontract.ArrayType, stackitem.StructT, over,
&binding.ExtendedType{ // Value-less, refer to exts.
Base: smartcontract.ArrayType,
Name: over.TypeName,
Name: extName,
}
case *types.Slice:
@ -580,9 +598,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

@ -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,11 +198,13 @@ 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 {
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 ||
@ -207,16 +220,21 @@ func (c *codegen) processNotify(f *funcScope, args []ast.Expr, hasEllipsis bool)
// 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].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

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 {

View file

@ -10,6 +10,7 @@ purposes, otherwise more specific types are recommended.
package nep11
import (
"errors"
"fmt"
"math/big"
"unicode/utf8"
@ -246,3 +247,71 @@ func UnwrapKnownProperties(m *stackitem.Map, err error) (map[string]string, erro
}
return res, nil
}
// TransferEventsFromApplicationLog retrieves all emitted TransferEvents from the
// provided [result.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 decode event from stackitem (event #%d, execution #%d): %w", j, i, err)
}
res = append(res, event)
}
}
return res, nil
}
// FromStackItem converts provided [stackitem.Array] to TransferEvent or returns an
// error if it's not possible to do to 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 event parameters")
}
b, err := arr[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid From: %w", err)
}
e.From, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode From: %w", err)
}
b, err = arr[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid To: %w", err)
}
e.To, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode To: %w", err)
}
e.Amount, err = arr[2].TryInteger()
if err != nil {
return fmt.Errorf("field to decode Avount: %w", err)
}
e.ID, err = arr[3].TryBytes()
if err != nil {
return fmt.Errorf("failed to decode ID: %w", err)
}
return nil
}

View file

@ -8,12 +8,15 @@ package nep17
import (
"errors"
"fmt"
"math/big"
"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/rpcclient/neptoken"
"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"
)
// Invoker is used by TokenReader to call various safe methods.
@ -147,3 +150,66 @@ func (t *TokenWriter) MultiTransferUnsigned(params []TransferParameters) (*trans
}
return t.actor.MakeUnsignedRun(script, nil)
}
// TransferEventsFromApplicationLog retrieves all emitted TransferEvents from the
// provided [result.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 decode event from stackitem (event #%d, execution #%d): %w", j, i, err)
}
res = append(res, event)
}
}
return res, nil
}
// FromStackItem converts provided [stackitem.Array] to TransferEvent or returns an
// error if it's not possible to do to 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 event parameters")
}
b, err := arr[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid From: %w", err)
}
e.From, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode From: %w", err)
}
b, err = arr[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid To: %w", err)
}
e.To, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode To: %w", err)
}
e.Amount, err = arr[2].TryInteger()
if err != nil {
return fmt.Errorf("field to decode Avount: %w", err)
}
return nil
}

View file

@ -53,7 +53,16 @@ type (
Hash util.Uint160 `yaml:"hash,omitempty"`
Overrides map[string]Override `yaml:"overrides,omitempty"`
CallFlags map[string]callflag.CallFlag `yaml:"callflags,omitempty"`
// NamedTypes contains exported structured types that have some name (even
// if the original structure doesn't) and a number of internal fields. The
// map key is in the form of `namespace.name`, the value is fully-qualified
// and possibly nested description of the type structure.
NamedTypes map[string]ExtendedType `yaml:"namedtypes,omitempty"`
// Types contains type structure description for various types used in
// 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:"-"`
}
@ -63,7 +72,7 @@ type (
Name string `yaml:"name,omitempty"` // Structure name, omitted for arrays, interfaces and maps.
Interface string `yaml:"interface,omitempty"` // Interface type name, "iterator" only for now.
Key smartcontract.ParamType `yaml:"key,omitempty"` // Key type (only simple types can be used for keys) for maps.
Value *ExtendedType `yaml:"value,omitempty"` // Value type for iterators and arrays.
Value *ExtendedType `yaml:"value,omitempty"` // Value type for iterators, arrays and maps.
Fields []FieldExtendedType `yaml:"fields,omitempty"` // Ordered type data for structure fields.
}
@ -256,3 +265,34 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract
func upperFirst(s string) string {
return strings.ToUpper(s[0:1]) + s[1:]
}
// Equals compares two extended types field-by-field and returns true if they are
// equal.
func (e *ExtendedType) Equals(other *ExtendedType) bool {
if e == nil && other == nil {
return true
}
if e != nil && other == nil ||
e == nil && other != nil {
return false
}
if !((e.Base == other.Base || (e.Base == smartcontract.ByteArrayType || e.Base == smartcontract.StringType) &&
(other.Base == smartcontract.ByteArrayType || other.Base == smartcontract.StringType)) &&
e.Name == other.Name &&
e.Interface == other.Interface &&
e.Key == other.Key) {
return false
}
if len(e.Fields) != len(other.Fields) {
return false
}
for i := range e.Fields {
if e.Fields[i].Field != other.Fields[i].Field {
return false
}
if !e.Fields[i].ExtendedType.Equals(&other.Fields[i].ExtendedType) {
return false
}
}
return (e.Value == nil && other.Value == nil) || (e.Value != nil && other.Value != nil && e.Value.Equals(other.Value))
}

View file

@ -0,0 +1,229 @@
package binding
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/stretchr/testify/assert"
)
func TestExtendedType_Equals(t *testing.T) {
crazyT := ExtendedType{
Base: smartcontract.StringType,
Name: "qwertyu",
Interface: "qwerty",
Key: smartcontract.BoolType,
Value: &ExtendedType{
Base: smartcontract.IntegerType,
},
Fields: []FieldExtendedType{
{
Field: "qwe",
ExtendedType: ExtendedType{
Base: smartcontract.IntegerType,
Name: "qwer",
Interface: "qw",
Key: smartcontract.ArrayType,
Fields: []FieldExtendedType{
{
Field: "as",
},
},
},
},
{
Field: "asf",
ExtendedType: ExtendedType{
Base: smartcontract.BoolType,
},
},
{
Field: "sffg",
ExtendedType: ExtendedType{
Base: smartcontract.AnyType,
},
},
},
}
tcs := map[string]struct {
a *ExtendedType
b *ExtendedType
expectedRes bool
}{
"both nil": {
a: nil,
b: nil,
expectedRes: true,
},
"a is nil": {
a: nil,
b: &ExtendedType{},
expectedRes: false,
},
"b is nil": {
a: &ExtendedType{},
b: nil,
expectedRes: false,
},
"base mismatch": {
a: &ExtendedType{
Base: smartcontract.StringType,
},
b: &ExtendedType{
Base: smartcontract.IntegerType,
},
expectedRes: false,
},
"name mismatch": {
a: &ExtendedType{
Base: smartcontract.ArrayType,
Name: "q",
},
b: &ExtendedType{
Base: smartcontract.ArrayType,
Name: "w",
},
expectedRes: false,
},
"number of fields mismatch": {
a: &ExtendedType{
Base: smartcontract.ArrayType,
Name: "q",
Fields: []FieldExtendedType{
{
Field: "IntField",
ExtendedType: ExtendedType{Base: smartcontract.IntegerType},
},
},
},
b: &ExtendedType{
Base: smartcontract.ArrayType,
Name: "w",
Fields: []FieldExtendedType{
{
Field: "IntField",
ExtendedType: ExtendedType{Base: smartcontract.IntegerType},
},
{
Field: "BoolField",
ExtendedType: ExtendedType{Base: smartcontract.BoolType},
},
},
},
expectedRes: false,
},
"field names mismatch": {
a: &ExtendedType{
Base: smartcontract.ArrayType,
Fields: []FieldExtendedType{
{
Field: "IntField",
ExtendedType: ExtendedType{Base: smartcontract.IntegerType},
},
},
},
b: &ExtendedType{
Base: smartcontract.ArrayType,
Fields: []FieldExtendedType{
{
Field: "BoolField",
ExtendedType: ExtendedType{Base: smartcontract.BoolType},
},
},
},
expectedRes: false,
},
"field types mismatch": {
a: &ExtendedType{
Base: smartcontract.ArrayType,
Fields: []FieldExtendedType{
{
Field: "Field",
ExtendedType: ExtendedType{Base: smartcontract.IntegerType},
},
},
},
b: &ExtendedType{
Base: smartcontract.ArrayType,
Fields: []FieldExtendedType{
{
Field: "Field",
ExtendedType: ExtendedType{Base: smartcontract.BoolType},
},
},
},
expectedRes: false,
},
"interface mismatch": {
a: &ExtendedType{Interface: "iterator"},
b: &ExtendedType{Interface: "unknown"},
expectedRes: false,
},
"value is nil": {
a: &ExtendedType{
Base: smartcontract.StringType,
},
b: &ExtendedType{
Base: smartcontract.StringType,
},
expectedRes: true,
},
"a value is not nil": {
a: &ExtendedType{
Base: smartcontract.ArrayType,
Value: &ExtendedType{},
},
b: &ExtendedType{
Base: smartcontract.ArrayType,
},
expectedRes: false,
},
"b value is not nil": {
a: &ExtendedType{
Base: smartcontract.ArrayType,
},
b: &ExtendedType{
Base: smartcontract.ArrayType,
Value: &ExtendedType{},
},
expectedRes: false,
},
"byte array tolerance for a": {
a: &ExtendedType{
Base: smartcontract.StringType,
},
b: &ExtendedType{
Base: smartcontract.ByteArrayType,
},
expectedRes: true,
},
"byte array tolerance for b": {
a: &ExtendedType{
Base: smartcontract.ByteArrayType,
},
b: &ExtendedType{
Base: smartcontract.StringType,
},
expectedRes: true,
},
"key mismatch": {
a: &ExtendedType{
Key: smartcontract.StringType,
},
b: &ExtendedType{
Key: smartcontract.IntegerType,
},
expectedRes: false,
},
"good nested": {
a: &crazyT,
b: &crazyT,
expectedRes: true,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.expectedRes, tc.a.Equals(tc.b))
})
}
}

View file

@ -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"
@ -12,8 +13,21 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
)
const srcTmpl = `
{{- define "SAFEMETHOD" -}}
// The set of constants containing parts of RPC binding template. Each block of code
// including template definition and var/type/method definitions contain new line at the
// 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}} represents "{{.ManifestName}}" event emitted by the contract.
type {{.Name}} struct {
{{- range $index, $arg := .Parameters}}
{{.Name}} {{.Type}}
{{- end}}
}
{{ end }}`
safemethodDefinition = `{{ define "SAFEMETHOD" }}
// {{.Name}} {{.Comment}}
func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}}
{{- if ne $index 0}}, {{end}}
@ -32,8 +46,7 @@ func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}}
{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
{{- end}}
}
{{- if eq .Unwrapper "SessionIterator"}}
{{ if eq .Unwrapper "SessionIterator" }}
// {{.Name}}Expanded is similar to {{.Name}} (uses the same contract
// method), but can be useful if the server used doesn't support sessions and
// doesn't expand iterators. It creates a script that will get the specified
@ -42,17 +55,16 @@ func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}}
func (c *ContractReader) {{.Name}}Expanded({{range $index, $arg := .Arguments}}{{.Name}} {{.Type}}, {{end}}_numOfIteratorItems int) ([]stackitem.Item, error) {
return unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "{{.NameABI}}", _numOfIteratorItems{{range $arg := .Arguments}}, {{.Name}}{{end}}))
}
{{- end -}}
{{- end -}}
{{- define "METHOD" -}}
{{- if eq .ReturnType "bool"}}func scriptFor{{.Name}}({{range $index, $arg := .Arguments -}}
{{ end }}{{ end }}`
methodDefinition = `{{ define "METHOD" }}{{ if eq .ReturnType "bool"}}
func scriptFor{{.Name}}({{range $index, $arg := .Arguments -}}
{{- if ne $index 0}}, {{end}}
{{- .Name}} {{.Type}}
{{- end}}) ([]byte, error) {
return smartcontract.CreateCallWithAssertScript(Hash, "{{ .NameABI }}"{{- range $index, $arg := .Arguments -}}, {{.Name}}{{end}})
}
{{end}}// {{.Name}} {{.Comment}}
{{ end }}
// {{.Name}} {{.Comment}}
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) {{.Name}}({{range $index, $arg := .Arguments -}}
@ -97,8 +109,9 @@ func (c *Contract) {{.Name}}Unsigned({{range $index, $arg := .Arguments -}}
}
return c.actor.MakeUnsignedRun(script, nil){{end}}
}
{{- end -}}
// Package {{.PackageName}} contains RPC wrappers for {{.ContractName}} contract.
{{end}}`
bindingDefinition = `// Package {{.PackageName}} contains RPC wrappers for {{.ContractName}} contract.
package {{.PackageName}}
import (
@ -107,16 +120,17 @@ import (
// Hash contains contract hash.
var Hash = {{ .Hash }}
{{range $name, $typ := .NamedTypes}}
{{ range $name, $typ := .NamedTypes }}
// {{toTypeName $name}} is a contract-specific {{$name}} type used by its methods.
type {{toTypeName $name}} struct {
{{- range $m := $typ.Fields}}
{{.Field}} {{etTypeToStr .ExtendedType}}
{{- end}}
}
{{end -}}
{{if .HasReader}}// Invoker is used by ContractReader to call various safe methods.
{{end}}
{{- range $e := .CustomEvents }}{{template "EVENT" $e }}{{ end -}}
{{- if .HasReader}}
// Invoker is used by ContractReader to call various safe methods.
type Invoker interface {
{{if or .IsNep11D .IsNep11ND}} nep11.Invoker
{{else -}}
@ -129,9 +143,9 @@ type Invoker interface {
{{end -}}
{{end -}}
}
{{end -}}
{{if .HasWriter}}// Actor is used by Contract to call state-changing methods.
{{- if .HasWriter}}
// Actor is used by Contract to call state-changing methods.
type Actor interface {
{{- if .HasReader}}
Invoker
@ -150,9 +164,9 @@ type Actor interface {
SendRun(script []byte) (util.Uint256, uint32, error)
{{end -}}
}
{{end -}}
{{if .HasReader}}// ContractReader implements safe contract methods.
{{- if .HasReader}}
// ContractReader implements safe contract methods.
type ContractReader struct {
{{if .IsNep11D}}nep11.DivisibleReader
{{end -}}
@ -162,9 +176,9 @@ type ContractReader struct {
{{end -}}
invoker Invoker
}
{{end -}}
{{if .HasWriter}}// Contract implements all contract methods.
{{- if .HasWriter}}
// Contract implements all contract methods.
type Contract struct {
{{if .HasReader}}ContractReader
{{end -}}
@ -176,9 +190,9 @@ type Contract struct {
{{end -}}
actor Actor
}
{{end -}}
{{if .HasReader}}// NewReader creates an instance of ContractReader using Hash and the given Invoker.
{{- if .HasReader}}
// NewReader creates an instance of ContractReader using Hash and the given Invoker.
func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{
{{- if .IsNep11D}}*nep11.NewDivisibleReader(invoker, Hash), {{end}}
@ -186,9 +200,9 @@ func NewReader(invoker Invoker) *ContractReader {
{{- if .IsNep17}}*nep17.NewReader(invoker, Hash), {{end -}}
invoker}
}
{{end -}}
{{if .HasWriter}}// New creates an instance of Contract using Hash and the given Actor.
{{- if .HasWriter}}
// New creates an instance of Contract using Hash and the given Actor.
func New(actor Actor) *Contract {
{{if .IsNep11D}}var nep11dt = nep11.NewDivisible(actor, Hash)
{{end -}}
@ -207,47 +221,114 @@ func New(actor Actor) *Contract {
{{- if .IsNep17}}nep17t.TokenWriter, {{end -}}
actor}
}
{{end -}}
{{range $m := .SafeMethods}}
{{template "SAFEMETHOD" $m }}
{{end}}
{{- range $m := .Methods}}
{{template "METHOD" $m }}
{{end}}
{{- range $name, $typ := .NamedTypes}}
{{- range $m := .SafeMethods }}{{template "SAFEMETHOD" $m }}{{ end -}}
{{- range $m := .Methods -}}{{template "METHOD" $m }}{{ end -}}
{{- range $name, $typ := .NamedTypes }}
// itemTo{{toTypeName $name}} converts stack item into *{{toTypeName $name}}.
func itemTo{{toTypeName $name}}(item stackitem.Item, err error) (*{{toTypeName $name}}, error) {
if err != nil {
return nil, err
}
var res = new({{toTypeName $name}})
err = res.FromStackItem(item)
return res, err
}
// FromStackItem retrieves fields of {{toTypeName $name}} from the given
// [stackitem.Item] or returns an error if it's not possible to do to so.
func (res *{{toTypeName $name}}) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return nil, errors.New("not an array")
return errors.New("not an array")
}
if len(arr) != {{len $typ.Fields}} {
return nil, errors.New("wrong number of structure elements")
return errors.New("wrong number of structure elements")
}
var res = new({{toTypeName $name}})
{{if len .Fields}} var index = -1
{{if len .Fields}}
var (
index = -1
err error
)
{{- range $m := $typ.Fields}}
index++
res.{{.Field}}, err = {{etTypeConverter .ExtendedType "arr[index]"}}
if err != nil {
return nil, fmt.Errorf("field {{.Field}}: %w", err)
return fmt.Errorf("field {{.Field}}: %w", err)
}
{{end}}
{{end}}
return res, err
{{- end}}
return nil
}
{{end}}`
{{ end -}}
{{- range $e := .CustomEvents }}
// {{$e.Name}}sFromApplicationLog retrieves a set of all emitted events
// with "{{$e.ManifestName}}" name from the provided [result.ApplicationLog].
func {{$e.Name}}sFromApplicationLog(log *result.ApplicationLog) ([]*{{$e.Name}}, error) {
if log == nil {
return nil, errors.New("nil application log")
}
var res []*{{$e.Name}}
for i, ex := range log.Executions {
for j, e := range ex.Events {
if e.Name != "{{$e.ManifestName}}" {
continue
}
event := new({{$e.Name}})
err := event.FromStackItem(e.Item)
if err != nil {
return nil, fmt.Errorf("failed to deserialize {{$e.Name}} 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}} or
// returns an error if it's not possible to do to so.
func (e *{{$e.Name}}) 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.{{.Name}}, err = {{etTypeConverter .ExtType "arr[index]"}}
if err != nil {
return fmt.Errorf("field {{.Name}}: %w", err)
}
{{end}}
{{- end}}
return nil
}
{{end -}}`
srcTmpl = bindingDefinition +
eventDefinition +
safemethodDefinition +
methodDefinition
)
type (
ContractTmpl struct {
binding.ContractTmpl
SafeMethods []SafeMethodTmpl
CustomEvents []CustomEventTemplate
NamedTypes map[string]binding.ExtendedType
IsNep11D bool
@ -265,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.
@ -296,12 +396,14 @@ func Generate(cfg binding.Config) error {
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11NonDivisible)
ctr.IsNep11ND = true
}
mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep11Base)
break // Can't be NEP-17 at the same time.
}
if std == manifest.NEP17StandardName && standard.ComplyABI(cfg.Manifest, standard.Nep17) == nil {
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep17)
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"] = struct{}{}
ctr.IsNep17 = true
mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep17)
break // Can't be NEP-11 at the same time.
}
}
@ -315,7 +417,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{
@ -344,6 +446,18 @@ func dropManifestMethods(meths []manifest.Method, manifested []manifest.Method)
return meths
}
func dropManifestEvents(events []manifest.Event, manifested []manifest.Event) []manifest.Event {
for _, e := range manifested {
for i := 0; i < len(events); i++ {
if events[i].Name == e.Name && len(events[i].Parameters) == len(e.Parameters) {
events = append(events[:i], events[i+1:]...)
i--
}
}
}
return events
}
func dropStdMethods(meths []manifest.Method, std *standard.Standard) []manifest.Method {
meths = dropManifestMethods(meths, std.Manifest.ABI.Methods)
if std.Optional != nil {
@ -355,6 +469,14 @@ func dropStdMethods(meths []manifest.Method, std *standard.Standard) []manifest.
return meths
}
func dropStdEvents(events []manifest.Event, std *standard.Standard) []manifest.Event {
events = dropManifestEvents(events, std.Manifest.ABI.Events)
if std.Base != nil {
return dropStdEvents(events, std.Base)
}
return events
}
func extendedTypeToGo(et binding.ExtendedType, named map[string]binding.ExtendedType) (string, string) {
switch et.Base {
case smartcontract.AnyType:
@ -389,7 +511,12 @@ func extendedTypeToGo(et binding.ExtendedType, named map[string]binding.Extended
case smartcontract.MapType:
kt, _ := extendedTypeToGo(binding.ExtendedType{Base: et.Key}, named)
vt, _ := extendedTypeToGo(*et.Value, named)
var vt string
if et.Value != nil {
vt, _ = extendedTypeToGo(*et.Value, named)
} else {
vt = "any"
}
return "map[" + kt + "]" + vt, "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
case smartcontract.InteropInterfaceType:
return "any", ""
@ -402,7 +529,7 @@ func extendedTypeToGo(et binding.ExtendedType, named map[string]binding.Extended
func etTypeConverter(et binding.ExtendedType, v string) string {
switch et.Base {
case smartcontract.AnyType:
return v + ".Value(), nil"
return v + ".Value(), error(nil)"
case smartcontract.BoolType:
return v + ".TryBool()"
case smartcontract.IntegerType:
@ -484,6 +611,7 @@ func etTypeConverter(et binding.ExtendedType, v string) string {
}, v)
case smartcontract.MapType:
if et.Value != nil {
at, _ := extendedTypeToGo(et, nil)
return `func (item stackitem.Item) (` + at + `, error) {
m, ok := item.Value().([]stackitem.MapElement)
@ -504,6 +632,14 @@ func etTypeConverter(et binding.ExtendedType, v string) string {
}
return res, nil
} (` + v + `)`
}
return etTypeConverter(binding.ExtendedType{
Base: smartcontract.MapType,
Key: et.Key,
Value: &binding.ExtendedType{
Base: smartcontract.AnyType,
},
}, v)
case smartcontract.InteropInterfaceType:
return "item.Value(), nil"
case smartcontract.VoidType:
@ -520,7 +656,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{}{}
}
@ -551,6 +687,47 @@ 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 {
eBindingName := ToEventBindingName(abiEvent.Name)
eTmp := CustomEventTemplate{
Name: eBindingName,
ManifestName: abiEvent.Name,
}
for i := range abiEvent.Parameters {
pBindingName := ToParameterBindingName(abiEvent.Parameters[i].Name)
fullPName := eBindingName + "." + pBindingName
typeStr, pkg := scTypeConverter(fullPName, abiEvent.Parameters[i].Type, &cfg)
if pkg != "" {
imports[pkg] = struct{}{}
}
var (
extType binding.ExtendedType
ok bool
)
if extType, ok = cfg.Types[fullPName]; !ok {
extType = binding.ExtendedType{
Base: abiEvent.Parameters[i].Type,
}
addETImports(extType, ctr.NamedTypes, imports)
}
eTmp.Parameters = append(eTmp.Parameters, EventParamTmpl{
ParamTmpl: binding.ParamTmpl{
Name: pBindingName,
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 {
@ -680,3 +857,44 @@ func toTypeName(s string) string {
func addIndent(str string, ind string) string {
return strings.ReplaceAll(str, "\n", "\n"+ind)
}
// ToEventBindingName converts event name specified in the contract manifest to
// a valid go exported event structure name.
func ToEventBindingName(eventName string) string {
return toPascalCase(eventName) + "Event"
}
// ToParameterBindingName converts parameter name specified in the contract
// manifest to a valid go structure's exported field name.
func ToParameterBindingName(paramName string) string {
return toPascalCase(paramName)
}
// 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:]
}