Merge pull request #922 from nspcc-dev/neo3/compiler/generate_abi
compiler: add ability to generate .abi.json file
This commit is contained in:
commit
712d7b9de6
6 changed files with 232 additions and 22 deletions
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
Loading…
Reference in a new issue