Merge pull request #922 from nspcc-dev/neo3/compiler/generate_abi

compiler: add ability to generate .abi.json file
This commit is contained in:
Roman Khimov 2020-05-04 12:04:20 +03:00 committed by GitHub
commit 712d7b9de6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 232 additions and 22 deletions

View file

@ -97,6 +97,14 @@ func NewCommands() []cli.Command {
Name: "debug, d", Name: "debug, d",
Usage: "Emit debug info in a separate file", 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 { if len(src) == 0 {
return cli.NewExitError(errNoInput, 1) 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{ o := &compiler.Options{
Outfile: ctx.String("out"), Outfile: ctx.String("out"),
DebugInfo: ctx.String("debug"), 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) result, err := compiler.CompileAndSave(src, o)
@ -505,11 +527,11 @@ func testInvokeScript(ctx *cli.Context) error {
// ProjectConfig contains project metadata. // ProjectConfig contains project metadata.
type ProjectConfig struct { type ProjectConfig struct {
Version uint Version uint
Contract request.ContractDetails `yaml:"project"` Contract smartcontract.ContractDetails `yaml:"project"`
} }
func parseContractDetails() request.ContractDetails { func parseContractDetails() smartcontract.ContractDetails {
details := request.ContractDetails{} details := smartcontract.ContractDetails{}
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print("Author: ") fmt.Print("Author: ")
@ -614,15 +636,9 @@ func contractDeploy(ctx *cli.Context) error {
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
confBytes, err := ioutil.ReadFile(confFile) conf, err := parseContractConfig(confFile)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return err
}
conf := ProjectConfig{}
err = yaml.Unmarshal(confBytes, &conf)
if err != nil {
return cli.NewExitError(fmt.Errorf("bad config: %v", err), 1)
} }
c, err := client.New(context.TODO(), endpoint, client.Options{}) 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()) fmt.Printf("Sent deployment transaction %s for contract %s\n", txHash.StringLE(), hash.Hash160(avm).StringLE())
return nil 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" "path/filepath"
"strings" "strings"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/loader"
) )
@ -26,6 +27,12 @@ type Options struct {
// The name of the output for debug info. // The name of the output for debug info.
DebugInfo string DebugInfo string
// The name of the output for application binary interface info.
ABIInfo string
// Contract metadata.
ContractDetails *smartcontract.ContractDetails
} }
type buildInfo struct { type buildInfo struct {
@ -105,5 +112,16 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
if err != nil { if err != nil {
return b, err 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" "go/types"
"strconv" "strconv"
"strings" "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. // DebugInfo represents smart-contract debug information.
@ -72,8 +76,42 @@ type DebugRange struct {
// DebugParam represents variables's name and type. // DebugParam represents variables's name and type.
type DebugParam struct { type DebugParam struct {
Name string Name string `json:"name"`
Type string 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) { func (c *codegen) saveSequencePoint(n ast.Node) {
@ -112,7 +150,7 @@ func (c *codegen) registerDebugVariable(name string, expr ast.Expr) {
func (c *codegen) methodInfoFromScope(name string, scope *funcScope) *MethodDebugInfo { func (c *codegen) methodInfoFromScope(name string, scope *funcScope) *MethodDebugInfo {
ps := scope.decl.Type.Params ps := scope.decl.Type.Params
params := make([]DebugParam, 0, ps.NumFields()) params := make([]DebugParam, 0, ps.NumFields())
for i := range params { for i := range ps.List {
for j := range ps.List[i].Names { for j := range ps.List[i].Names {
params = append(params, DebugParam{ params = append(params, DebugParam{
Name: ps.List[i].Names[j].Name, Name: ps.List[i].Names[j].Name,
@ -260,3 +298,40 @@ func parsePairJSON(data []byte, sep string) (string, string, error) {
} }
return ss[0], ss[1], nil 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 ( import (
"testing" "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/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -28,6 +30,9 @@ func methodInt(a string) int {
} }
return 3 return 3
} }
func methodConcat(a, b string, c string) string{
return a + b + c
}
func methodString() string { return "" } func methodString() string { return "" }
func methodByteArray() []byte { return nil } func methodByteArray() []byte { return nil }
func methodArray() []bool { return nil } func methodArray() []bool { return nil }
@ -48,6 +53,7 @@ func methodStruct() struct{} { return struct{}{} }
t.Run("return types", func(t *testing.T) { t.Run("return types", func(t *testing.T) {
returnTypes := map[string]string{ returnTypes := map[string]string{
"methodInt": "Integer", "methodInt": "Integer",
"methodConcat": "String",
"methodString": "String", "methodByteArray": "ByteArray", "methodString": "String", "methodByteArray": "ByteArray",
"methodArray": "Array", "methodStruct": "Struct", "methodArray": "Array", "methodStruct": "Struct",
"Main": "Boolean", "Main": "Boolean",
@ -70,12 +76,95 @@ func methodStruct() struct{} { return struct{}{} }
} }
}) })
t.Run("param types", func(t *testing.T) {
paramTypes := map[string][]DebugParam{
"methodInt": {{
Name: "a",
Type: "String",
}},
"methodConcat": {
{
Name: "a",
Type: "String",
},
{
Name: "b",
Type: "String",
},
{
Name: "c",
Type: "String",
},
},
"Main": {{
Name: "op",
Type: "String",
}},
}
for i := range d.Methods {
v, ok := paramTypes[d.Methods[i].Name.Name]
if ok {
require.Equal(t, v, d.Methods[i].Parameters)
}
}
})
// basic check that last instruction of every method is indeed RET // basic check that last instruction of every method is indeed RET
for i := range d.Methods { for i := range d.Methods {
index := d.Methods[i].Range.End index := d.Methods[i].Range.End
require.True(t, int(index) < len(buf)) require.True(t, int(index) < len(buf))
require.EqualValues(t, opcode.RET, buf[index]) 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) { func TestSequencePoints(t *testing.T) {

View file

@ -78,7 +78,7 @@ func AddInputsAndUnspentsToTx(tx *transaction.Transaction, addr string, assetID
// DetailsToSCProperties extract the fields needed from ContractDetails // DetailsToSCProperties extract the fields needed from ContractDetails
// and converts them to smartcontract.PropertyState. // and converts them to smartcontract.PropertyState.
func DetailsToSCProperties(contract *ContractDetails) smartcontract.PropertyState { func DetailsToSCProperties(contract *smartcontract.ContractDetails) smartcontract.PropertyState {
var props smartcontract.PropertyState var props smartcontract.PropertyState
if contract.HasStorage { if contract.HasStorage {
props |= smartcontract.HasStorage props |= smartcontract.HasStorage
@ -94,7 +94,7 @@ func DetailsToSCProperties(contract *ContractDetails) smartcontract.PropertyStat
// CreateDeploymentScript returns a script that deploys given smart contract // CreateDeploymentScript returns a script that deploys given smart contract
// with its metadata. // with its metadata.
func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) { func CreateDeploymentScript(avm []byte, contract *smartcontract.ContractDetails) ([]byte, error) {
script := io.NewBufBinWriter() script := io.NewBufBinWriter()
emit.Bytes(script.BinWriter, []byte(contract.Description)) emit.Bytes(script.BinWriter, []byte(contract.Description))
emit.Bytes(script.BinWriter, []byte(contract.Email)) emit.Bytes(script.BinWriter, []byte(contract.Email))

View file

@ -1,6 +1,4 @@
package request package smartcontract
import "github.com/nspcc-dev/neo-go/pkg/smartcontract"
// ContractDetails contains contract metadata. // ContractDetails contains contract metadata.
type ContractDetails struct { type ContractDetails struct {
@ -12,6 +10,6 @@ type ContractDetails struct {
HasStorage bool HasStorage bool
HasDynamicInvocation bool HasDynamicInvocation bool
IsPayable bool IsPayable bool
ReturnType smartcontract.ParamType ReturnType ParamType
Parameters []smartcontract.ParamType Parameters []ParamType
} }