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).
This commit is contained in:
Anna Shaleva 2020-04-28 19:39:01 +03:00
parent ab7f2cb4fb
commit 2ec1d76320
4 changed files with 186 additions and 11 deletions

View file

@ -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
}

View file

@ -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)
}

View file

@ -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,
}
}

View file

@ -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) {