a0d991a500
Unfortunately, without pre-set user extended types configuration for events and without --guess-eventtypes flag set we are allowed to rely only on manifest information about types. Manifest can't give us a lot of information, but we still need to be able to generate RPC binding. Arrays and structs are correctly handled by the current code, but maps always rely on the fact that map's value type is set. It's not true in the described case, so make the maps type convertor handle this situation in a similar way how arrays are handled. Without this commit the following panic occurs on attempt to generate RPC binding: ``` --- FAIL: TestAssistedRPCBindings/testdata/notifications (0.01s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x7f7c0e] goroutine 190 [running]: testing.tRunner.func1.2({0x109cb40, 0x1d58760}) /usr/local/go/src/testing/testing.go:1396 +0x24e testing.tRunner.func1() /usr/local/go/src/testing/testing.go:1399 +0x39f panic({0x109cb40, 0x1d58760}) /usr/local/go/src/runtime/panic.go:884 +0x212 github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding.extendedTypeToGo({0x22, {0x0, 0x0}, {0x0, 0x0}, 0x0, 0x0, {0x0, 0x0, 0x0}}, ...) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding/binding.go:515 +0x36e github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding.scTypeToGo({0xc000206d92?, 0xc000206d80?}, 0x22, 0xc0005d70e0) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding/binding.go:643 +0x138 github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding.scTemplateToRPC({{0xc00049bb07, 0x7}, 0xc0004c89c0, {0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, ...}, ...}, ...) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding/binding.go:686 +0xbc4 github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding.Generate({{0xc00049bb07, 0x7}, 0xc0004c89c0, {0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, ...}, ...}) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding/binding.go:421 +0x387 github.com/nspcc-dev/neo-go/cli/smartcontract.contractGenerateSomething(0xc00043e2c0, 0x137cd00) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/cli/smartcontract/generate.go:99 +0x855 github.com/nspcc-dev/neo-go/cli/smartcontract.contractGenerateRPCWrapper(0xc00043e2c0?) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/cli/smartcontract/generate.go:60 +0x25 github.com/urfave/cli.HandleAction({0x1048380?, 0x137c660?}, 0x13?) /home/anna/go/pkg/mod/github.com/urfave/cli@v1.22.5/app.go:524 +0x50 github.com/urfave/cli.Command.Run({{0x123539d, 0x13}, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x12577ad, 0x2a}, {0x127ad35, ...}, ...}, ...) /home/anna/go/pkg/mod/github.com/urfave/cli@v1.22.5/command.go:173 +0x65b github.com/urfave/cli.(*App).RunAsSubcommand(0xc0001f4000, 0xc00043e000) /home/anna/go/pkg/mod/github.com/urfave/cli@v1.22.5/app.go:405 +0x91b github.com/urfave/cli.Command.startApp({{0x12281e1, 0x8}, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x1254d8a, 0x28}, {0x0, ...}, ...}, ...) /home/anna/go/pkg/mod/github.com/urfave/cli@v1.22.5/command.go:372 +0x6e7 github.com/urfave/cli.Command.Run({{0x12281e1, 0x8}, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x1254d8a, 0x28}, {0x0, ...}, ...}, ...) /home/anna/go/pkg/mod/github.com/urfave/cli@v1.22.5/command.go:102 +0x825 github.com/urfave/cli.(*App).Run(0xc00024e000, {0xc0004f6420, 0xb, 0xb}) /home/anna/go/pkg/mod/github.com/urfave/cli@v1.22.5/app.go:277 +0x8a7 github.com/nspcc-dev/neo-go/cli/smartcontract.TestAssistedRPCBindings.func1.1(0x9f8829?) /home/anna/Documents/GitProjects/nspcc-dev/neo-go/cli/smartcontract/generate_test.go:395 +0x5fc testing.tRunner(0xc0006824e0, 0xc0004a3680) /usr/local/go/src/testing/testing.go:1446 +0x10b created by testing.(*T).Run /usr/local/go/src/testing/testing.go:1493 +0x35f ``` Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
900 lines
29 KiB
Go
900 lines
29 KiB
Go
package rpcbinding
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"unicode"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/binding"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
|
|
)
|
|
|
|
// 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}}
|
|
{{- .Name}} {{.Type}}
|
|
{{- end}}) {{if .ReturnType }}({{ .ReturnType }}, error) {
|
|
return {{if and (not .ItemTo) (eq .Unwrapper "Item")}}func (item stackitem.Item, err error) ({{ .ReturnType }}, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return {{addIndent (etTypeConverter .ExtendedReturn "item") "\t"}}
|
|
} ( {{- end -}} {{if .ItemTo -}} itemTo{{ .ItemTo }}( {{- end -}}
|
|
unwrap.{{.Unwrapper}}(c.invoker.Call(Hash, "{{ .NameABI }}"
|
|
{{- range $arg := .Arguments -}}, {{.Name}}{{end -}} )) {{- if or .ItemTo (eq .Unwrapper "Item") -}} ) {{- end}}
|
|
{{- else -}} (*result.Invoke, error) {
|
|
c.invoker.Call(Hash, "{{ .NameABI }}"
|
|
{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
|
|
{{- end}}
|
|
}
|
|
{{ 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
|
|
// number of result items from the iterator right in the VM and return them to
|
|
// you. It's only limited by VM stack and GAS available for RPC invocations.
|
|
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 }}`
|
|
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}}
|
|
// 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 -}}
|
|
{{- if ne $index 0}}, {{end}}
|
|
{{- .Name}} {{.Type}}
|
|
{{- end}}) (util.Uint256, uint32, error) {
|
|
{{if ne .ReturnType "bool"}}return c.actor.SendCall(Hash, "{{ .NameABI }}"
|
|
{{- range $index, $arg := .Arguments -}}, {{.Name}}{{end}}){{else}}script, err := scriptFor{{.Name}}({{- range $index, $arg := .Arguments -}}{{- if ne $index 0}}, {{end}}{{.Name}}{{end}})
|
|
if err != nil {
|
|
return util.Uint256{}, 0, err
|
|
}
|
|
return c.actor.SendRun(script){{end}}
|
|
}
|
|
|
|
// {{.Name}}Transaction {{.Comment}}
|
|
// This transaction is signed, but not sent to the network, instead it's
|
|
// returned to the caller.
|
|
func (c *Contract) {{.Name}}Transaction({{range $index, $arg := .Arguments -}}
|
|
{{- if ne $index 0}}, {{end}}
|
|
{{- .Name}} {{.Type}}
|
|
{{- end}}) (*transaction.Transaction, error) {
|
|
{{if ne .ReturnType "bool"}}return c.actor.MakeCall(Hash, "{{ .NameABI }}"
|
|
{{- range $index, $arg := .Arguments -}}, {{.Name}}{{end}}){{else}}script, err := scriptFor{{.Name}}({{- range $index, $arg := .Arguments -}}{{- if ne $index 0}}, {{end}}{{.Name}}{{end}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.actor.MakeRun(script){{end}}
|
|
}
|
|
|
|
// {{.Name}}Unsigned {{.Comment}}
|
|
// This transaction is not signed, it's simply returned to the caller.
|
|
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
|
|
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
|
|
func (c *Contract) {{.Name}}Unsigned({{range $index, $arg := .Arguments -}}
|
|
{{- if ne $index 0}}, {{end}}
|
|
{{- .Name}} {{.Type}}
|
|
{{- end}}) (*transaction.Transaction, error) {
|
|
{{if ne .ReturnType "bool"}}return c.actor.MakeUnsignedCall(Hash, "{{ .NameABI }}", nil
|
|
{{- range $index, $arg := .Arguments -}}, {{.Name}}{{end}}){{else}}script, err := scriptFor{{.Name}}({{- range $index, $arg := .Arguments -}}{{- if ne $index 0}}, {{end}}{{.Name}}{{end}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.actor.MakeUnsignedRun(script, nil){{end}}
|
|
}
|
|
{{end}}`
|
|
|
|
bindingDefinition = `// Package {{.PackageName}} contains RPC wrappers for {{.ContractName}} contract.
|
|
package {{.PackageName}}
|
|
|
|
import (
|
|
{{range $m := .Imports}} "{{ $m }}"
|
|
{{end}})
|
|
|
|
// 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 {
|
|
{{- range $m := $typ.Fields}}
|
|
{{.Field}} {{etTypeToStr .ExtendedType}}
|
|
{{- end}}
|
|
}
|
|
{{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 -}}
|
|
{{ if .IsNep17}} nep17.Invoker
|
|
{{else if len .SafeMethods}} Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error)
|
|
{{end -}}
|
|
{{if .HasIterator}} CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...any) (*result.Invoke, error)
|
|
TerminateSession(sessionID uuid.UUID) error
|
|
TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error)
|
|
{{end -}}
|
|
{{end -}}
|
|
}
|
|
{{end -}}
|
|
{{- if .HasWriter}}
|
|
// Actor is used by Contract to call state-changing methods.
|
|
type Actor interface {
|
|
{{- if .HasReader}}
|
|
Invoker
|
|
{{end}}
|
|
{{- if or .IsNep11D .IsNep11ND}}
|
|
nep11.Actor
|
|
{{else if .IsNep17}}
|
|
nep17.Actor
|
|
{{end}}
|
|
{{- if len .Methods}}
|
|
MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error)
|
|
MakeRun(script []byte) (*transaction.Transaction, error)
|
|
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error)
|
|
MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error)
|
|
SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error)
|
|
SendRun(script []byte) (util.Uint256, uint32, error)
|
|
{{end -}}
|
|
}
|
|
{{end -}}
|
|
{{- if .HasReader}}
|
|
// ContractReader implements safe contract methods.
|
|
type ContractReader struct {
|
|
{{if .IsNep11D}}nep11.DivisibleReader
|
|
{{end -}}
|
|
{{if .IsNep11ND}}nep11.NonDivisibleReader
|
|
{{end -}}
|
|
{{if .IsNep17}}nep17.TokenReader
|
|
{{end -}}
|
|
invoker Invoker
|
|
}
|
|
{{end -}}
|
|
{{- if .HasWriter}}
|
|
// Contract implements all contract methods.
|
|
type Contract struct {
|
|
{{if .HasReader}}ContractReader
|
|
{{end -}}
|
|
{{if .IsNep11D}}nep11.DivisibleWriter
|
|
{{end -}}
|
|
{{if .IsNep11ND}}nep11.BaseWriter
|
|
{{end -}}
|
|
{{if .IsNep17}}nep17.TokenWriter
|
|
{{end -}}
|
|
actor Actor
|
|
}
|
|
{{end -}}
|
|
{{- 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}}
|
|
{{- if .IsNep11ND}}*nep11.NewNonDivisibleReader(invoker, Hash), {{end}}
|
|
{{- if .IsNep17}}*nep17.NewReader(invoker, Hash), {{end -}}
|
|
invoker}
|
|
}
|
|
{{end -}}
|
|
{{- 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 -}}
|
|
{{if .IsNep11ND}}var nep11ndt = nep11.NewNonDivisible(actor, Hash)
|
|
{{end -}}
|
|
{{if .IsNep17}}var nep17t = nep17.New(actor, Hash)
|
|
{{end -}}
|
|
return &Contract{
|
|
{{- if .HasReader}}ContractReader{
|
|
{{- if .IsNep11D}}nep11dt.DivisibleReader, {{end -}}
|
|
{{- if .IsNep11ND}}nep11ndt.NonDivisibleReader, {{end -}}
|
|
{{- if .IsNep17}}nep17t.TokenReader, {{end -}}
|
|
actor}, {{end -}}
|
|
{{- if .IsNep11D}}nep11dt.DivisibleWriter, {{end -}}
|
|
{{- if .IsNep11ND}}nep11ndt.BaseWriter, {{end -}}
|
|
{{- if .IsNep17}}nep17t.TokenWriter, {{end -}}
|
|
actor}
|
|
}
|
|
{{end -}}
|
|
{{- range $m := .SafeMethods }}{{template "SAFEMETHOD" $m }}{{ end -}}
|
|
{{- range $m := .Methods -}}{{template "METHOD" $m }}{{ end -}}
|
|
{{- range $name, $typ := .NamedTypes }}
|
|
// itemTo{{toTypeName $name}} converts stack item into *{{toTypeName $name}}.
|
|
func itemTo{{toTypeName $name}}(item stackitem.Item, err error) (*{{toTypeName $name}}, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var res = new({{toTypeName $name}})
|
|
err = res.FromStackItem(item)
|
|
return res, err
|
|
}
|
|
|
|
// FromStackItem retrieves fields of {{toTypeName $name}} from the given stack item
|
|
// and returns an error if 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 fmt.Errorf("field {{.Field}}: %w", err)
|
|
}
|
|
{{end}}
|
|
{{- end}}
|
|
return nil
|
|
}
|
|
{{ end -}}
|
|
{{- range $e := .CustomEvents }}
|
|
// {{$e.Name}}sFromApplicationLog retrieves a set of all emitted events
|
|
// with "{{$e.ManifestName}}" name from the provided 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}} and
|
|
// returns an error if 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
|
|
IsNep11ND bool
|
|
IsNep17 bool
|
|
|
|
HasReader bool
|
|
HasWriter bool
|
|
HasIterator bool
|
|
}
|
|
|
|
SafeMethodTmpl struct {
|
|
binding.MethodTmpl
|
|
Unwrapper string
|
|
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.
|
|
func NewConfig() binding.Config {
|
|
return binding.NewConfig()
|
|
}
|
|
|
|
// Generate writes Go file containing smartcontract bindings to the `cfg.Output`.
|
|
// It doesn't check manifest from Config for validity, incorrect manifest can
|
|
// lead to unexpected results.
|
|
func Generate(cfg binding.Config) error {
|
|
// Avoid changing *cfg.Manifest.
|
|
mfst := *cfg.Manifest
|
|
mfst.ABI.Methods = make([]manifest.Method, len(mfst.ABI.Methods))
|
|
copy(mfst.ABI.Methods, cfg.Manifest.ABI.Methods)
|
|
cfg.Manifest = &mfst
|
|
|
|
var imports = make(map[string]struct{})
|
|
var ctr ContractTmpl
|
|
|
|
// Strip standard methods from NEP-XX packages.
|
|
for _, std := range cfg.Manifest.SupportedStandards {
|
|
if std == manifest.NEP11StandardName {
|
|
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"] = struct{}{}
|
|
if standard.ComplyABI(cfg.Manifest, standard.Nep11Divisible) == nil {
|
|
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11Divisible)
|
|
ctr.IsNep11D = true
|
|
} else if standard.ComplyABI(cfg.Manifest, standard.Nep11NonDivisible) == nil {
|
|
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.
|
|
}
|
|
}
|
|
|
|
// OnNepXXPayment handlers normally can't be called directly.
|
|
if standard.ComplyABI(cfg.Manifest, standard.Nep11Payable) == nil {
|
|
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11Payable)
|
|
}
|
|
if standard.ComplyABI(cfg.Manifest, standard.Nep17Payable) == nil {
|
|
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep17Payable)
|
|
}
|
|
|
|
ctr.ContractTmpl = binding.TemplateFromManifest(cfg, scTypeToGo)
|
|
ctr = scTemplateToRPC(cfg, ctr, imports, scTypeToGo)
|
|
ctr.NamedTypes = cfg.NamedTypes
|
|
|
|
var srcTemplate = template.Must(template.New("generate").Funcs(template.FuncMap{
|
|
"addIndent": addIndent,
|
|
"etTypeConverter": etTypeConverter,
|
|
"etTypeToStr": func(et binding.ExtendedType) string {
|
|
r, _ := extendedTypeToGo(et, ctr.NamedTypes)
|
|
return r
|
|
},
|
|
"toTypeName": toTypeName,
|
|
"cutPointer": cutPointer,
|
|
}).Parse(srcTmpl))
|
|
|
|
return srcTemplate.Execute(cfg.Output, ctr)
|
|
}
|
|
|
|
func dropManifestMethods(meths []manifest.Method, manifested []manifest.Method) []manifest.Method {
|
|
for _, m := range manifested {
|
|
for i := 0; i < len(meths); i++ {
|
|
if meths[i].Name == m.Name && len(meths[i].Parameters) == len(m.Parameters) {
|
|
meths = append(meths[:i], meths[i+1:]...)
|
|
i--
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
meths = dropManifestMethods(meths, std.Optional)
|
|
}
|
|
if std.Base != nil {
|
|
return dropStdMethods(meths, std.Base)
|
|
}
|
|
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:
|
|
return "any", ""
|
|
case smartcontract.BoolType:
|
|
return "bool", ""
|
|
case smartcontract.IntegerType:
|
|
return "*big.Int", "math/big"
|
|
case smartcontract.ByteArrayType:
|
|
return "[]byte", ""
|
|
case smartcontract.StringType:
|
|
return "string", ""
|
|
case smartcontract.Hash160Type:
|
|
return "util.Uint160", "github.com/nspcc-dev/neo-go/pkg/util"
|
|
case smartcontract.Hash256Type:
|
|
return "util.Uint256", "github.com/nspcc-dev/neo-go/pkg/util"
|
|
case smartcontract.PublicKeyType:
|
|
return "*keys.PublicKey", "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
case smartcontract.SignatureType:
|
|
return "[]byte", ""
|
|
case smartcontract.ArrayType:
|
|
if len(et.Name) > 0 {
|
|
return "*" + toTypeName(et.Name), "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
} else if et.Value != nil {
|
|
if et.Value.Base == smartcontract.PublicKeyType { // Special array wrapper.
|
|
return "keys.PublicKeys", "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
}
|
|
sub, pkg := extendedTypeToGo(*et.Value, named)
|
|
return "[]" + sub, pkg
|
|
}
|
|
return "[]any", ""
|
|
|
|
case smartcontract.MapType:
|
|
kt, _ := extendedTypeToGo(binding.ExtendedType{Base: et.Key}, 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", ""
|
|
case smartcontract.VoidType:
|
|
return "", ""
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
func etTypeConverter(et binding.ExtendedType, v string) string {
|
|
switch et.Base {
|
|
case smartcontract.AnyType:
|
|
return v + ".Value(), error(nil)"
|
|
case smartcontract.BoolType:
|
|
return v + ".TryBool()"
|
|
case smartcontract.IntegerType:
|
|
return v + ".TryInteger()"
|
|
case smartcontract.ByteArrayType, smartcontract.SignatureType:
|
|
return v + ".TryBytes()"
|
|
case smartcontract.StringType:
|
|
return `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
|
|
} (` + v + `)`
|
|
case smartcontract.Hash160Type:
|
|
return `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
|
|
} (` + v + `)`
|
|
case smartcontract.Hash256Type:
|
|
return `func (item stackitem.Item) (util.Uint256, error) {
|
|
b, err := item.TryBytes()
|
|
if err != nil {
|
|
return util.Uint256{}, err
|
|
}
|
|
u, err := util.Uint256DecodeBytesBE(b)
|
|
if err != nil {
|
|
return util.Uint256{}, err
|
|
}
|
|
return u, nil
|
|
} (` + v + `)`
|
|
case smartcontract.PublicKeyType:
|
|
return `func (item stackitem.Item) (*keys.PublicKey, error) {
|
|
b, err := item.TryBytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k, err := keys.NewPublicKeyFromBytes(b, elliptic.P256())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return k, nil
|
|
} (` + v + `)`
|
|
case smartcontract.ArrayType:
|
|
if len(et.Name) > 0 {
|
|
return "itemTo" + toTypeName(et.Name) + "(" + v + ", nil)"
|
|
} else if et.Value != nil {
|
|
at, _ := extendedTypeToGo(et, nil)
|
|
return `func (item stackitem.Item) (` + at + `, error) {
|
|
arr, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return nil, errors.New("not an array")
|
|
}
|
|
res := make(` + at + `, len(arr))
|
|
for i := range res {
|
|
res[i], err = ` + addIndent(etTypeConverter(*et.Value, "arr[i]"), "\t\t") + `
|
|
if err != nil {
|
|
return nil, fmt.Errorf("item %d: %w", i, err)
|
|
}
|
|
}
|
|
return res, nil
|
|
} (` + v + `)`
|
|
}
|
|
return etTypeConverter(binding.ExtendedType{
|
|
Base: smartcontract.ArrayType,
|
|
Value: &binding.ExtendedType{
|
|
Base: smartcontract.AnyType,
|
|
},
|
|
}, 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)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s is not a map", item.Type().String())
|
|
}
|
|
res := make(` + at + `)
|
|
for i := range m {
|
|
k, err := ` + addIndent(etTypeConverter(binding.ExtendedType{Base: et.Key}, "m[i].Key"), "\t\t") + `
|
|
if err != nil {
|
|
return nil, fmt.Errorf("key %d: %w", i, err)
|
|
}
|
|
v, err := ` + addIndent(etTypeConverter(*et.Value, "m[i].Value"), "\t\t") + `
|
|
if err != nil {
|
|
return nil, fmt.Errorf("value %d: %w", i, err)
|
|
}
|
|
res[k] = v
|
|
}
|
|
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:
|
|
return ""
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
func scTypeToGo(name string, typ smartcontract.ParamType, cfg *binding.Config) (string, string) {
|
|
et, ok := cfg.Types[name]
|
|
if !ok {
|
|
et = binding.ExtendedType{Base: typ}
|
|
}
|
|
return extendedTypeToGo(et, cfg.NamedTypes)
|
|
}
|
|
|
|
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{}{}
|
|
}
|
|
ctr.Hash = fmt.Sprintf("%#v", cfg.Hash)
|
|
for i := 0; i < len(ctr.Methods); i++ {
|
|
abim := cfg.Manifest.ABI.GetMethod(ctr.Methods[i].NameABI, len(ctr.Methods[i].Arguments))
|
|
if abim.Safe {
|
|
ctr.SafeMethods = append(ctr.SafeMethods, SafeMethodTmpl{MethodTmpl: ctr.Methods[i]})
|
|
et, ok := cfg.Types[abim.Name]
|
|
if ok {
|
|
ctr.SafeMethods[len(ctr.SafeMethods)-1].ExtendedReturn = et
|
|
if abim.ReturnType == smartcontract.ArrayType && len(et.Name) > 0 {
|
|
ctr.SafeMethods[len(ctr.SafeMethods)-1].ItemTo = cutPointer(ctr.Methods[i].ReturnType)
|
|
}
|
|
}
|
|
ctr.Methods = append(ctr.Methods[:i], ctr.Methods[i+1:]...)
|
|
i--
|
|
} else {
|
|
ctr.Methods[i].Comment = fmt.Sprintf("creates a transaction invoking `%s` method of the contract.", ctr.Methods[i].NameABI)
|
|
if ctr.Methods[i].ReturnType == "bool" {
|
|
imports["github.com/nspcc-dev/neo-go/pkg/smartcontract"] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
for _, et := range cfg.NamedTypes {
|
|
addETImports(et, ctr.NamedTypes, imports)
|
|
}
|
|
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 {
|
|
case "any":
|
|
abim := cfg.Manifest.ABI.GetMethod(ctr.SafeMethods[i].NameABI, len(ctr.SafeMethods[i].Arguments))
|
|
if abim.ReturnType == smartcontract.InteropInterfaceType {
|
|
imports["github.com/google/uuid"] = struct{}{}
|
|
imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{}
|
|
imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{}
|
|
ctr.SafeMethods[i].ReturnType = "uuid.UUID, result.Iterator"
|
|
ctr.SafeMethods[i].Unwrapper = "SessionIterator"
|
|
ctr.HasIterator = true
|
|
} else {
|
|
imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{}
|
|
ctr.SafeMethods[i].ReturnType = "any"
|
|
ctr.SafeMethods[i].Unwrapper = "Item"
|
|
}
|
|
case "bool":
|
|
ctr.SafeMethods[i].Unwrapper = "Bool"
|
|
case "*big.Int":
|
|
ctr.SafeMethods[i].Unwrapper = "BigInt"
|
|
case "string":
|
|
ctr.SafeMethods[i].Unwrapper = "UTF8String"
|
|
case "util.Uint160":
|
|
ctr.SafeMethods[i].Unwrapper = "Uint160"
|
|
case "util.Uint256":
|
|
ctr.SafeMethods[i].Unwrapper = "Uint256"
|
|
case "*keys.PublicKey":
|
|
ctr.SafeMethods[i].Unwrapper = "PublicKey"
|
|
case "[]byte":
|
|
ctr.SafeMethods[i].Unwrapper = "Bytes"
|
|
case "[]any":
|
|
imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{}
|
|
ctr.SafeMethods[i].ReturnType = "[]stackitem.Item"
|
|
ctr.SafeMethods[i].Unwrapper = "Array"
|
|
case "*stackitem.Map":
|
|
ctr.SafeMethods[i].Unwrapper = "Map"
|
|
case "[]bool":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfBools"
|
|
case "[]*big.Int":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfBigInts"
|
|
case "[][]byte":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfBytes"
|
|
case "[]string":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfUTF8Strings"
|
|
case "[]util.Uint160":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfUint160"
|
|
case "[]util.Uint256":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfUint256"
|
|
case "keys.PublicKeys":
|
|
ctr.SafeMethods[i].Unwrapper = "ArrayOfPublicKeys"
|
|
default:
|
|
addETImports(ctr.SafeMethods[i].ExtendedReturn, ctr.NamedTypes, imports)
|
|
ctr.SafeMethods[i].Unwrapper = "Item"
|
|
}
|
|
}
|
|
|
|
imports["github.com/nspcc-dev/neo-go/pkg/util"] = struct{}{}
|
|
if len(ctr.SafeMethods) > 0 {
|
|
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"] = struct{}{}
|
|
if !(ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND) {
|
|
imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{}
|
|
}
|
|
}
|
|
if len(ctr.Methods) > 0 {
|
|
imports["github.com/nspcc-dev/neo-go/pkg/core/transaction"] = struct{}{}
|
|
}
|
|
if len(ctr.Methods) > 0 || ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND {
|
|
ctr.HasWriter = true
|
|
}
|
|
if len(ctr.SafeMethods) > 0 || ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND {
|
|
ctr.HasReader = true
|
|
}
|
|
ctr.Imports = ctr.Imports[:0]
|
|
for imp := range imports {
|
|
ctr.Imports = append(ctr.Imports, imp)
|
|
}
|
|
sort.Strings(ctr.Imports)
|
|
return ctr
|
|
}
|
|
|
|
func addETImports(et binding.ExtendedType, named map[string]binding.ExtendedType, imports map[string]struct{}) {
|
|
_, pkg := extendedTypeToGo(et, named)
|
|
if pkg != "" {
|
|
imports[pkg] = struct{}{}
|
|
}
|
|
// Additional packages used during decoding.
|
|
switch et.Base {
|
|
case smartcontract.StringType:
|
|
imports["unicode/utf8"] = struct{}{}
|
|
imports["errors"] = struct{}{}
|
|
case smartcontract.PublicKeyType:
|
|
imports["crypto/elliptic"] = struct{}{}
|
|
case smartcontract.MapType:
|
|
imports["fmt"] = struct{}{}
|
|
case smartcontract.ArrayType:
|
|
imports["errors"] = struct{}{}
|
|
imports["fmt"] = struct{}{}
|
|
}
|
|
if et.Value != nil {
|
|
addETImports(*et.Value, named, imports)
|
|
}
|
|
if et.Base == smartcontract.MapType {
|
|
addETImports(binding.ExtendedType{Base: et.Key}, named, imports)
|
|
}
|
|
for i := range et.Fields {
|
|
addETImports(et.Fields[i].ExtendedType, named, imports)
|
|
}
|
|
}
|
|
|
|
func cutPointer(s string) string {
|
|
if s[0] == '*' {
|
|
return s[1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func toTypeName(s string) string {
|
|
return strings.Map(func(c rune) rune {
|
|
if c == '.' {
|
|
return -1
|
|
}
|
|
return c
|
|
}, strings.ToUpper(s[0:1])+s[1:])
|
|
}
|
|
|
|
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:]
|
|
}
|