diff --git a/cli/contract_test.go b/cli/contract_test.go index 7ecacd70d..6ba7be18b 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/hex" "encoding/json" "io/ioutil" @@ -91,6 +92,116 @@ func TestCalcHash(t *testing.T) { }) } +func TestContractBindings(t *testing.T) { + // For proper nef generation. + config.Version = "v0.98.1-test" + + // For proper contract init. The actual version as it will be replaced. + smartcontract.ModVersion = "v0.0.0" + + tmpDir := t.TempDir() + e := newExecutor(t, false) + + ctrPath := filepath.Join(tmpDir, "testcontract") + e.Run(t, "neo-go", "contract", "init", "--name", ctrPath) + + srcPath := filepath.Join(ctrPath, "main.go") + require.NoError(t, ioutil.WriteFile(srcPath, []byte(`package testcontract +import( + alias "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" +) +type MyPair struct { + Key int + Value string +} +func ToMap(a []MyPair) map[int]string { + return nil +} +func ToArray(m map[int]string) []MyPair { + return nil +} +func Block() *alias.Block{ + return alias.GetBlock(1) +} +func Blocks() []*alias.Block { + return []*alias.Block{ + alias.GetBlock(10), + alias.GetBlock(11), + } +} +`), os.ModePerm)) + + cfgPath := filepath.Join(ctrPath, "neo-go.yml") + manifestPath := filepath.Join(tmpDir, "manifest.json") + bindingsPath := filepath.Join(tmpDir, "bindings.yml") + cmd := []string{"neo-go", "contract", "compile"} + + cmd = append(cmd, "--in", ctrPath, "--bindings", bindingsPath) + + // Replace `pkg/interop` in go.mod to avoid getting an actual module version. + goMod := filepath.Join(ctrPath, "go.mod") + data, err := ioutil.ReadFile(goMod) + require.NoError(t, err) + + i := bytes.IndexByte(data, '\n') + data = append([]byte("module myimport.com/testcontract"), data[i:]...) + + wd, err := os.Getwd() + require.NoError(t, err) + data = append(data, "\nreplace github.com/nspcc-dev/neo-go/pkg/interop => "...) + data = append(data, filepath.Join(wd, "../pkg/interop")...) + require.NoError(t, ioutil.WriteFile(goMod, data, os.ModePerm)) + + cmd = append(cmd, "--config", cfgPath, + "--out", filepath.Join(tmpDir, "out.nef"), + "--manifest", manifestPath, + "--bindings", bindingsPath) + e.Run(t, cmd...) + e.checkEOF(t) + require.FileExists(t, bindingsPath) + + outPath := filepath.Join(t.TempDir(), "binding.go") + e.Run(t, "neo-go", "contract", "generate-wrapper", + "--config", bindingsPath, "--manifest", manifestPath, + "--out", outPath, "--hash", "0x0123456789987654321001234567899876543210") + + bs, err := ioutil.ReadFile(outPath) + require.NoError(t, err) + require.Equal(t, `// Package testcontract contains wrappers for testcontract contract. +package testcontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" + "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal" + "myimport.com/testcontract" +) + +// Hash contains contract hash in big-endian form. +const Hash = "\x10\x32\x54\x76\x98\x89\x67\x45\x23\x01\x10\x32\x54\x76\x98\x89\x67\x45\x23\x01" + +// Block invokes `+"`block`"+` method of contract. +func Block() *ledger.Block { + return neogointernal.CallWithToken(Hash, "block", int(contract.All)).(*ledger.Block) +} + +// Blocks invokes `+"`blocks`"+` method of contract. +func Blocks() []*ledger.Block { + return neogointernal.CallWithToken(Hash, "blocks", int(contract.All)).([]*ledger.Block) +} + +// ToArray invokes `+"`toArray`"+` method of contract. +func ToArray(m map[int]string) []testcontract.MyPair { + return neogointernal.CallWithToken(Hash, "toArray", int(contract.All), m).([]testcontract.MyPair) +} + +// ToMap invokes `+"`toMap`"+` method of contract. +func ToMap(a []testcontract.MyPair) map[int]string { + return neogointernal.CallWithToken(Hash, "toMap", int(contract.All), a).(map[int]string) +} +`, string(bs)) +} + func TestContractInitAndCompile(t *testing.T) { // For proper nef generation. config.Version = "v0.98.1-test" diff --git a/cli/smartcontract/generate.go b/cli/smartcontract/generate.go index c3e627866..7c9d3e493 100644 --- a/cli/smartcontract/generate.go +++ b/cli/smartcontract/generate.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "github.com/nspcc-dev/neo-go/pkg/smartcontract/binding" "github.com/nspcc-dev/neo-go/pkg/util" @@ -58,7 +59,7 @@ func contractGenerateWrapper(ctx *cli.Context) error { cfg.Manifest = m - h, err := util.Uint160DecodeStringLE(ctx.String("hash")) + h, err := util.Uint160DecodeStringLE(strings.TrimPrefix(ctx.String("hash"), "0x")) if err != nil { return cli.NewExitError(fmt.Errorf("invalid contract hash: %w", err), 1) } diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index 83d455209..28e04c624 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -256,7 +256,7 @@ func TestGenerateValidPackageName(t *testing.T) { require.NoError(t, app.Run([]string{"", "generate-wrapper", "--manifest", manifestFile, "--out", outFile, - "--hash", h.StringLE(), + "--hash", "0x" + h.StringLE(), })) data, err := ioutil.ReadFile(outFile) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 9e99b8686..6bd34b6df 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -166,6 +166,10 @@ func NewCommands() []cli.Command { Name: "no-permissions", Usage: "do not check if invoked contracts are allowed in manifest", }, + cli.StringFlag{ + Name: "bindings", + Usage: "output file for smart-contract bindings configuration", + }, }, }, { @@ -495,6 +499,7 @@ func contractCompile(ctx *cli.Context) error { DebugInfo: debugFile, ManifestFile: manifestFile, + BindingsFile: ctx.String("bindings"), NoStandardCheck: ctx.Bool("no-standards"), NoEventsCheck: ctx.Bool("no-events"), diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 4e7ac96aa..88655b188 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -855,7 +855,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { c.convertMap(n) default: if tn, ok := t.(*types.Named); ok && isInteropPath(tn.String()) { - st, _ := scAndVMInteropTypeFromExpr(tn) + st, _, _ := scAndVMInteropTypeFromExpr(tn, false) expectedLen := -1 switch st { case smartcontract.Hash160Type: diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 47ba5db21..ff8e664c9 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -15,11 +15,13 @@ import ( "runtime" "strings" + "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" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "golang.org/x/tools/go/packages" + "gopkg.in/yaml.v2" ) const fileExt = "nef" @@ -72,6 +74,9 @@ type Options struct { // Permissions is a list of permissions for every contract method. Permissions []manifest.Permission + + // BindingsFile contains configuration for smart-contract bindings generator. + BindingsFile string } type buildInfo struct { @@ -258,7 +263,7 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { if err != nil { return f.Script, err } - if o.DebugInfo == "" && o.ManifestFile == "" { + if o.DebugInfo == "" && o.ManifestFile == "" && o.BindingsFile == "" { return f.Script, nil } @@ -289,6 +294,29 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { } } + if o.BindingsFile != "" { + cfg := binding.NewConfig() + cfg.Package = di.MainPkg + for _, m := range di.Methods { + for _, p := range m.Parameters { + if p.RealType.TypeName != "" { + cfg.Overrides[m.Name.Name+"."+p.Name] = p.RealType + } + } + if m.ReturnTypeReal.TypeName != "" { + cfg.Overrides[m.Name.Name] = m.ReturnTypeReal + } + } + data, err := yaml.Marshal(&cfg) + if err != nil { + return nil, fmt.Errorf("can't marshal bindings configuration: %w", err) + } + err = ioutil.WriteFile(o.BindingsFile, data, os.ModePerm) + if err != nil { + return nil, fmt.Errorf("can't write bindings configuration: %w", err) + } + } + if o.ManifestFile != "" { m, err := CreateManifest(di, o) if err != nil { diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index b8bf2b306..2389ba524 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -6,12 +6,14 @@ import ( "fmt" "go/ast" "go/types" + "sort" "strconv" "strings" "unicode" "unicode/utf8" "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/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -49,6 +51,8 @@ type MethodDebugInfo struct { Parameters []DebugParam `json:"params"` // ReturnType is method's return type. ReturnType string `json:"return"` + // ReturnTypeReal is method's return type as specified in Go code. + ReturnTypeReal binding.Override `json:"-"` // ReturnTypeSC is return type to use in manifest. ReturnTypeSC smartcontract.ParamType `json:"-"` Variables []string `json:"variables"` @@ -94,9 +98,10 @@ type DebugRange struct { // DebugParam represents variables's name and type. type DebugParam struct { - Name string `json:"name"` - Type string `json:"type"` - TypeSC smartcontract.ParamType `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + RealType binding.Override `json:"-"` + TypeSC smartcontract.ParamType `json:"-"` } func (c *codegen) saveSequencePoint(n ast.Node) { @@ -175,6 +180,8 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo { Variables: c.deployVariables, }) } + + start := len(d.Methods) for name, scope := range c.funcs { m := c.methodInfoFromScope(name, scope) if m.Range.Start == m.Range.End { @@ -182,13 +189,16 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo { } d.Methods = append(d.Methods, *m) } + sort.Slice(d.Methods[start:], func(i, j int) bool { + return d.Methods[start+i].Name.Name < d.Methods[start+j].Name.Name + }) d.EmittedEvents = c.emittedEvents d.InvokedContracts = c.invokedContracts return d } func (c *codegen) registerDebugVariable(name string, expr ast.Expr) { - _, vt := c.scAndVMTypeFromExpr(expr) + _, vt, _ := c.scAndVMTypeFromExpr(expr) if c.scope == nil { c.staticVariables = append(c.staticVariables, name+","+vt.String()) return @@ -201,106 +211,151 @@ func (c *codegen) methodInfoFromScope(name string, scope *funcScope) *MethodDebu params := make([]DebugParam, 0, ps.NumFields()) for i := range ps.List { for j := range ps.List[i].Names { - st, vt := c.scAndVMTypeFromExpr(ps.List[i].Type) + st, vt, rt := c.scAndVMTypeFromExpr(ps.List[i].Type) params = append(params, DebugParam{ - Name: ps.List[i].Names[j].Name, - Type: vt.String(), - TypeSC: st, + Name: ps.List[i].Names[j].Name, + Type: vt.String(), + RealType: rt, + TypeSC: st, }) } } ss := strings.Split(name, ".") name = ss[len(ss)-1] r, n := utf8.DecodeRuneInString(name) - st, vt := c.scAndVMReturnTypeFromScope(scope) + st, vt, rt := c.scAndVMReturnTypeFromScope(scope) + return &MethodDebugInfo{ ID: name, Name: DebugMethodName{ Name: string(unicode.ToLower(r)) + name[n:], Namespace: scope.pkg.Name(), }, - IsExported: scope.decl.Name.IsExported(), - IsFunction: scope.decl.Recv == nil, - Range: scope.rng, - Parameters: params, - ReturnType: vt, - ReturnTypeSC: st, - SeqPoints: c.sequencePoints[name], - Variables: scope.variables, + IsExported: scope.decl.Name.IsExported(), + IsFunction: scope.decl.Recv == nil, + Range: scope.rng, + Parameters: params, + ReturnType: vt, + ReturnTypeReal: rt, + ReturnTypeSC: st, + SeqPoints: c.sequencePoints[name], + Variables: scope.variables, } } -func (c *codegen) scAndVMReturnTypeFromScope(scope *funcScope) (smartcontract.ParamType, string) { +func (c *codegen) scAndVMReturnTypeFromScope(scope *funcScope) (smartcontract.ParamType, string, binding.Override) { results := scope.decl.Type.Results switch results.NumFields() { case 0: - return smartcontract.VoidType, "Void" + return smartcontract.VoidType, "Void", binding.Override{} case 1: - st, vt := c.scAndVMTypeFromExpr(results.List[0].Type) - return st, vt.String() + st, vt, s := c.scAndVMTypeFromExpr(results.List[0].Type) + return st, vt.String(), s default: // multiple return values are not supported in debugger - return smartcontract.AnyType, "Any" + return smartcontract.AnyType, "Any", binding.Override{} } } -func scAndVMInteropTypeFromExpr(named *types.Named) (smartcontract.ParamType, stackitem.Type) { +func scAndVMInteropTypeFromExpr(named *types.Named, isPointer bool) (smartcontract.ParamType, stackitem.Type, binding.Override) { name := named.Obj().Name() pkg := named.Obj().Pkg().Name() switch pkg { case "ledger", "contract": - return smartcontract.ArrayType, stackitem.ArrayT // Block, Transaction, Contract + typeName := pkg + "." + name + if isPointer { + typeName = "*" + typeName + } + return smartcontract.ArrayType, stackitem.ArrayT, binding.Override{ + Package: named.Obj().Pkg().Path(), + TypeName: typeName, + } // Block, Transaction, Contract case "interop": if name != "Interface" { + over := binding.Override{ + Package: interopPrefix, + TypeName: "interop." + name, + } switch name { case "Hash160": - return smartcontract.Hash160Type, stackitem.ByteArrayT + return smartcontract.Hash160Type, stackitem.ByteArrayT, over case "Hash256": - return smartcontract.Hash256Type, stackitem.ByteArrayT + return smartcontract.Hash256Type, stackitem.ByteArrayT, over case "PublicKey": - return smartcontract.PublicKeyType, stackitem.ByteArrayT + return smartcontract.PublicKeyType, stackitem.ByteArrayT, over case "Signature": - return smartcontract.SignatureType, stackitem.ByteArrayT + return smartcontract.SignatureType, stackitem.ByteArrayT, over } } } - return smartcontract.InteropInterfaceType, stackitem.InteropT + return smartcontract.InteropInterfaceType, stackitem.InteropT, binding.Override{TypeName: "interface{}"} } -func (c *codegen) scAndVMTypeFromExpr(typ ast.Expr) (smartcontract.ParamType, stackitem.Type) { - t := c.typeOf(typ) - if c.typeOf(typ) == nil { - return smartcontract.AnyType, stackitem.AnyT +func (c *codegen) scAndVMTypeFromExpr(typ ast.Expr) (smartcontract.ParamType, stackitem.Type, binding.Override) { + return c.scAndVMTypeFromType(c.typeOf(typ)) +} + +func (c *codegen) scAndVMTypeFromType(t types.Type) (smartcontract.ParamType, stackitem.Type, binding.Override) { + if t == nil { + return smartcontract.AnyType, stackitem.AnyT, binding.Override{TypeName: "interface{}"} } - if named, ok := t.(*types.Named); ok { - if isInteropPath(named.String()) { - return scAndVMInteropTypeFromExpr(named) + + var isPtr bool + + named, isNamed := t.(*types.Named) + if !isNamed { + var ptr *types.Pointer + if ptr, isPtr = t.(*types.Pointer); isPtr { + named, isNamed = ptr.Elem().(*types.Named) } } + if isNamed { + if isInteropPath(named.String()) { + return scAndVMInteropTypeFromExpr(named, isPtr) + } + } + + var over binding.Override switch t := t.Underlying().(type) { case *types.Basic: info := t.Info() switch { case info&types.IsInteger != 0: - return smartcontract.IntegerType, stackitem.IntegerT + over.TypeName = "int" + return smartcontract.IntegerType, stackitem.IntegerT, over case info&types.IsBoolean != 0: - return smartcontract.BoolType, stackitem.BooleanT + over.TypeName = "bool" + return smartcontract.BoolType, stackitem.BooleanT, over case info&types.IsString != 0: - return smartcontract.StringType, stackitem.ByteArrayT + over.TypeName = "string" + return smartcontract.StringType, stackitem.ByteArrayT, over default: - return smartcontract.AnyType, stackitem.AnyT + over.TypeName = "interface{}" + return smartcontract.AnyType, stackitem.AnyT, over } case *types.Map: - return smartcontract.MapType, stackitem.MapT + _, _, over := c.scAndVMTypeFromType(t.Elem()) + over.TypeName = "map[" + t.Key().String() + "]" + over.TypeName + return smartcontract.MapType, stackitem.MapT, over case *types.Struct: - return smartcontract.ArrayType, stackitem.StructT + if isNamed { + over.Package = named.Obj().Pkg().Path() + over.TypeName = named.Obj().Pkg().Name() + "." + named.Obj().Name() + } + return smartcontract.ArrayType, stackitem.StructT, over case *types.Slice: if isByte(t.Elem()) { - return smartcontract.ByteArrayType, stackitem.ByteArrayT + over.TypeName = "[]byte" + return smartcontract.ByteArrayType, stackitem.ByteArrayT, over } - return smartcontract.ArrayType, stackitem.ArrayT + _, _, over := c.scAndVMTypeFromType(t.Elem()) + if over.TypeName != "" { + over.TypeName = "[]" + over.TypeName + } + return smartcontract.ArrayType, stackitem.ArrayT, over default: - return smartcontract.AnyType, stackitem.AnyT + over.TypeName = "interface{}" + return smartcontract.AnyType, stackitem.AnyT, over } } diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go index b52cde0b9..dbe382561 100644 --- a/pkg/compiler/debug_test.go +++ b/pkg/compiler/debug_test.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neo-go/internal/testserdes" "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/vm/opcode" "github.com/stretchr/testify/assert" @@ -124,30 +125,45 @@ func _deploy(data interface{}, isUpdate bool) { x := 1; _ = x } }, }, "MethodInt": {{ - Name: "a", - Type: "ByteString", + Name: "a", + Type: "ByteString", + RealType: binding.Override{ + TypeName: "string", + }, TypeSC: smartcontract.StringType, }}, "MethodConcat": { { - Name: "a", - Type: "ByteString", + Name: "a", + Type: "ByteString", + RealType: binding.Override{ + TypeName: "string", + }, TypeSC: smartcontract.StringType, }, { - Name: "b", - Type: "ByteString", + Name: "b", + Type: "ByteString", + RealType: binding.Override{ + TypeName: "string", + }, TypeSC: smartcontract.StringType, }, { - Name: "c", - Type: "ByteString", + Name: "c", + Type: "ByteString", + RealType: binding.Override{ + TypeName: "string", + }, TypeSC: smartcontract.StringType, }, }, "Main": {{ - Name: "op", - Type: "ByteString", + Name: "op", + Type: "ByteString", + RealType: binding.Override{ + TypeName: "string", + }, TypeSC: smartcontract.StringType, }}, } diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 5fc3b5d94..a80f85487 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -149,7 +149,7 @@ func (c *codegen) processNotify(f *funcScope, args []ast.Expr) { params := make([]string, 0, len(args[1:])) for _, p := range args[1:] { - st, _ := c.scAndVMTypeFromExpr(p) + st, _, _ := c.scAndVMTypeFromExpr(p) params = append(params, st.String()) }