c5dbecb754
Allow dynamic contract hash for contract bindings. Close #3007 Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
356 lines
11 KiB
Go
356 lines
11 KiB
Go
package binding
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/format"
|
|
"go/token"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"unicode"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
)
|
|
|
|
const srcTmpl = `
|
|
{{- define "FUNCTION" -}}
|
|
// {{.Name}} {{.Comment}}
|
|
func {{.Name}}({{range $index, $arg := .Arguments -}}
|
|
{{- if ne $index 0}}, {{end}}
|
|
{{- .Name}} {{.Type}}
|
|
{{- end}}) {{if .ReturnType }}{{ .ReturnType }} {
|
|
return neogointernal.CallWithToken(Hash, "{{ .NameABI }}", int(contract.{{ .CallFlag }})
|
|
{{- range $arg := .Arguments -}}, {{.Name}}{{end}}).({{ .ReturnType }})
|
|
{{- else -}} {
|
|
neogointernal.CallWithTokenNoRet(Hash, "{{ .NameABI }}", int(contract.{{ .CallFlag }})
|
|
{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
|
|
{{- end}}
|
|
}
|
|
{{- end -}}
|
|
{{- define "METHOD" -}}
|
|
// {{.Name}} {{.Comment}}
|
|
func (c Contract) {{.Name}}({{range $index, $arg := .Arguments -}}
|
|
{{- if ne $index 0}}, {{end}}
|
|
{{- .Name}} {{.Type}}
|
|
{{- end}}) {{if .ReturnType }}{{ .ReturnType}} {
|
|
return contract.Call(c.Hash, "{{ .NameABI }}", contract.{{ .CallFlag }}
|
|
{{- range $arg := .Arguments -}}, {{.Name}}{{end}}).({{ .ReturnType }})
|
|
{{- else -}} {
|
|
contract.Call(c.Hash, "{{ .NameABI }}", contract.{{ .CallFlag }}
|
|
{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
|
|
{{- end}}
|
|
}
|
|
{{- end -}}
|
|
// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.
|
|
|
|
// Package {{.PackageName}} contains wrappers for {{.ContractName}} contract.
|
|
package {{.PackageName}}
|
|
|
|
import (
|
|
{{range $m := .Imports}} "{{ $m }}"
|
|
{{end}})
|
|
|
|
{{if .Hash}}
|
|
// Hash contains contract hash in big-endian form.
|
|
const Hash = "{{ .Hash }}"
|
|
{{range $m := .Methods}}
|
|
{{template "FUNCTION" $m }}
|
|
{{end}}
|
|
{{else}}
|
|
// Contract represents the {{.ContractName}} smart contract.
|
|
type Contract struct {
|
|
Hash interop.Hash160
|
|
}
|
|
|
|
// NewContract returns a new Contract instance with the specified hash.
|
|
func NewContract(hash interop.Hash160) Contract {
|
|
return Contract{Hash: hash}
|
|
}
|
|
{{range $m := .Methods}}
|
|
{{template "METHOD" $m }}
|
|
{{end}}
|
|
{{end}}`
|
|
|
|
type (
|
|
// Config contains parameter for the generated binding.
|
|
Config struct {
|
|
Package string `yaml:"package,omitempty"`
|
|
Manifest *manifest.Manifest `yaml:"-"`
|
|
// Hash denotes the contract hash and is allowed to be empty for RPC bindings
|
|
// generation (if not provided by the user).
|
|
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:"-"`
|
|
}
|
|
|
|
ExtendedType struct {
|
|
Base smartcontract.ParamType `yaml:"base"`
|
|
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, arrays and maps.
|
|
Fields []FieldExtendedType `yaml:"fields,omitempty"` // Ordered type data for structure fields.
|
|
}
|
|
|
|
FieldExtendedType struct {
|
|
Field string `yaml:"field"`
|
|
ExtendedType `yaml:",inline"`
|
|
}
|
|
|
|
ContractTmpl struct {
|
|
PackageName string
|
|
ContractName string
|
|
Imports []string
|
|
Hash string
|
|
Methods []MethodTmpl
|
|
}
|
|
|
|
MethodTmpl struct {
|
|
Name string
|
|
NameABI string
|
|
CallFlag string
|
|
Comment string
|
|
Arguments []ParamTmpl
|
|
ReturnType string
|
|
}
|
|
|
|
ParamTmpl struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
)
|
|
|
|
var srcTemplate = template.Must(template.New("generate").Parse(srcTmpl))
|
|
|
|
// NewConfig initializes and returns a new config instance.
|
|
func NewConfig() Config {
|
|
return Config{
|
|
Overrides: make(map[string]Override),
|
|
CallFlags: make(map[string]callflag.CallFlag),
|
|
NamedTypes: make(map[string]ExtendedType),
|
|
Types: make(map[string]ExtendedType),
|
|
}
|
|
}
|
|
|
|
// 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 Config) error {
|
|
ctr := TemplateFromManifest(cfg, scTypeToGo)
|
|
ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/contract")
|
|
if ctr.Hash != "" {
|
|
ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal")
|
|
}
|
|
sort.Strings(ctr.Imports)
|
|
|
|
return FExecute(srcTemplate, cfg.Output, ctr)
|
|
}
|
|
|
|
// FExecute tries to execute given template over the data provided, apply gofmt
|
|
// rules to the result and write the result to the provided io.Writer. If a
|
|
// format error occurs while formatting the resulting binding, then the generated
|
|
// binding is written "as is" and no error is returned.
|
|
func FExecute(tmplt *template.Template, out io.Writer, data any) error {
|
|
in := bytes.NewBuffer(nil)
|
|
err := tmplt.Execute(in, data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute template: %w", err)
|
|
}
|
|
res := in.Bytes()
|
|
|
|
fmtRes, err := format.Source(res)
|
|
if err != nil {
|
|
// OK, still write something to the resulting file, our generator has known
|
|
// bugs that make the resulting code uncompilable.
|
|
fmtRes = res
|
|
}
|
|
_, err = out.Write(fmtRes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write the resulting binding: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scTypeToGo(name string, typ smartcontract.ParamType, cfg *Config) (string, string) {
|
|
if over, ok := cfg.Overrides[name]; ok {
|
|
return over.TypeName, over.Package
|
|
}
|
|
|
|
switch typ {
|
|
case smartcontract.AnyType:
|
|
return "any", ""
|
|
case smartcontract.BoolType:
|
|
return "bool", ""
|
|
case smartcontract.IntegerType:
|
|
return "int", ""
|
|
case smartcontract.ByteArrayType:
|
|
return "[]byte", ""
|
|
case smartcontract.StringType:
|
|
return "string", ""
|
|
case smartcontract.Hash160Type:
|
|
return "interop.Hash160", "github.com/nspcc-dev/neo-go/pkg/interop"
|
|
case smartcontract.Hash256Type:
|
|
return "interop.Hash256", "github.com/nspcc-dev/neo-go/pkg/interop"
|
|
case smartcontract.PublicKeyType:
|
|
return "interop.PublicKey", "github.com/nspcc-dev/neo-go/pkg/interop"
|
|
case smartcontract.SignatureType:
|
|
return "interop.Signature", "github.com/nspcc-dev/neo-go/pkg/interop"
|
|
case smartcontract.ArrayType:
|
|
return "[]any", ""
|
|
case smartcontract.MapType:
|
|
return "map[string]any", ""
|
|
case smartcontract.InteropInterfaceType:
|
|
return "any", ""
|
|
case smartcontract.VoidType:
|
|
return "", ""
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
|
|
// TemplateFromManifest create a contract template using the given configuration
|
|
// and type conversion function. It assumes manifest to be present in the
|
|
// configuration and assumes it to be correct (passing IsValid check).
|
|
func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract.ParamType, *Config) (string, string)) ContractTmpl {
|
|
var hStr string
|
|
if !cfg.Hash.Equals(util.Uint160{}) {
|
|
for _, b := range cfg.Hash.BytesBE() {
|
|
hStr += fmt.Sprintf("\\x%02x", b)
|
|
}
|
|
}
|
|
|
|
ctr := ContractTmpl{
|
|
PackageName: cfg.Package,
|
|
ContractName: cfg.Manifest.Name,
|
|
Hash: hStr,
|
|
}
|
|
if ctr.PackageName == "" {
|
|
buf := bytes.NewBuffer(make([]byte, 0, len(cfg.Manifest.Name)))
|
|
for _, r := range cfg.Manifest.Name {
|
|
if unicode.IsLetter(r) {
|
|
buf.WriteRune(unicode.ToLower(r))
|
|
}
|
|
}
|
|
|
|
ctr.PackageName = buf.String()
|
|
}
|
|
|
|
imports := make(map[string]struct{})
|
|
seen := make(map[string]bool)
|
|
for _, m := range cfg.Manifest.ABI.Methods {
|
|
seen[m.Name] = false
|
|
}
|
|
for _, m := range cfg.Manifest.ABI.Methods {
|
|
if m.Name[0] == '_' {
|
|
continue
|
|
}
|
|
|
|
// Consider `perform(a)` and `perform(a, b)` methods.
|
|
// First, try to export the second method with `Perform2` name.
|
|
// If `perform2` is already in the manifest, use `perform3` with uprising suffix as many times
|
|
// as needed to eliminate name conflicts. If `perform3` is already in the manifest, use `perform4` etc.
|
|
name := m.Name
|
|
for suffix := 2; seen[name]; suffix++ {
|
|
name = m.Name + strconv.Itoa(suffix)
|
|
}
|
|
seen[name] = true
|
|
|
|
mtd := MethodTmpl{
|
|
Name: upperFirst(name),
|
|
NameABI: m.Name,
|
|
CallFlag: callflag.All.String(),
|
|
Comment: fmt.Sprintf("invokes `%s` method of contract.", m.Name),
|
|
}
|
|
if f, ok := cfg.CallFlags[m.Name]; ok {
|
|
mtd.CallFlag = f.String()
|
|
} else if m.Safe {
|
|
mtd.CallFlag = callflag.ReadOnly.String()
|
|
}
|
|
|
|
var varnames = make(map[string]bool)
|
|
for i := range m.Parameters {
|
|
name := m.Parameters[i].Name
|
|
typeStr, pkg := scTypeConverter(m.Name+"."+name, m.Parameters[i].Type, &cfg)
|
|
if pkg != "" {
|
|
imports[pkg] = struct{}{}
|
|
}
|
|
if token.IsKeyword(name) {
|
|
name = name + "v"
|
|
}
|
|
for varnames[name] {
|
|
name = name + "_"
|
|
}
|
|
varnames[name] = true
|
|
mtd.Arguments = append(mtd.Arguments, ParamTmpl{
|
|
Name: name,
|
|
Type: typeStr,
|
|
})
|
|
}
|
|
|
|
typeStr, pkg := scTypeConverter(m.Name, m.ReturnType, &cfg)
|
|
if pkg != "" {
|
|
imports[pkg] = struct{}{}
|
|
}
|
|
mtd.ReturnType = typeStr
|
|
ctr.Methods = append(ctr.Methods, mtd)
|
|
}
|
|
|
|
for imp := range imports {
|
|
ctr.Imports = append(ctr.Imports, imp)
|
|
}
|
|
|
|
return ctr
|
|
}
|
|
|
|
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))
|
|
}
|