diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 21730b9a1..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) @@ -505,11 +527,11 @@ func testInvokeScript(ctx *cli.Context) error { // ProjectConfig contains project metadata. type ProjectConfig struct { Version uint - Contract request.ContractDetails `yaml:"project"` + Contract smartcontract.ContractDetails `yaml:"project"` } -func parseContractDetails() request.ContractDetails { - details := request.ContractDetails{} +func parseContractDetails() smartcontract.ContractDetails { + details := smartcontract.ContractDetails{} reader := bufio.NewReader(os.Stdin) fmt.Print("Author: ") @@ -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 be1b96d92..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) { @@ -112,7 +150,7 @@ func (c *codegen) registerDebugVariable(name string, expr ast.Expr) { func (c *codegen) methodInfoFromScope(name string, scope *funcScope) *MethodDebugInfo { ps := scope.decl.Type.Params params := make([]DebugParam, 0, ps.NumFields()) - for i := range params { + for i := range ps.List { for j := range ps.List[i].Names { params = append(params, DebugParam{ 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 } + +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 baecbb09e..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" @@ -28,6 +30,9 @@ func methodInt(a string) int { } return 3 } +func methodConcat(a, b string, c string) string{ + return a + b + c +} func methodString() string { return "" } func methodByteArray() []byte { return nil } func methodArray() []bool { return nil } @@ -48,6 +53,7 @@ func methodStruct() struct{} { return struct{}{} } t.Run("return types", func(t *testing.T) { returnTypes := map[string]string{ "methodInt": "Integer", + "methodConcat": "String", "methodString": "String", "methodByteArray": "ByteArray", "methodArray": "Array", "methodStruct": "Struct", "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 for i := range d.Methods { index := d.Methods[i].Range.End 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) { diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index a0eb12c27..a9659a266 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -78,7 +78,7 @@ func AddInputsAndUnspentsToTx(tx *transaction.Transaction, addr string, assetID // DetailsToSCProperties extract the fields needed from ContractDetails // and converts them to smartcontract.PropertyState. -func DetailsToSCProperties(contract *ContractDetails) smartcontract.PropertyState { +func DetailsToSCProperties(contract *smartcontract.ContractDetails) smartcontract.PropertyState { var props smartcontract.PropertyState if contract.HasStorage { props |= smartcontract.HasStorage @@ -94,7 +94,7 @@ func DetailsToSCProperties(contract *ContractDetails) smartcontract.PropertyStat // CreateDeploymentScript returns a script that deploys given smart contract // with its metadata. -func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) { +func CreateDeploymentScript(avm []byte, contract *smartcontract.ContractDetails) ([]byte, error) { script := io.NewBufBinWriter() emit.Bytes(script.BinWriter, []byte(contract.Description)) emit.Bytes(script.BinWriter, []byte(contract.Email)) diff --git a/pkg/rpc/request/scdetails.go b/pkg/smartcontract/contract_details.go similarity index 65% rename from pkg/rpc/request/scdetails.go rename to pkg/smartcontract/contract_details.go index c53e21e61..05132f0c8 100644 --- a/pkg/rpc/request/scdetails.go +++ b/pkg/smartcontract/contract_details.go @@ -1,6 +1,4 @@ -package request - -import "github.com/nspcc-dev/neo-go/pkg/smartcontract" +package smartcontract // ContractDetails contains contract metadata. type ContractDetails struct { @@ -12,6 +10,6 @@ type ContractDetails struct { HasStorage bool HasDynamicInvocation bool IsPayable bool - ReturnType smartcontract.ParamType - Parameters []smartcontract.ParamType + ReturnType ParamType + Parameters []ParamType }