From 2ec1d763202ee395cd6ea457cbdbfc1dae411bb5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 28 Apr 2020 19:39:01 +0300 Subject: [PATCH] compiler: add ability to generate .abi.json file A part of integration with NEO Blockchain Toolkit (see #902). To be able to deploy smart-contract compiled with neo-go compiler via NEO Express, we have to generate additional .abi.json file. This file contains the following information: - hash of the compiled contract - smart-contract metadata (title, description, version, author, email, has-storage, has-dynamic-invoke, is-payable) - smart-contract entry point - functions - events However, this .abi.json file is slightly different from the one, described in manifest.go, so we have to add auxilaury stractures for json marshalling. The .abi.json format used by NEO-Express is described [here](https://github.com/neo-project/neo-devpack-dotnet/blob/master/src/Neo.Compiler.MSIL/FuncExport.cs#L66). --- cli/smartcontract/smart_contract.go | 46 ++++++++++++++--- pkg/compiler/compiler.go | 20 +++++++- pkg/compiler/debug.go | 79 ++++++++++++++++++++++++++++- pkg/compiler/debug_test.go | 52 +++++++++++++++++++ 4 files changed, 186 insertions(+), 11 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 8c9041ccf..ccf9bb1bf 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -97,6 +97,14 @@ func NewCommands() []cli.Command { Name: "debug, d", Usage: "Emit debug info in a separate file", }, + cli.StringFlag{ + Name: "abi, a", + Usage: "Emit application binary interface (.abi.json) file into separate file using configuration input file (*.yml)", + }, + cli.StringFlag{ + Name: "config, c", + Usage: "Configuration input file (*.yml)", + }, }, }, { @@ -349,11 +357,25 @@ func contractCompile(ctx *cli.Context) error { if len(src) == 0 { return cli.NewExitError(errNoInput, 1) } + abi := ctx.String("abi") + confFile := ctx.String("config") + if len(abi) != 0 && len(confFile) == 0 { + return cli.NewExitError(errNoConfFile, 1) + } o := &compiler.Options{ Outfile: ctx.String("out"), DebugInfo: ctx.String("debug"), + ABIInfo: abi, + } + + if len(confFile) != 0 { + conf, err := parseContractConfig(confFile) + if err != nil { + return err + } + o.ContractDetails = &conf.Contract } result, err := compiler.CompileAndSave(src, o) @@ -614,15 +636,9 @@ func contractDeploy(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - confBytes, err := ioutil.ReadFile(confFile) + conf, err := parseContractConfig(confFile) if err != nil { - return cli.NewExitError(err, 1) - } - - conf := ProjectConfig{} - err = yaml.Unmarshal(confBytes, &conf) - if err != nil { - return cli.NewExitError(fmt.Errorf("bad config: %v", err), 1) + return err } c, err := client.New(context.TODO(), endpoint, client.Options{}) @@ -644,3 +660,17 @@ func contractDeploy(ctx *cli.Context) error { fmt.Printf("Sent deployment transaction %s for contract %s\n", txHash.StringLE(), hash.Hash160(avm).StringLE()) return nil } + +func parseContractConfig(confFile string) (ProjectConfig, error) { + conf := ProjectConfig{} + confBytes, err := ioutil.ReadFile(confFile) + if err != nil { + return conf, cli.NewExitError(err, 1) + } + + err = yaml.Unmarshal(confBytes, &conf) + if err != nil { + return conf, cli.NewExitError(fmt.Errorf("bad config: %v", err), 1) + } + return conf, nil +} diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index e53c3eaac..f72d7ba47 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "golang.org/x/tools/go/loader" ) @@ -26,6 +27,12 @@ type Options struct { // The name of the output for debug info. DebugInfo string + + // The name of the output for application binary interface info. + ABIInfo string + + // Contract metadata. + ContractDetails *smartcontract.ContractDetails } type buildInfo struct { @@ -105,5 +112,16 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { if err != nil { return b, err } - return b, ioutil.WriteFile(o.DebugInfo, data, os.ModePerm) + if err := ioutil.WriteFile(o.DebugInfo, data, os.ModePerm); err != nil { + return b, err + } + if o.ABIInfo == "" { + return b, err + } + abi := di.convertToABI(b, o.ContractDetails) + abiData, err := json.Marshal(abi) + if err != nil { + return b, err + } + return b, ioutil.WriteFile(o.ABIInfo, abiData, os.ModePerm) } diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 0e9162ab7..0d607eaae 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -8,6 +8,10 @@ import ( "go/types" "strconv" "strings" + + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" ) // DebugInfo represents smart-contract debug information. @@ -72,8 +76,42 @@ type DebugRange struct { // DebugParam represents variables's name and type. type DebugParam struct { - Name string - Type string + Name string `json:"name"` + Type string `json:"type"` +} + +// ABI represents ABI contract info in compatible with NEO Blockchain Toolkit format +type ABI struct { + Hash util.Uint160 `json:"hash"` + Metadata Metadata `json:"metadata"` + EntryPoint string `json:"entrypoint"` + Functions []Method `json:"functions"` + Events []Event `json:"events"` +} + +// Metadata represents ABI contract metadata +type Metadata struct { + Author string `json:"author"` + Email string `json:"email"` + Version string `json:"version"` + Title string `json:"title"` + Description string `json:"description"` + HasStorage bool `json:"has-storage"` + HasDynamicInvocation bool `json:"has-dynamic-invoke"` + IsPayable bool `json:"is-payable"` +} + +// Method represents ABI method's metadata. +type Method struct { + Name string `json:"name"` + Parameters []DebugParam `json:"parameters"` + ReturnType string `json:"returntype"` +} + +// Event represents ABI event's metadata. +type Event struct { + Name string `json:"name"` + Parameters []DebugParam `json:"parameters"` } func (c *codegen) saveSequencePoint(n ast.Node) { @@ -260,3 +298,40 @@ func parsePairJSON(data []byte, sep string) (string, string, error) { } return ss[0], ss[1], nil } + +func (di *DebugInfo) convertToABI(contract []byte, cd *smartcontract.ContractDetails) ABI { + methods := make([]Method, 0) + for _, method := range di.Methods { + if method.Name.Name == di.EntryPoint { + methods = append(methods, Method{ + Name: method.Name.Name, + Parameters: method.Parameters, + ReturnType: cd.ReturnType.String(), + }) + break + } + } + events := make([]Event, len(di.Events)) + for i, event := range di.Events { + events[i] = Event{ + Name: event.Name, + Parameters: event.Parameters, + } + } + return ABI{ + Hash: hash.Hash160(contract), + Metadata: Metadata{ + Author: cd.Author, + Email: cd.Email, + Version: cd.Version, + Title: cd.ProjectName, + Description: cd.Description, + HasStorage: cd.HasStorage, + HasDynamicInvocation: cd.HasDynamicInvocation, + IsPayable: cd.IsPayable, + }, + EntryPoint: di.EntryPoint, + Functions: methods, + Events: events, + } +} diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go index 96d905c27..25cd7d035 100644 --- a/pkg/compiler/debug_test.go +++ b/pkg/compiler/debug_test.go @@ -3,7 +3,9 @@ package compiler import ( "testing" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -113,6 +115,56 @@ func methodStruct() struct{} { return struct{}{} } require.True(t, int(index) < len(buf)) require.EqualValues(t, opcode.RET, buf[index]) } + + t.Run("convert to ABI", func(t *testing.T) { + author := "Joe" + email := "Joe@ex.com" + version := "1.0" + title := "MyProj" + description := "Description" + actual := d.convertToABI(buf, &smartcontract.ContractDetails{ + Author: author, + Email: email, + Version: version, + ProjectName: title, + Description: description, + HasStorage: true, + HasDynamicInvocation: false, + IsPayable: false, + ReturnType: smartcontract.BoolType, + Parameters: []smartcontract.ParamType{ + smartcontract.StringType, + }, + }) + expected := ABI{ + Hash: hash.Hash160(buf), + Metadata: Metadata{ + Author: author, + Email: email, + Version: version, + Title: title, + Description: description, + HasStorage: true, + HasDynamicInvocation: false, + IsPayable: false, + }, + EntryPoint: mainIdent, + Functions: []Method{ + { + Name: mainIdent, + Parameters: []DebugParam{ + { + Name: "op", + Type: "String", + }, + }, + ReturnType: "Boolean", + }, + }, + Events: []Event{}, + } + assert.Equal(t, expected, actual) + }) } func TestSequencePoints(t *testing.T) {