Merge pull request #2766 from nspcc-dev/rpc-wrapper-autogen

Rpc wrapper autogeneration
This commit is contained in:
Roman Khimov 2022-10-28 11:54:47 +07:00 committed by GitHub
commit b590d4ca04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1330 additions and 100 deletions

View file

@ -7,18 +7,13 @@ import (
"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/binding"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/urfave/cli"
"gopkg.in/yaml.v3"
)
var generateWrapperCmd = cli.Command{
Name: "generate-wrapper",
Usage: "generate wrapper to use in other contracts",
UsageText: "neo-go contract generate-wrapper --manifest <file.json> --out <file.go> --hash <hash>",
Description: ``,
Action: contractGenerateWrapper,
Flags: []cli.Flag{
var generatorFlags = []cli.Flag{
cli.StringFlag{
Name: "config, c",
Usage: "Configuration file to use",
@ -35,11 +30,35 @@ var generateWrapperCmd = cli.Command{
Name: "hash",
Usage: "Smart-contract hash",
},
},
}
// contractGenerateWrapper deploys contract.
var generateWrapperCmd = cli.Command{
Name: "generate-wrapper",
Usage: "generate wrapper to use in other contracts",
UsageText: "neo-go contract generate-wrapper --manifest <file.json> --out <file.go> --hash <hash>",
Description: ``,
Action: contractGenerateWrapper,
Flags: generatorFlags,
}
var generateRPCWrapperCmd = cli.Command{
Name: "generate-rpcwrapper",
Usage: "generate RPC wrapper to use for data reads",
UsageText: "neo-go contract generate-rpcwrapper --manifest <file.json> --out <file.go> --hash <hash>",
Action: contractGenerateRPCWrapper,
Flags: generatorFlags,
}
func contractGenerateWrapper(ctx *cli.Context) error {
return contractGenerateSomething(ctx, binding.Generate)
}
func contractGenerateRPCWrapper(ctx *cli.Context) error {
return contractGenerateSomething(ctx, rpcbinding.Generate)
}
// contractGenerateSomething reads generator parameters and calls the given callback.
func contractGenerateSomething(ctx *cli.Context, cb func(binding.Config) error) error {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
@ -74,7 +93,7 @@ func contractGenerateWrapper(ctx *cli.Context) error {
cfg.Output = f
err = binding.Generate(cfg)
err = cb(cfg)
if err != nil {
return cli.NewExitError(fmt.Errorf("error during generation: %w", err), 1)
}

View file

@ -1,6 +1,7 @@
package smartcontract
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
@ -44,6 +45,15 @@ func TestGenerate(t *testing.T) {
ReturnType: smartcontract.IntegerType,
Safe: true,
},
manifest.Method{
Name: "zum",
Parameters: []manifest.Parameter{
manifest.NewParameter("type", smartcontract.IntegerType),
manifest.NewParameter("typev", smartcontract.IntegerType),
manifest.NewParameter("func", smartcontract.IntegerType),
},
ReturnType: smartcontract.IntegerType,
},
manifest.Method{
Name: "justExecute",
Parameters: []manifest.Parameter{
@ -171,6 +181,11 @@ func Sum3() int {
return neogointernal.CallWithToken(Hash, "sum3", int(contract.ReadOnly)).(int)
}
// Zum invokes ` + "`zum`" + ` method of contract.
func Zum(typev int, typev_ int, funcv int) int {
return neogointernal.CallWithToken(Hash, "zum", int(contract.All), typev, typev_, funcv).(int)
}
// JustExecute invokes ` + "`justExecute`" + ` method of contract.
func JustExecute(arr []interface{}) {
neogointernal.CallWithTokenNoRet(Hash, "justExecute", int(contract.All), arr)
@ -224,6 +239,7 @@ func TestGenerateValidPackageName(t *testing.T) {
Name: "get",
Parameters: []manifest.Parameter{},
ReturnType: smartcontract.IntegerType,
Safe: true,
},
)
@ -239,7 +255,7 @@ func TestGenerateValidPackageName(t *testing.T) {
0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF, 0x03, 0x04,
}
app := cli.NewApp()
app.Commands = []cli.Command{generateWrapperCmd}
app.Commands = []cli.Command{generateWrapperCmd, generateRPCWrapperCmd}
require.NoError(t, app.Run([]string{"", "generate-wrapper",
"--manifest", manifestFile,
"--out", outFile,
@ -261,9 +277,85 @@ const Hash = "\x04\x08\x15\x16\x23\x42\x43\x44\x00\x01\xca\xfe\xba\xbe\xde\xad\x
// Get invokes `+"`get`"+` method of contract.
func Get() int {
return neogointernal.CallWithToken(Hash, "get", int(contract.All)).(int)
return neogointernal.CallWithToken(Hash, "get", int(contract.ReadOnly)).(int)
}
`, string(data))
require.NoError(t, app.Run([]string{"", "generate-rpcwrapper",
"--manifest", manifestFile,
"--out", outFile,
"--hash", "0x" + h.StringLE(),
}))
data, err = os.ReadFile(outFile)
require.NoError(t, err)
require.Equal(t, `// Package myspacecontract contains RPC wrappers for My space contract contract.
package myspacecontract
import (
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"math/big"
)
// Hash contains contract hash.
var Hash = util.Uint160{0x4, 0x8, 0x15, 0x16, 0x23, 0x42, 0x43, 0x44, 0x0, 0x1, 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef, 0x3, 0x4}
// Invoker is used by ContractReader to call various safe methods.
type Invoker interface {
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
}
// ContractReader implements safe contract methods.
type ContractReader struct {
invoker Invoker
}
// NewReader creates an instance of ContractReader using Hash and the given Invoker.
func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{invoker}
}
// Get invokes `+"`get`"+` method of contract.
func (c *ContractReader) Get() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "get"))
}
`, string(data))
}
func TestGenerateRPCBindings(t *testing.T) {
tmpDir := t.TempDir()
app := cli.NewApp()
app.Commands = []cli.Command{generateWrapperCmd, generateRPCWrapperCmd}
outFile := filepath.Join(tmpDir, "out.go")
require.NoError(t, app.Run([]string{"", "generate-rpcwrapper",
"--manifest", filepath.Join("testdata", "nex", "nex.manifest.json"),
"--out", outFile,
"--hash", "0xa2a67f09e8cf22c6bfd5cea24adc0f4bf0a11aa8",
}))
data, err := os.ReadFile(outFile)
require.NoError(t, err)
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
expected, err := os.ReadFile(filepath.Join("testdata", "nex", "nex.go"))
require.NoError(t, err)
expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
require.Equal(t, string(expected), string(data))
require.NoError(t, app.Run([]string{"", "generate-rpcwrapper",
"--manifest", filepath.Join("testdata", "nameservice", "nns.manifest.json"),
"--out", outFile,
"--hash", "0x50ac1c37690cc2cfc594472833cf57505d5f46de",
}))
data, err = os.ReadFile(outFile)
require.NoError(t, err)
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
expected, err = os.ReadFile(filepath.Join("testdata", "nameservice", "nns.go"))
require.NoError(t, err)
expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
require.Equal(t, string(expected), string(data))
}
func TestGenerate_Errors(t *testing.T) {

View file

@ -182,6 +182,7 @@ func NewCommands() []cli.Command {
Flags: deployFlags,
},
generateWrapperCmd,
generateRPCWrapperCmd,
{
Name: "invokefunction",
Usage: "invoke deployed contract on the blockchain",

View file

@ -0,0 +1,62 @@
// Package nameservice contains RPC wrappers for NameService contract.
package nameservice
import (
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"math/big"
)
// Hash contains contract hash.
var Hash = util.Uint160{0xde, 0x46, 0x5f, 0x5d, 0x50, 0x57, 0xcf, 0x33, 0x28, 0x47, 0x94, 0xc5, 0xcf, 0xc2, 0xc, 0x69, 0x37, 0x1c, 0xac, 0x50}
// Invoker is used by ContractReader to call various safe methods.
type Invoker interface {
nep11.Invoker
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
}
// ContractReader implements safe contract methods.
type ContractReader struct {
nep11.NonDivisibleReader
invoker Invoker
}
// NewReader creates an instance of ContractReader using Hash and the given Invoker.
func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{*nep11.NewNonDivisibleReader(invoker, Hash), invoker}
}
// Roots invokes `roots` method of contract.
func (c *ContractReader) Roots() (stackitem.Item, error) {
return unwrap.Item(c.invoker.Call(Hash, "roots"))
}
// GetPrice invokes `getPrice` method of contract.
func (c *ContractReader) GetPrice(length *big.Int) (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "getPrice", length))
}
// IsAvailable invokes `isAvailable` method of contract.
func (c *ContractReader) IsAvailable(name string) (bool, error) {
return unwrap.Bool(c.invoker.Call(Hash, "isAvailable", name))
}
// GetRecord invokes `getRecord` method of contract.
func (c *ContractReader) GetRecord(name string, typev *big.Int) (string, error) {
return unwrap.UTF8String(c.invoker.Call(Hash, "getRecord", name, typev))
}
// GetAllRecords invokes `getAllRecords` method of contract.
func (c *ContractReader) GetAllRecords(name string) (stackitem.Item, error) {
return unwrap.Item(c.invoker.Call(Hash, "getAllRecords", name))
}
// Resolve invokes `resolve` method of contract.
func (c *ContractReader) Resolve(name string, typev *big.Int) (string, error) {
return unwrap.UTF8String(c.invoker.Call(Hash, "resolve", name, typev))
}

View file

@ -0,0 +1,441 @@
{
"abi" : {
"events" : [
{
"parameters" : [
{
"name" : "from",
"type" : "Hash160"
},
{
"name" : "to",
"type" : "Hash160"
},
{
"name" : "amount",
"type" : "Integer"
},
{
"type" : "ByteArray",
"name" : "tokenId"
}
],
"name" : "Transfer"
},
{
"parameters" : [
{
"type" : "String",
"name" : "name"
},
{
"type" : "Hash160",
"name" : "oldAdmin"
},
{
"type" : "Hash160",
"name" : "newAdmin"
}
],
"name" : "SetAdmin"
},
{
"name" : "Renew",
"parameters" : [
{
"name" : "name",
"type" : "String"
},
{
"type" : "Integer",
"name" : "oldExpiration"
},
{
"name" : "newExpiration",
"type" : "Integer"
}
]
}
],
"methods" : [
{
"safe" : true,
"parameters" : [],
"name" : "symbol",
"returntype" : "String",
"offset" : 0
},
{
"parameters" : [],
"name" : "decimals",
"returntype" : "Integer",
"safe" : true,
"offset" : 6
},
{
"offset" : 8,
"returntype" : "Integer",
"name" : "totalSupply",
"parameters" : [],
"safe" : true
},
{
"offset" : 53,
"safe" : true,
"parameters" : [
{
"type" : "ByteArray",
"name" : "tokenId"
}
],
"name" : "ownerOf",
"returntype" : "Hash160"
},
{
"safe" : true,
"name" : "properties",
"returntype" : "Map",
"parameters" : [
{
"type" : "ByteArray",
"name" : "tokenId"
}
],
"offset" : 184
},
{
"safe" : true,
"returntype" : "Integer",
"name" : "balanceOf",
"parameters" : [
{
"name" : "owner",
"type" : "Hash160"
}
],
"offset" : 341
},
{
"safe" : true,
"returntype" : "InteropInterface",
"name" : "tokens",
"parameters" : [],
"offset" : 453
},
{
"safe" : true,
"name" : "tokensOf",
"returntype" : "InteropInterface",
"parameters" : [
{
"name" : "owner",
"type" : "Hash160"
}
],
"offset" : 494
},
{
"offset" : 600,
"safe" : false,
"parameters" : [
{
"type" : "Hash160",
"name" : "to"
},
{
"name" : "tokenId",
"type" : "ByteArray"
},
{
"name" : "data",
"type" : "Any"
}
],
"name" : "transfer",
"returntype" : "Boolean"
},
{
"name" : "update",
"returntype" : "Void",
"parameters" : [
{
"type" : "ByteArray",
"name" : "nef"
},
{
"name" : "manifest",
"type" : "String"
}
],
"safe" : false,
"offset" : 1121
},
{
"offset" : 1291,
"returntype" : "Void",
"name" : "addRoot",
"parameters" : [
{
"name" : "root",
"type" : "String"
}
],
"safe" : false
},
{
"offset" : 1725,
"safe" : true,
"parameters" : [],
"returntype" : "InteropInterface",
"name" : "roots"
},
{
"offset" : 1757,
"parameters" : [
{
"type" : "Array",
"name" : "priceList"
}
],
"name" : "setPrice",
"returntype" : "Void",
"safe" : false
},
{
"offset" : 1952,
"safe" : true,
"parameters" : [
{
"name" : "length",
"type" : "Integer"
}
],
"name" : "getPrice",
"returntype" : "Integer"
},
{
"offset" : 2017,
"safe" : true,
"parameters" : [
{
"type" : "String",
"name" : "name"
}
],
"name" : "isAvailable",
"returntype" : "Boolean"
},
{
"offset" : 2405,
"parameters" : [
{
"type" : "String",
"name" : "name"
},
{
"type" : "Hash160",
"name" : "owner"
}
],
"name" : "register",
"returntype" : "Boolean",
"safe" : false
},
{
"name" : "renew",
"returntype" : "Integer",
"parameters" : [
{
"type" : "String",
"name" : "name"
}
],
"safe" : false,
"offset" : 3113
},
{
"offset" : 3123,
"safe" : false,
"parameters" : [
{
"name" : "name",
"type" : "String"
},
{
"name" : "years",
"type" : "Integer"
}
],
"name" : "renew",
"returntype" : "Integer"
},
{
"offset" : 3697,
"parameters" : [
{
"type" : "String",
"name" : "name"
},
{
"name" : "admin",
"type" : "Hash160"
}
],
"name" : "setAdmin",
"returntype" : "Void",
"safe" : false
},
{
"safe" : false,
"returntype" : "Void",
"name" : "setRecord",
"parameters" : [
{
"name" : "name",
"type" : "String"
},
{
"type" : "Integer",
"name" : "type"
},
{
"name" : "data",
"type" : "String"
}
],
"offset" : 3921
},
{
"name" : "getRecord",
"returntype" : "String",
"parameters" : [
{
"name" : "name",
"type" : "String"
},
{
"type" : "Integer",
"name" : "type"
}
],
"safe" : true,
"offset" : 5877
},
{
"returntype" : "InteropInterface",
"name" : "getAllRecords",
"parameters" : [
{
"name" : "name",
"type" : "String"
}
],
"safe" : true,
"offset" : 6201
},
{
"safe" : false,
"returntype" : "Void",
"name" : "deleteRecord",
"parameters" : [
{
"name" : "name",
"type" : "String"
},
{
"type" : "Integer",
"name" : "type"
}
],
"offset" : 6281
},
{
"offset" : 6565,
"name" : "resolve",
"returntype" : "String",
"parameters" : [
{
"name" : "name",
"type" : "String"
},
{
"type" : "Integer",
"name" : "type"
}
],
"safe" : true
},
{
"safe" : false,
"parameters" : [
{
"name" : "data",
"type" : "Any"
},
{
"name" : "update",
"type" : "Boolean"
}
],
"name" : "_deploy",
"returntype" : "Void",
"offset" : 7152
},
{
"offset" : 7514,
"parameters" : [],
"returntype" : "Void",
"name" : "_initialize",
"safe" : false
}
]
},
"supportedstandards" : [
"NEP-11"
],
"permissions" : [
{
"contract" : "0x726cb6e0cd8628a1350a611384688911ab75f51b",
"methods" : [
"ripemd160"
]
},
{
"contract" : "0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0",
"methods" : [
"atoi",
"deserialize",
"serialize",
"stringSplit"
]
},
{
"contract" : "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5",
"methods" : [
"getCommittee"
]
},
{
"contract" : "0xfffdc93764dbaddd97c48f252a53ea4643faa3fd",
"methods" : [
"getContract",
"update"
]
},
{
"methods" : [
"onNEP11Payment"
],
"contract" : "*"
}
],
"features" : {},
"name" : "NameService",
"trusts" : [],
"extra" : {
"Author" : "The Neo Project",
"Description" : "Neo Name Service",
"Email" : "dev@neo.org"
},
"groups" : []
}

52
cli/smartcontract/testdata/nex/nex.go vendored Normal file
View file

@ -0,0 +1,52 @@
// Package nextoken contains RPC wrappers for NEX Token contract.
package nextoken
import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
"math/big"
)
// Hash contains contract hash.
var Hash = util.Uint160{0xa8, 0x1a, 0xa1, 0xf0, 0x4b, 0xf, 0xdc, 0x4a, 0xa2, 0xce, 0xd5, 0xbf, 0xc6, 0x22, 0xcf, 0xe8, 0x9, 0x7f, 0xa6, 0xa2}
// Invoker is used by ContractReader to call various safe methods.
type Invoker interface {
nep17.Invoker
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
}
// ContractReader implements safe contract methods.
type ContractReader struct {
nep17.TokenReader
invoker Invoker
}
// NewReader creates an instance of ContractReader using Hash and the given Invoker.
func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{*nep17.NewReader(invoker, Hash), invoker}
}
// Cap invokes `cap` method of contract.
func (c *ContractReader) Cap() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "cap"))
}
// GetMinter invokes `getMinter` method of contract.
func (c *ContractReader) GetMinter() (*keys.PublicKey, error) {
return unwrap.PublicKey(c.invoker.Call(Hash, "getMinter"))
}
// GetOwner invokes `getOwner` method of contract.
func (c *ContractReader) GetOwner() (util.Uint160, error) {
return unwrap.Uint160(c.invoker.Call(Hash, "getOwner"))
}
// TotalMinted invokes `totalMinted` method of contract.
func (c *ContractReader) TotalMinted() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "totalMinted"))
}

View file

@ -0,0 +1,275 @@
{
"name" : "NEX Token",
"abi" : {
"events" : [
{
"parameters" : [
{
"type" : "Hash160",
"name" : "from"
},
{
"name" : "to",
"type" : "Hash160"
},
{
"name" : "amount",
"type" : "Integer"
}
],
"name" : "Transfer"
},
{
"name" : "OnMint",
"parameters" : [
{
"name" : "from",
"type" : "Hash160"
},
{
"type" : "Hash160",
"name" : "to"
},
{
"type" : "Integer",
"name" : "amount"
},
{
"name" : "swapId",
"type" : "Integer"
}
]
}
],
"methods" : [
{
"parameters" : [],
"offset" : 0,
"name" : "_initialize",
"safe" : false,
"returntype" : "Void"
},
{
"parameters" : [
{
"type" : "Any",
"name" : "data"
},
{
"name" : "isUpdate",
"type" : "Boolean"
}
],
"offset" : 3,
"name" : "_deploy",
"safe" : false,
"returntype" : "Void"
},
{
"parameters" : [
{
"type" : "Hash160",
"name" : "holder"
}
],
"offset" : 484,
"returntype" : "Integer",
"safe" : true,
"name" : "balanceOf"
},
{
"safe" : true,
"returntype" : "Integer",
"name" : "cap",
"offset" : 1881,
"parameters" : []
},
{
"name" : "changeMinter",
"safe" : false,
"returntype" : "Void",
"parameters" : [
{
"name" : "newMinter",
"type" : "PublicKey"
}
],
"offset" : 1678
},
{
"parameters" : [
{
"type" : "Hash160",
"name" : "newOwner"
}
],
"offset" : 1659,
"name" : "changeOwner",
"safe" : false,
"returntype" : "Void"
},
{
"offset" : 466,
"parameters" : [],
"safe" : true,
"name" : "decimals",
"returntype" : "Integer"
},
{
"returntype" : "Void",
"safe" : false,
"name" : "destroy",
"parameters" : [],
"offset" : 1194
},
{
"safe" : true,
"returntype" : "PublicKey",
"name" : "getMinter",
"offset" : 1671,
"parameters" : []
},
{
"parameters" : [],
"offset" : 1652,
"name" : "getOwner",
"safe" : true,
"returntype" : "Hash160"
},
{
"name" : "maxSupply",
"safe" : false,
"returntype" : "Integer",
"parameters" : [],
"offset" : 468
},
{
"offset" : 1222,
"parameters" : [
{
"name" : "from",
"type" : "Hash160"
},
{
"name" : "to",
"type" : "Hash160"
},
{
"type" : "Integer",
"name" : "amount"
},
{
"name" : "swapId",
"type" : "Integer"
},
{
"name" : "signature",
"type" : "Signature"
},
{
"name" : "data",
"type" : "Any"
}
],
"safe" : false,
"name" : "mint",
"returntype" : "Void"
},
{
"safe" : true,
"name" : "symbol",
"returntype" : "String",
"parameters" : [],
"offset" : 460
},
{
"offset" : 1854,
"parameters" : [],
"name" : "totalMinted",
"safe" : true,
"returntype" : "Integer"
},
{
"offset" : 478,
"parameters" : [],
"name" : "totalSupply",
"safe" : true,
"returntype" : "Integer"
},
{
"offset" : 543,
"parameters" : [
{
"name" : "from",
"type" : "Hash160"
},
{
"name" : "to",
"type" : "Hash160"
},
{
"type" : "Integer",
"name" : "amount"
},
{
"name" : "data",
"type" : "Any"
}
],
"name" : "transfer",
"safe" : false,
"returntype" : "Boolean"
},
{
"offset" : 1205,
"parameters" : [
{
"type" : "ByteArray",
"name" : "nef"
},
{
"name" : "manifest",
"type" : "ByteArray"
}
],
"safe" : false,
"returntype" : "Void",
"name" : "update"
},
{
"offset" : 1717,
"parameters" : [
{
"type" : "Integer",
"name" : "newCap"
}
],
"returntype" : "Void",
"safe" : false,
"name" : "updateCap"
}
]
},
"supportedstandards" : [
"NEP-17"
],
"extra" : null,
"trusts" : [],
"features" : {},
"groups" : [],
"permissions" : [
{
"methods" : [
"onNEP17Payment"
],
"contract" : "*"
},
{
"methods" : [
"update",
"destroy"
],
"contract" : "0xfffdc93764dbaddd97c48f252a53ea4643faa3fd"
}
]
}

View file

@ -410,6 +410,43 @@ given RPC server and wallet and paying 0.00001 extra GAS for this transaction):
$ ./bin/neo-go contract invokefunction -r http://localhost:20331 -w my_wallet.json -g 0.00001 f84d6a337fbc3d3a201d41da99e86b479e7a2554 balanceOf AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y
```
### Generating contract bindings
To be able to use deployed contract from another contract one needs to have
its interface definition (exported methods and hash). While it is possible to
use generic contract.Call interop interface, it's not very convenient and
efficient. NeoGo can autogenerate contract bindings in Go language for any
deployed contract based on its manifest, it creates a Go source file with all
of the contract's methods that then can be imported and used as a regular Go
package.
```
$ ./bin/neo-go contract generate-wrapper --manifest manifest.json --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
```
Notice that some structured types can be omitted this way (when a function
returns some structure it's just an "Array" type in the manifest with no
internal details), but if the contract you're using is written in Go
originally you can create a specific configuration file during compilation
that will add this data for wrapper generator to use:
```
$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --manifest manifest.json --bindings contract.bindings.yml
$ ./bin/neo-go contract generate-wrapper --manifest manifest.json --config contract.bindings.yml --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
```
### Generating RPC contract bindings
To simplify interacting with the contract via RPC you can generate
contract-specific RPC bindings with the "generate-rpcwrapper" command. At the
moment it only works for safe (read-only) methods. If your contract is NEP-11
or NEP-17 that's autodetected and an appropriate package is included as
well. Notice that the type data available in the manifest is limited, so in
some cases the interface generated may use generic stackitem types. Iterators
are not supported yet.
```
$ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
```
## Smart contract examples
Some examples are provided in the [examples directory](../examples). For more

View file

@ -146,6 +146,16 @@ func Uint256(r *result.Invoke, err error) (util.Uint256, error) {
return util.Uint256DecodeBytesBE(b)
}
// PublicKey expects correct execution (HALT state) with a single stack item
// returned. A public key is extracted from this item and returned.
func PublicKey(r *result.Invoke, err error) (*keys.PublicKey, error) {
b, err := Bytes(r, err)
if err != nil {
return nil, err
}
return keys.NewPublicKeyFromBytes(b, elliptic.P256())
}
// SessionIterator expects correct execution (HALT state) with a single stack
// item returned. If this item is an iterator it's returned to the caller along
// with the session ID. Notice that this function also returns successfully

View file

@ -43,6 +43,9 @@ func TestStdErrors(t *testing.T) {
func(r *result.Invoke, err error) (interface{}, error) {
return Uint256(r, err)
},
func(r *result.Invoke, err error) (interface{}, error) {
return PublicKey(r, err)
},
func(r *result.Invoke, err error) (interface{}, error) {
_, _, err = SessionIterator(r, err)
return nil, err
@ -189,6 +192,18 @@ func TestUint256(t *testing.T) {
require.Equal(t, util.Uint256{1, 2, 3}, u)
}
func TestPublicKey(t *testing.T) {
k, err := keys.NewPrivateKey()
require.NoError(t, err)
_, err = PublicKey(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(util.Uint160{1, 2, 3}.BytesBE())}}, nil)
require.Error(t, err)
pk, err := PublicKey(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(k.PublicKey().Bytes())}}, nil)
require.NoError(t, err)
require.Equal(t, k.PublicKey(), pk)
}
func TestSessionIterator(t *testing.T) {
_, _, err := SessionIterator(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil)
require.Error(t, err)

View file

@ -3,6 +3,7 @@ package binding
import (
"bytes"
"fmt"
"go/token"
"io"
"sort"
"strconv"
@ -55,24 +56,24 @@ type (
Output io.Writer `yaml:"-"`
}
contractTmpl struct {
ContractTmpl struct {
PackageName string
ContractName string
Imports []string
Hash string
Methods []methodTmpl
Methods []MethodTmpl
}
methodTmpl struct {
MethodTmpl struct {
Name string
NameABI string
CallFlag string
Comment string
Arguments []paramTmpl
Arguments []ParamTmpl
ReturnType string
}
paramTmpl struct {
ParamTmpl struct {
Name string
Type string
}
@ -88,15 +89,15 @@ func NewConfig() Config {
// Generate writes Go file containing smartcontract bindings to the `cfg.Output`.
func Generate(cfg Config) error {
ctr, err := templateFromManifest(cfg)
ctr, err := TemplateFromManifest(cfg, scTypeToGo)
if err != nil {
return err
}
ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/contract")
ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal")
sort.Strings(ctr.Imports)
tmp, err := template.New("generate").Funcs(template.FuncMap{
"lowerFirst": lowerFirst,
"scTypeToGo": scTypeToGo,
}).Parse(srcTmpl)
tmp, err := template.New("generate").Parse(srcTmpl)
if err != nil {
return err
}
@ -104,46 +105,52 @@ func Generate(cfg Config) error {
return tmp.Execute(cfg.Output, ctr)
}
func scTypeToGo(typ smartcontract.ParamType) string {
func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]Override) (string, string) {
if over, ok := overrides[name]; ok {
return over.TypeName, over.Package
}
switch typ {
case smartcontract.AnyType:
return "interface{}"
return "interface{}", ""
case smartcontract.BoolType:
return "bool"
return "bool", ""
case smartcontract.IntegerType:
return "int"
return "int", ""
case smartcontract.ByteArrayType:
return "[]byte"
return "[]byte", ""
case smartcontract.StringType:
return "string"
return "string", ""
case smartcontract.Hash160Type:
return "interop.Hash160"
return "interop.Hash160", "github.com/nspcc-dev/neo-go/pkg/interop"
case smartcontract.Hash256Type:
return "interop.Hash256"
return "interop.Hash256", "github.com/nspcc-dev/neo-go/pkg/interop"
case smartcontract.PublicKeyType:
return "interop.PublicKey"
return "interop.PublicKey", "github.com/nspcc-dev/neo-go/pkg/interop"
case smartcontract.SignatureType:
return "interop.Signature"
return "interop.Signature", "github.com/nspcc-dev/neo-go/pkg/interop"
case smartcontract.ArrayType:
return "[]interface{}"
return "[]interface{}", ""
case smartcontract.MapType:
return "map[string]interface{}"
return "map[string]interface{}", ""
case smartcontract.InteropInterfaceType:
return "interface{}"
return "interface{}", ""
case smartcontract.VoidType:
return ""
return "", ""
default:
panic("unreachable")
}
}
func templateFromManifest(cfg Config) (contractTmpl, error) {
// TemplateFromManifest create a contract template using the given configuration
// and type conversion function.
func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract.ParamType, map[string]Override) (string, string)) (ContractTmpl, error) {
hStr := ""
for _, b := range cfg.Hash.BytesBE() {
hStr += fmt.Sprintf("\\x%02x", b)
}
ctr := contractTmpl{
ctr := ContractTmpl{
PackageName: cfg.Package,
ContractName: cfg.Manifest.Name,
Hash: hStr,
@ -169,9 +176,6 @@ func templateFromManifest(cfg Config) (contractTmpl, error) {
continue
}
imports["github.com/nspcc-dev/neo-go/pkg/interop/contract"] = struct{}{}
imports["github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"] = struct{}{}
// Consider `perform(a)` and `perform(a, b)` methods.
// First, try to export the second method with `Perform2` name.
// If `perform2` is already in the manifest, use `perform_2` with as many underscores
@ -187,7 +191,7 @@ func templateFromManifest(cfg Config) (contractTmpl, error) {
}
seen[name] = true
mtd := methodTmpl{
mtd := MethodTmpl{
Name: upperFirst(name),
NameABI: m.Name,
CallFlag: callflag.All.String(),
@ -198,49 +202,42 @@ func templateFromManifest(cfg Config) (contractTmpl, error) {
} else if m.Safe {
mtd.CallFlag = callflag.ReadOnly.String()
}
var varnames = make(map[string]bool)
for i := range m.Parameters {
name := m.Parameters[i].Name
if name == "" {
return ctr, fmt.Errorf("manifest ABI method %q/%d: parameter #%d is unnamed", m.Name, len(m.Parameters), i)
}
var typeStr string
if over, ok := cfg.Overrides[m.Name+"."+name]; ok {
typeStr = over.TypeName
if over.Package != "" {
imports[over.Package] = struct{}{}
typeStr, pkg := scTypeConverter(m.Name+"."+name, m.Parameters[i].Type, cfg.Overrides)
if pkg != "" {
imports[pkg] = struct{}{}
}
} else {
typeStr = scTypeToGo(m.Parameters[i].Type)
if token.IsKeyword(name) {
name = name + "v"
}
mtd.Arguments = append(mtd.Arguments, paramTmpl{
for varnames[name] {
name = name + "_"
}
varnames[name] = true
mtd.Arguments = append(mtd.Arguments, ParamTmpl{
Name: name,
Type: typeStr,
})
}
if over, ok := cfg.Overrides[m.Name]; ok {
mtd.ReturnType = over.TypeName
if over.Package != "" {
imports[over.Package] = struct{}{}
typeStr, pkg := scTypeConverter(m.Name, m.ReturnType, cfg.Overrides)
if pkg != "" {
imports[pkg] = struct{}{}
}
} else {
mtd.ReturnType = scTypeToGo(m.ReturnType)
switch m.ReturnType {
case smartcontract.Hash160Type, smartcontract.Hash256Type, smartcontract.InteropInterfaceType,
smartcontract.SignatureType, smartcontract.PublicKeyType:
imports["github.com/nspcc-dev/neo-go/pkg/interop"] = struct{}{}
}
}
mtd.ReturnType = typeStr
ctr.Methods = append(ctr.Methods, mtd)
}
for imp := range imports {
ctr.Imports = append(ctr.Imports, imp)
}
sort.Strings(ctr.Imports)
return ctr, nil
}
@ -248,7 +245,3 @@ func templateFromManifest(cfg Config) (contractTmpl, error) {
func upperFirst(s string) string {
return strings.ToUpper(s[0:1]) + s[1:]
}
func lowerFirst(s string) string {
return strings.ToLower(s[0:1]) + s[1:]
}

View file

@ -19,10 +19,10 @@ var (
)
var checks = map[string][]*Standard{
manifest.NEP11StandardName: {nep11NonDivisible, nep11Divisible},
manifest.NEP17StandardName: {nep17},
manifest.NEP11Payable: {nep11payable},
manifest.NEP17Payable: {nep17payable},
manifest.NEP11StandardName: {Nep11NonDivisible, Nep11Divisible},
manifest.NEP17StandardName: {Nep17},
manifest.NEP11Payable: {Nep11Payable},
manifest.NEP17Payable: {Nep17Payable},
}
// Check checks if the manifest complies with all provided standards.
@ -55,12 +55,12 @@ func check(m *manifest.Manifest, checkNames bool, standards ...string) error {
}
// Comply if m has all methods and event from st manifest and they have the same signature.
// Parameter names are ignored.
// Parameter names are checked to exactly match the ones in the given standard.
func Comply(m *manifest.Manifest, st *Standard) error {
return comply(m, true, st)
}
// ComplyABI is similar to comply but doesn't check parameter names.
// ComplyABI is similar to Comply but doesn't check parameter names.
func ComplyABI(m *manifest.Manifest, st *Standard) error {
return comply(m, false, st)
}

View file

@ -135,9 +135,9 @@ func TestCheck(t *testing.T) {
m := manifest.NewManifest("Test")
require.Error(t, Check(m, manifest.NEP17StandardName))
m.ABI.Methods = append(m.ABI.Methods, decimalTokenBase.ABI.Methods...)
m.ABI.Methods = append(m.ABI.Methods, nep17.ABI.Methods...)
m.ABI.Events = append(m.ABI.Events, nep17.ABI.Events...)
m.ABI.Methods = append(m.ABI.Methods, DecimalTokenBase.ABI.Methods...)
m.ABI.Methods = append(m.ABI.Methods, Nep17.ABI.Methods...)
m.ABI.Events = append(m.ABI.Events, Nep17.ABI.Events...)
require.NoError(t, Check(m, manifest.NEP17StandardName))
require.NoError(t, CheckABI(m, manifest.NEP17StandardName))
}

View file

@ -5,8 +5,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)
var nep11Base = &Standard{
Base: decimalTokenBase,
// Nep11Base is a Standard containing common NEP-11 methods.
var Nep11Base = &Standard{
Base: DecimalTokenBase,
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{
@ -66,8 +67,9 @@ var nep11Base = &Standard{
},
}
var nep11NonDivisible = &Standard{
Base: nep11Base,
// Nep11NonDivisible is a NEP-11 non-divisible Standard.
var Nep11NonDivisible = &Standard{
Base: Nep11Base,
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{
@ -84,8 +86,9 @@ var nep11NonDivisible = &Standard{
},
}
var nep11Divisible = &Standard{
Base: nep11Base,
// Nep11Divisible is a NEP-11 divisible Standard.
var Nep11Divisible = &Standard{
Base: Nep11Base,
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{

View file

@ -5,8 +5,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)
var nep17 = &Standard{
Base: decimalTokenBase,
// Nep17 is a NEP-17 Standard.
var Nep17 = &Standard{
Base: DecimalTokenBase,
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{

View file

@ -5,7 +5,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)
var nep11payable = &Standard{
// Nep11Payable contains NEP-11's onNEP11Payment method definition.
var Nep11Payable = &Standard{
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{{
@ -22,7 +23,8 @@ var nep11payable = &Standard{
},
}
var nep17payable = &Standard{
// Nep17Payable contains NEP-17's onNEP17Payment method definition.
var Nep17Payable = &Standard{
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{{

View file

@ -5,7 +5,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)
var decimalTokenBase = &Standard{
// DecimalTokenBase contains methods common to NEP-11 and NEP-17 token standards.
var DecimalTokenBase = &Standard{
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{

View file

@ -0,0 +1,226 @@
package rpcbinding
import (
"fmt"
"sort"
"text/template"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/binding"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
)
const srcTmpl = `
{{- define "METHOD" -}}
// {{.Name}} {{.Comment}}
func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}}
{{- if ne $index 0}}, {{end}}
{{- .Name}} {{.Type}}
{{- end}}) {{if .ReturnType }}({{ .ReturnType }}, error) {
return unwrap.{{.CallFlag}}(c.invoker.Call(Hash, "{{ .NameABI }}"{{/* CallFlag field is used for function name */}}
{{- range $arg := .Arguments -}}, {{.Name}}{{end}}))
{{- else -}} (*result.Invoke, error) {
c.invoker.Call(Hash, "{{ .NameABI }}"
{{- range $arg := .Arguments -}}, {{.Name}}{{end}})
{{- end}}
}
{{- end -}}
// Package {{.PackageName}} contains RPC wrappers for {{.ContractName}} contract.
package {{.PackageName}}
import (
{{range $m := .Imports}} "{{ $m }}"
{{end}})
// Hash contains contract hash.
var Hash = {{ .Hash }}
// Invoker is used by ContractReader to call various safe methods.
type Invoker interface {
{{if or .IsNep11D .IsNep11ND}}nep11.Invoker
{{end -}}
{{if .IsNep17}}nep17.Invoker
{{end -}}
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
}
// ContractReader implements safe contract methods.
type ContractReader struct {
{{if .IsNep11D}}nep11.DivisibleReader
{{end -}}
{{if .IsNep11ND}}nep11.NonDivisibleReader
{{end -}}
{{if .IsNep17}}nep17.TokenReader
{{end -}}
invoker Invoker
}
// NewReader creates an instance of ContractReader using Hash and the given Invoker.
func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{
{{- if .IsNep11D}}*nep11.NewDivisibleReader(invoker, Hash), {{end}}
{{- if .IsNep11ND}}*nep11.NewNonDivisibleReader(invoker, Hash), {{end}}
{{- if .IsNep17}}*nep17.NewReader(invoker, Hash), {{end -}}
invoker}
}
{{range $m := .Methods}}
{{template "METHOD" $m }}
{{end}}`
var srcTemplate = template.Must(template.New("generate").Parse(srcTmpl))
type (
ContractTmpl struct {
binding.ContractTmpl
IsNep11D bool
IsNep11ND bool
IsNep17 bool
}
)
// NewConfig initializes and returns a new config instance.
func NewConfig() binding.Config {
return binding.NewConfig()
}
// Generate writes Go file containing smartcontract bindings to the `cfg.Output`.
func Generate(cfg binding.Config) error {
bctr, err := binding.TemplateFromManifest(cfg, scTypeToGo)
if err != nil {
return err
}
ctr := scTemplateToRPC(cfg, bctr)
return srcTemplate.Execute(cfg.Output, ctr)
}
func dropManifestMethods(meths []binding.MethodTmpl, manifested []manifest.Method) []binding.MethodTmpl {
for _, m := range manifested {
for i := 0; i < len(meths); i++ {
if meths[i].NameABI == m.Name && len(meths[i].Arguments) == len(m.Parameters) {
meths = append(meths[:i], meths[i+1:]...)
i--
}
}
}
return meths
}
func dropStdMethods(meths []binding.MethodTmpl, std *standard.Standard) []binding.MethodTmpl {
meths = dropManifestMethods(meths, std.Manifest.ABI.Methods)
if std.Optional != nil {
meths = dropManifestMethods(meths, std.Optional)
}
if std.Base != nil {
return dropStdMethods(meths, std.Base)
}
return meths
}
func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]binding.Override) (string, string) {
switch typ {
case smartcontract.AnyType:
return "interface{}", ""
case smartcontract.BoolType:
return "bool", ""
case smartcontract.IntegerType:
return "*big.Int", "math/big"
case smartcontract.ByteArrayType:
return "[]byte", ""
case smartcontract.StringType:
return "string", ""
case smartcontract.Hash160Type:
return "util.Uint160", "github.com/nspcc-dev/neo-go/pkg/util"
case smartcontract.Hash256Type:
return "util.Uint256", "github.com/nspcc-dev/neo-go/pkg/util"
case smartcontract.PublicKeyType:
return "*keys.PublicKey", "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
case smartcontract.SignatureType:
return "[]byte", ""
case smartcontract.ArrayType:
return "[]interface{}", ""
case smartcontract.MapType:
return "*stackitem.Map", "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
case smartcontract.InteropInterfaceType:
return "interface{}", ""
case smartcontract.VoidType:
return "", ""
default:
panic("unreachable")
}
}
func scTemplateToRPC(cfg binding.Config, bctr binding.ContractTmpl) ContractTmpl {
var imports = make(map[string]struct{})
var ctr = ContractTmpl{ContractTmpl: bctr}
for i := range ctr.Imports {
imports[ctr.Imports[i]] = struct{}{}
}
ctr.Hash = fmt.Sprintf("%#v", cfg.Hash)
for _, std := range cfg.Manifest.SupportedStandards {
if std == manifest.NEP11StandardName {
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"] = struct{}{}
if standard.ComplyABI(cfg.Manifest, standard.Nep11Divisible) == nil {
ctr.Methods = dropStdMethods(ctr.Methods, standard.Nep11Divisible)
ctr.IsNep11D = true
} else if standard.ComplyABI(cfg.Manifest, standard.Nep11NonDivisible) == nil {
ctr.Methods = dropStdMethods(ctr.Methods, standard.Nep11NonDivisible)
ctr.IsNep11ND = true
}
break // Can't be NEP-17 at the same time.
}
if std == manifest.NEP17StandardName && standard.ComplyABI(cfg.Manifest, standard.Nep17) == nil {
ctr.Methods = dropStdMethods(ctr.Methods, standard.Nep17)
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"] = struct{}{}
ctr.IsNep17 = true
break // Can't be NEP-11 at the same time.
}
}
for i := 0; i < len(ctr.Methods); i++ {
abim := cfg.Manifest.ABI.GetMethod(ctr.Methods[i].NameABI, len(ctr.Methods[i].Arguments))
if !abim.Safe {
ctr.Methods = append(ctr.Methods[:i], ctr.Methods[i+1:]...)
i--
}
}
// We're misusing CallFlag field for function name here.
for i := range ctr.Methods {
switch ctr.Methods[i].ReturnType {
case "interface{}":
imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{}
ctr.Methods[i].ReturnType = "stackitem.Item"
ctr.Methods[i].CallFlag = "Item"
case "bool":
ctr.Methods[i].CallFlag = "Bool"
case "*big.Int":
ctr.Methods[i].CallFlag = "BigInt"
case "string":
ctr.Methods[i].CallFlag = "UTF8String"
case "util.Uint160":
ctr.Methods[i].CallFlag = "Uint160"
case "util.Uint256":
ctr.Methods[i].CallFlag = "Uint256"
case "*keys.PublicKey":
ctr.Methods[i].CallFlag = "PublicKey"
case "[]byte":
ctr.Methods[i].CallFlag = "Bytes"
case "[]interface{}":
imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{}
ctr.Methods[i].ReturnType = "[]stackitem.Item"
ctr.Methods[i].CallFlag = "Array"
case "*stackitem.Map":
ctr.Methods[i].CallFlag = "Map"
}
}
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"] = struct{}{}
imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{}
ctr.Imports = ctr.Imports[:0]
for imp := range imports {
ctr.Imports = append(ctr.Imports, imp)
}
sort.Strings(ctr.Imports)
return ctr
}