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 }