-
Notifications
You must be signed in to change notification settings - Fork 77
/
generate.go
356 lines (323 loc) · 10.9 KB
/
generate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
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))
}