commit
772e723e8e
45 changed files with 5313 additions and 324 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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`")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
1
cli/smartcontract/testdata/gas/gas.go
vendored
1
cli/smartcontract/testdata/gas/gas.go
vendored
|
@ -44,4 +44,3 @@ func New(actor Actor) *Contract {
|
|||
var nep17t = nep17.New(actor, Hash)
|
||||
return &Contract{ContractReader{nep17t.TokenReader, actor}, nep17t.TokenWriter, actor}
|
||||
}
|
||||
|
||||
|
|
7
cli/smartcontract/testdata/invalid5/invalid.go
vendored
Normal file
7
cli/smartcontract/testdata/invalid5/invalid.go
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
package invalid5
|
||||
|
||||
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
|
||||
func Main() {
|
||||
runtime.Notify("Non declared event")
|
||||
}
|
1
cli/smartcontract/testdata/invalid5/invalid.yml
vendored
Normal file
1
cli/smartcontract/testdata/invalid5/invalid.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
name: Test undeclared event
|
7
cli/smartcontract/testdata/invalid6/invalid.go
vendored
Normal file
7
cli/smartcontract/testdata/invalid6/invalid.go
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
package invalid6
|
||||
|
||||
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
|
||||
func Main() {
|
||||
runtime.Notify("SomeEvent", "p1", "p2")
|
||||
}
|
6
cli/smartcontract/testdata/invalid6/invalid.yml
vendored
Normal file
6
cli/smartcontract/testdata/invalid6/invalid.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
name: Test undeclared event
|
||||
events:
|
||||
- name: SomeEvent
|
||||
parameters:
|
||||
- name: p1
|
||||
type: String
|
7
cli/smartcontract/testdata/invalid7/invalid.go
vendored
Normal file
7
cli/smartcontract/testdata/invalid7/invalid.go
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
package invalid7
|
||||
|
||||
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
|
||||
func Main() {
|
||||
runtime.Notify("SomeEvent", "p1", 5)
|
||||
}
|
8
cli/smartcontract/testdata/invalid7/invalid.yml
vendored
Normal file
8
cli/smartcontract/testdata/invalid7/invalid.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: Test undeclared event
|
||||
events:
|
||||
- name: SomeEvent
|
||||
parameters:
|
||||
- name: p1
|
||||
type: String
|
||||
- name: p2
|
||||
type: String
|
17
cli/smartcontract/testdata/invalid8/invalid.go
vendored
Normal file
17
cli/smartcontract/testdata/invalid8/invalid.go
vendored
Normal 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"})
|
||||
}
|
6
cli/smartcontract/testdata/invalid8/invalid.yml
vendored
Normal file
6
cli/smartcontract/testdata/invalid8/invalid.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
name: Test undeclared event
|
||||
events:
|
||||
- name: SomeEvent
|
||||
parameters:
|
||||
- name: p1
|
||||
type: Array
|
12
cli/smartcontract/testdata/invalid9/invalid.go
vendored
Normal file
12
cli/smartcontract/testdata/invalid9/invalid.go
vendored
Normal 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}
|
||||
}
|
16
cli/smartcontract/testdata/invalid9/invalid.yml
vendored
Normal file
16
cli/smartcontract/testdata/invalid9/invalid.yml
vendored
Normal 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
|
184
cli/smartcontract/testdata/nameservice/nns.go
vendored
184
cli/smartcontract/testdata/nameservice/nns.go
vendored
|
@ -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
|
||||
}
|
||||
|
|
103
cli/smartcontract/testdata/nex/nex.go
vendored
103
cli/smartcontract/testdata/nex/nex.go
vendored
|
@ -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
|
||||
}
|
||||
|
|
1
cli/smartcontract/testdata/nonepiter/iter.go
vendored
1
cli/smartcontract/testdata/nonepiter/iter.go
vendored
|
@ -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"))
|
||||
|
|
19
cli/smartcontract/testdata/notifications/config.yml
vendored
Normal file
19
cli/smartcontract/testdata/notifications/config.yml
vendored
Normal 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
|
47
cli/smartcontract/testdata/notifications/config_extended.yml
vendored
Normal file
47
cli/smartcontract/testdata/notifications/config_extended.yml
vendored
Normal 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
|
19
cli/smartcontract/testdata/notifications/config_guessed.yml
vendored
Normal file
19
cli/smartcontract/testdata/notifications/config_guessed.yml
vendored
Normal 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
|
25
cli/smartcontract/testdata/notifications/notifications.go
vendored
Normal file
25
cli/smartcontract/testdata/notifications/notifications.go
vendored
Normal 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{})
|
||||
}
|
1030
cli/smartcontract/testdata/notifications/rpcbindings.out
vendored
Normal file
1030
cli/smartcontract/testdata/notifications/rpcbindings.out
vendored
Normal file
File diff suppressed because it is too large
Load diff
1126
cli/smartcontract/testdata/notifications/rpcbindings_extended.out
vendored
Executable file
1126
cli/smartcontract/testdata/notifications/rpcbindings_extended.out
vendored
Executable file
File diff suppressed because it is too large
Load diff
1139
cli/smartcontract/testdata/notifications/rpcbindings_guessed.out
vendored
Executable file
1139
cli/smartcontract/testdata/notifications/rpcbindings_guessed.out
vendored
Executable file
File diff suppressed because it is too large
Load diff
596
cli/smartcontract/testdata/structs/rpcbindings.out
vendored
596
cli/smartcontract/testdata/structs/rpcbindings.out
vendored
File diff suppressed because it is too large
Load diff
2
cli/smartcontract/testdata/types/config.yml
vendored
2
cli/smartcontract/testdata/types/config.yml
vendored
|
@ -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"]
|
||||
|
|
31
cli/smartcontract/testdata/types/rpcbindings.out
vendored
31
cli/smartcontract/testdata/types/rpcbindings.out
vendored
|
@ -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))
|
||||
|
|
4
cli/smartcontract/testdata/types/types.go
vendored
4
cli/smartcontract/testdata/types/types.go
vendored
|
@ -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
|
||||
}
|
||||
|
|
75
cli/smartcontract/testdata/verifyrpc/verify.go
vendored
75
cli/smartcontract/testdata/verifyrpc/verify.go
vendored
|
@ -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
|
||||
}
|
||||
|
|
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -18,6 +18,10 @@ events:
|
|||
parameters:
|
||||
- name: m
|
||||
type: Map
|
||||
- name: SomeCrazyMap
|
||||
parameters:
|
||||
- name: m
|
||||
type: Map
|
||||
- name: SomeArray
|
||||
parameters:
|
||||
- name: a
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
229
pkg/smartcontract/binding/generate_test.go
Normal file
229
pkg/smartcontract/binding/generate_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,7 +120,6 @@ import (
|
|||
|
||||
// Hash contains contract hash.
|
||||
var Hash = {{ .Hash }}
|
||||
|
||||
{{ range $name, $typ := .NamedTypes }}
|
||||
// {{toTypeName $name}} is a contract-specific {{$name}} type used by its methods.
|
||||
type {{toTypeName $name}} struct {
|
||||
|
@ -115,8 +127,10 @@ type {{toTypeName $name}} struct {
|
|||
{{.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 $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
|
||||
}
|
||||
arr, ok := item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
return nil, errors.New("not an array")
|
||||
}
|
||||
if len(arr) != {{len $typ.Fields}} {
|
||||
return nil, errors.New("wrong number of structure elements")
|
||||
var res = new({{toTypeName $name}})
|
||||
err = res.FromStackItem(item)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var res = new({{toTypeName $name}})
|
||||
{{if len .Fields}} var index = -1
|
||||
// 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 errors.New("not an array")
|
||||
}
|
||||
if len(arr) != {{len $typ.Fields}} {
|
||||
return errors.New("wrong number of structure elements")
|
||||
}
|
||||
{{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:]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue