github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/smartcontract/binding/generate.go (about)

     1  package binding
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/format"
     7  	"go/token"
     8  	"io"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"text/template"
    13  	"unicode"
    14  
    15  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    16  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
    17  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
    18  	"github.com/nspcc-dev/neo-go/pkg/util"
    19  )
    20  
    21  const srcTmpl = `
    22  {{- define "FUNCTION" -}}
    23  // {{.Name}} {{.Comment}}
    24  func {{.Name}}({{range $index, $arg := .Arguments -}}
    25  	{{- if ne $index 0}}, {{end}}
    26  		{{- .Name}} {{.Type}}
    27  	{{- end}}) {{if .ReturnType }}{{ .ReturnType }} {
    28  	return neogointernal.CallWithToken(Hash, "{{ .NameABI }}", int(contract.{{ .CallFlag }})
    29  		{{- range $arg := .Arguments -}}, {{.Name}}{{end}}).({{ .ReturnType }})
    30  	{{- else -}} {
    31  	neogointernal.CallWithTokenNoRet(Hash, "{{ .NameABI }}", int(contract.{{ .CallFlag }})
    32  		{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
    33  	{{- end}}
    34  }
    35  {{- end -}}
    36  {{- define "METHOD" -}}
    37  // {{.Name}} {{.Comment}}
    38  func (c Contract) {{.Name}}({{range $index, $arg := .Arguments -}}
    39  	{{- if ne $index 0}}, {{end}}
    40  		{{- .Name}} {{.Type}}
    41  	{{- end}}) {{if .ReturnType }}{{ .ReturnType}} {
    42  	return contract.Call(c.Hash, "{{ .NameABI }}", contract.{{ .CallFlag }}
    43  		{{- range $arg := .Arguments -}}, {{.Name}}{{end}}).({{ .ReturnType }})
    44  	{{- else -}} {
    45  	contract.Call(c.Hash, "{{ .NameABI }}", contract.{{ .CallFlag }}
    46  		{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
    47  	{{- end}}
    48  }
    49  {{- end -}}
    50  // Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.
    51  
    52  // Package {{.PackageName}} contains wrappers for {{.ContractName}} contract.
    53  package {{.PackageName}}
    54  
    55  import (
    56  {{range $m := .Imports}}	"{{ $m }}"
    57  {{end}})
    58  
    59  {{if .Hash}}
    60  // Hash contains contract hash in big-endian form.
    61  const Hash = "{{ .Hash }}"
    62  {{range $m := .Methods}}
    63  {{template "FUNCTION" $m }}
    64  {{end}}
    65  {{else}}
    66  // Contract represents the {{.ContractName}} smart contract.
    67  type Contract struct {
    68      Hash interop.Hash160
    69  }
    70  
    71  // NewContract returns a new Contract instance with the specified hash.
    72  func NewContract(hash interop.Hash160) Contract {
    73      return Contract{Hash: hash}
    74  }
    75  {{range $m := .Methods}}
    76  {{template "METHOD" $m }}
    77  {{end}}
    78  {{end}}`
    79  
    80  type (
    81  	// Config contains parameter for the generated binding.
    82  	Config struct {
    83  		Package  string             `yaml:"package,omitempty"`
    84  		Manifest *manifest.Manifest `yaml:"-"`
    85  		// Hash denotes the contract hash and is allowed to be empty for RPC bindings
    86  		// generation (if not provided by the user).
    87  		Hash      util.Uint160                 `yaml:"hash,omitempty"`
    88  		Overrides map[string]Override          `yaml:"overrides,omitempty"`
    89  		CallFlags map[string]callflag.CallFlag `yaml:"callflags,omitempty"`
    90  		// NamedTypes contains exported structured types that have some name (even
    91  		// if the original structure doesn't) and a number of internal fields. The
    92  		// map key is in the form of `namespace.name`, the value is fully-qualified
    93  		// and possibly nested description of the type structure.
    94  		NamedTypes map[string]ExtendedType `yaml:"namedtypes,omitempty"`
    95  		// Types contains type structure description for various types used in
    96  		// smartcontract. The map key has one of the following forms:
    97  		// - `methodName` for method return value;
    98  		// - `mathodName.paramName` for method's parameter value.
    99  		// - `eventName.paramName` for event's parameter value.
   100  		Types  map[string]ExtendedType `yaml:"types,omitempty"`
   101  		Output io.Writer               `yaml:"-"`
   102  	}
   103  
   104  	ExtendedType struct {
   105  		Base      smartcontract.ParamType `yaml:"base"`
   106  		Name      string                  `yaml:"name,omitempty"`      // Structure name, omitted for arrays, interfaces and maps.
   107  		Interface string                  `yaml:"interface,omitempty"` // Interface type name, "iterator" only for now.
   108  		Key       smartcontract.ParamType `yaml:"key,omitempty"`       // Key type (only simple types can be used for keys) for maps.
   109  		Value     *ExtendedType           `yaml:"value,omitempty"`     // Value type for iterators, arrays and maps.
   110  		Fields    []FieldExtendedType     `yaml:"fields,omitempty"`    // Ordered type data for structure fields.
   111  	}
   112  
   113  	FieldExtendedType struct {
   114  		Field        string `yaml:"field"`
   115  		ExtendedType `yaml:",inline"`
   116  	}
   117  
   118  	ContractTmpl struct {
   119  		PackageName  string
   120  		ContractName string
   121  		Imports      []string
   122  		Hash         string
   123  		Methods      []MethodTmpl
   124  	}
   125  
   126  	MethodTmpl struct {
   127  		Name       string
   128  		NameABI    string
   129  		CallFlag   string
   130  		Comment    string
   131  		Arguments  []ParamTmpl
   132  		ReturnType string
   133  	}
   134  
   135  	ParamTmpl struct {
   136  		Name string
   137  		Type string
   138  	}
   139  )
   140  
   141  var srcTemplate = template.Must(template.New("generate").Parse(srcTmpl))
   142  
   143  // NewConfig initializes and returns a new config instance.
   144  func NewConfig() Config {
   145  	return Config{
   146  		Overrides:  make(map[string]Override),
   147  		CallFlags:  make(map[string]callflag.CallFlag),
   148  		NamedTypes: make(map[string]ExtendedType),
   149  		Types:      make(map[string]ExtendedType),
   150  	}
   151  }
   152  
   153  // Generate writes Go file containing smartcontract bindings to the `cfg.Output`.
   154  // It doesn't check manifest from Config for validity, incorrect manifest can
   155  // lead to unexpected results.
   156  func Generate(cfg Config) error {
   157  	ctr := TemplateFromManifest(cfg, scTypeToGo)
   158  	ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/contract")
   159  	if ctr.Hash != "" {
   160  		ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal")
   161  	}
   162  	sort.Strings(ctr.Imports)
   163  
   164  	return FExecute(srcTemplate, cfg.Output, ctr)
   165  }
   166  
   167  // FExecute tries to execute given template over the data provided, apply gofmt
   168  // rules to the result and write the result to the provided io.Writer. If a
   169  // format error occurs while formatting the resulting binding, then the generated
   170  // binding is written "as is" and no error is returned.
   171  func FExecute(tmplt *template.Template, out io.Writer, data any) error {
   172  	in := bytes.NewBuffer(nil)
   173  	err := tmplt.Execute(in, data)
   174  	if err != nil {
   175  		return fmt.Errorf("failed to execute template: %w", err)
   176  	}
   177  	res := in.Bytes()
   178  
   179  	fmtRes, err := format.Source(res)
   180  	if err != nil {
   181  		// OK, still write something to the resulting file, our generator has known
   182  		// bugs that make the resulting code uncompilable.
   183  		fmtRes = res
   184  	}
   185  	_, err = out.Write(fmtRes)
   186  	if err != nil {
   187  		return fmt.Errorf("failed to write the resulting binding: %w", err)
   188  	}
   189  	return nil
   190  }
   191  
   192  func scTypeToGo(name string, typ smartcontract.ParamType, cfg *Config) (string, string) {
   193  	if over, ok := cfg.Overrides[name]; ok {
   194  		return over.TypeName, over.Package
   195  	}
   196  
   197  	switch typ {
   198  	case smartcontract.AnyType:
   199  		return "any", ""
   200  	case smartcontract.BoolType:
   201  		return "bool", ""
   202  	case smartcontract.IntegerType:
   203  		return "int", ""
   204  	case smartcontract.ByteArrayType:
   205  		return "[]byte", ""
   206  	case smartcontract.StringType:
   207  		return "string", ""
   208  	case smartcontract.Hash160Type:
   209  		return "interop.Hash160", "github.com/nspcc-dev/neo-go/pkg/interop"
   210  	case smartcontract.Hash256Type:
   211  		return "interop.Hash256", "github.com/nspcc-dev/neo-go/pkg/interop"
   212  	case smartcontract.PublicKeyType:
   213  		return "interop.PublicKey", "github.com/nspcc-dev/neo-go/pkg/interop"
   214  	case smartcontract.SignatureType:
   215  		return "interop.Signature", "github.com/nspcc-dev/neo-go/pkg/interop"
   216  	case smartcontract.ArrayType:
   217  		return "[]any", ""
   218  	case smartcontract.MapType:
   219  		return "map[string]any", ""
   220  	case smartcontract.InteropInterfaceType:
   221  		return "any", ""
   222  	case smartcontract.VoidType:
   223  		return "", ""
   224  	default:
   225  		panic("unreachable")
   226  	}
   227  }
   228  
   229  // TemplateFromManifest create a contract template using the given configuration
   230  // and type conversion function. It assumes manifest to be present in the
   231  // configuration and assumes it to be correct (passing IsValid check).
   232  func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract.ParamType, *Config) (string, string)) ContractTmpl {
   233  	var hStr string
   234  	if !cfg.Hash.Equals(util.Uint160{}) {
   235  		for _, b := range cfg.Hash.BytesBE() {
   236  			hStr += fmt.Sprintf("\\x%02x", b)
   237  		}
   238  	}
   239  
   240  	ctr := ContractTmpl{
   241  		PackageName:  cfg.Package,
   242  		ContractName: cfg.Manifest.Name,
   243  		Hash:         hStr,
   244  	}
   245  	if ctr.PackageName == "" {
   246  		buf := bytes.NewBuffer(make([]byte, 0, len(cfg.Manifest.Name)))
   247  		for _, r := range cfg.Manifest.Name {
   248  			if unicode.IsLetter(r) {
   249  				buf.WriteRune(unicode.ToLower(r))
   250  			}
   251  		}
   252  
   253  		ctr.PackageName = buf.String()
   254  	}
   255  
   256  	imports := make(map[string]struct{})
   257  	seen := make(map[string]bool)
   258  	for _, m := range cfg.Manifest.ABI.Methods {
   259  		seen[m.Name] = false
   260  	}
   261  	for _, m := range cfg.Manifest.ABI.Methods {
   262  		if m.Name[0] == '_' {
   263  			continue
   264  		}
   265  
   266  		// Consider `perform(a)` and `perform(a, b)` methods.
   267  		// First, try to export the second method with `Perform2` name.
   268  		// If `perform2` is already in the manifest, use `perform3` with uprising suffix as many times
   269  		// as needed to eliminate name conflicts. If `perform3` is already in the manifest, use `perform4` etc.
   270  		name := m.Name
   271  		for suffix := 2; seen[name]; suffix++ {
   272  			name = m.Name + strconv.Itoa(suffix)
   273  		}
   274  		seen[name] = true
   275  
   276  		mtd := MethodTmpl{
   277  			Name:     upperFirst(name),
   278  			NameABI:  m.Name,
   279  			CallFlag: callflag.All.String(),
   280  			Comment:  fmt.Sprintf("invokes `%s` method of contract.", m.Name),
   281  		}
   282  		if f, ok := cfg.CallFlags[m.Name]; ok {
   283  			mtd.CallFlag = f.String()
   284  		} else if m.Safe {
   285  			mtd.CallFlag = callflag.ReadOnly.String()
   286  		}
   287  
   288  		var varnames = make(map[string]bool)
   289  		for i := range m.Parameters {
   290  			name := m.Parameters[i].Name
   291  			typeStr, pkg := scTypeConverter(m.Name+"."+name, m.Parameters[i].Type, &cfg)
   292  			if pkg != "" {
   293  				imports[pkg] = struct{}{}
   294  			}
   295  			if token.IsKeyword(name) {
   296  				name = name + "v"
   297  			}
   298  			for varnames[name] {
   299  				name = name + "_"
   300  			}
   301  			varnames[name] = true
   302  			mtd.Arguments = append(mtd.Arguments, ParamTmpl{
   303  				Name: name,
   304  				Type: typeStr,
   305  			})
   306  		}
   307  
   308  		typeStr, pkg := scTypeConverter(m.Name, m.ReturnType, &cfg)
   309  		if pkg != "" {
   310  			imports[pkg] = struct{}{}
   311  		}
   312  		mtd.ReturnType = typeStr
   313  		ctr.Methods = append(ctr.Methods, mtd)
   314  	}
   315  
   316  	for imp := range imports {
   317  		ctr.Imports = append(ctr.Imports, imp)
   318  	}
   319  
   320  	return ctr
   321  }
   322  
   323  func upperFirst(s string) string {
   324  	return strings.ToUpper(s[0:1]) + s[1:]
   325  }
   326  
   327  // Equals compares two extended types field-by-field and returns true if they are
   328  // equal.
   329  func (e *ExtendedType) Equals(other *ExtendedType) bool {
   330  	if e == nil && other == nil {
   331  		return true
   332  	}
   333  	if e != nil && other == nil ||
   334  		e == nil && other != nil {
   335  		return false
   336  	}
   337  	if !((e.Base == other.Base || (e.Base == smartcontract.ByteArrayType || e.Base == smartcontract.StringType) &&
   338  		(other.Base == smartcontract.ByteArrayType || other.Base == smartcontract.StringType)) &&
   339  		e.Name == other.Name &&
   340  		e.Interface == other.Interface &&
   341  		e.Key == other.Key) {
   342  		return false
   343  	}
   344  	if len(e.Fields) != len(other.Fields) {
   345  		return false
   346  	}
   347  	for i := range e.Fields {
   348  		if e.Fields[i].Field != other.Fields[i].Field {
   349  			return false
   350  		}
   351  		if !e.Fields[i].ExtendedType.Equals(&other.Fields[i].ExtendedType) {
   352  			return false
   353  		}
   354  	}
   355  	return (e.Value == nil && other.Value == nil) || (e.Value != nil && other.Value != nil && e.Value.Equals(other.Value))
   356  }