From d569fe01e670a8d05050ecc039f84c5db43754b6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 9 Nov 2022 10:20:44 +0300 Subject: [PATCH 1/5] rpcbinding: initial support for iterators, see #2768 Already better than stackitem.Item. --- cli/smartcontract/testdata/nameservice/nns.go | 11 ++++++----- docs/compiler.md | 5 ++++- pkg/smartcontract/rpcbinding/binding.go | 14 +++++++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/cli/smartcontract/testdata/nameservice/nns.go b/cli/smartcontract/testdata/nameservice/nns.go index 5c849b8e7..daf1e6842 100644 --- a/cli/smartcontract/testdata/nameservice/nns.go +++ b/cli/smartcontract/testdata/nameservice/nns.go @@ -2,12 +2,13 @@ package nameservice import ( + "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "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/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "math/big" ) @@ -59,8 +60,8 @@ func New(actor Actor) *Contract { // Roots invokes `roots` method of contract. -func (c *ContractReader) Roots() (stackitem.Item, error) { - return unwrap.Item(c.invoker.Call(Hash, "roots")) +func (c *ContractReader) Roots() (uuid.UUID, result.Iterator, error) { + return unwrap.SessionIterator(c.invoker.Call(Hash, "roots")) } // GetPrice invokes `getPrice` method of contract. @@ -79,8 +80,8 @@ func (c *ContractReader) GetRecord(name string, typev *big.Int) (string, error) } // GetAllRecords invokes `getAllRecords` method of contract. -func (c *ContractReader) GetAllRecords(name string) (stackitem.Item, error) { - return unwrap.Item(c.invoker.Call(Hash, "getAllRecords", name)) +func (c *ContractReader) GetAllRecords(name string) (uuid.UUID, result.Iterator, error) { + return unwrap.SessionIterator(c.invoker.Call(Hash, "getAllRecords", name)) } // Resolve invokes `resolve` method of contract. diff --git a/docs/compiler.md b/docs/compiler.md index ab1358b7e..78e6d0416 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -449,7 +449,10 @@ returns false. 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. +stackitem types. Any InteropInterface returned from a method is treated as +iterator and an appropriate unwrapper is used with UUID and iterator structure +result. This pair can then be used in Invoker `TraverseIterator` method to +retrieve actual resulting items. ``` $ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176 diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index d3e9bb507..8fdc4d26a 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -334,9 +334,17 @@ func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]st for i := range ctr.SafeMethods { switch ctr.SafeMethods[i].ReturnType { case "interface{}": - imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} - ctr.SafeMethods[i].ReturnType = "stackitem.Item" - ctr.SafeMethods[i].CallFlag = "Item" + abim := cfg.Manifest.ABI.GetMethod(ctr.SafeMethods[i].NameABI, len(ctr.SafeMethods[i].Arguments)) + if abim.ReturnType == smartcontract.InteropInterfaceType { + imports["github.com/google/uuid"] = struct{}{} + imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{} + ctr.SafeMethods[i].ReturnType = "uuid.UUID, result.Iterator" + ctr.SafeMethods[i].CallFlag = "SessionIterator" + } else { + imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} + ctr.SafeMethods[i].ReturnType = "stackitem.Item" + ctr.SafeMethods[i].CallFlag = "Item" + } case "bool": ctr.SafeMethods[i].CallFlag = "Bool" case "*big.Int": From a7f86dcb7fbc31dcfdaa33a11ff066308819c051 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 9 Nov 2022 11:58:01 +0300 Subject: [PATCH 2/5] rpcbinding: generate Expanded methods for iterators Refs. #2768. --- cli/smartcontract/generate_test.go | 3 + cli/smartcontract/testdata/nameservice/nns.go | 19 ++++++ cli/smartcontract/testdata/nonepiter/iter.go | 60 +++++++++++++++++++ .../testdata/nonepiter/iter.manifest.json | 33 ++++++++++ pkg/smartcontract/rpcbinding/binding.go | 26 +++++++- 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 cli/smartcontract/testdata/nonepiter/iter.go create mode 100644 cli/smartcontract/testdata/nonepiter/iter.manifest.json diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index b6a1bf253..80a33aa89 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -360,6 +360,9 @@ func TestGenerateRPCBindings(t *testing.T) { checkBinding(filepath.Join("testdata", "verifyrpc", "verify.manifest.json"), "0x00112233445566778899aabbccddeeff00112233", filepath.Join("testdata", "verifyrpc", "verify.go")) + checkBinding(filepath.Join("testdata", "nonepiter", "iter.manifest.json"), + "0x00112233445566778899aabbccddeeff00112233", + filepath.Join("testdata", "nonepiter", "iter.go")) } func TestGenerate_Errors(t *testing.T) { diff --git a/cli/smartcontract/testdata/nameservice/nns.go b/cli/smartcontract/testdata/nameservice/nns.go index daf1e6842..d156d985c 100644 --- a/cli/smartcontract/testdata/nameservice/nns.go +++ b/cli/smartcontract/testdata/nameservice/nns.go @@ -9,6 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "math/big" ) @@ -64,6 +65,15 @@ func (c *ContractReader) Roots() (uuid.UUID, result.Iterator, error) { return unwrap.SessionIterator(c.invoker.Call(Hash, "roots")) } +// RootsExpanded is similar to Roots (uses the same contract +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get the specified +// number of result items from the iterator right in the VM and return them to +// you. It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) RootsExpanded(_numOfIteratorItems int) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "roots", _numOfIteratorItems)) +} + // 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)) @@ -84,6 +94,15 @@ func (c *ContractReader) GetAllRecords(name string) (uuid.UUID, result.Iterator, return unwrap.SessionIterator(c.invoker.Call(Hash, "getAllRecords", name)) } +// GetAllRecordsExpanded is similar to GetAllRecords (uses the same contract +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get the specified +// number of result items from the iterator right in the VM and return them to +// you. It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) GetAllRecordsExpanded(name string, _numOfIteratorItems int) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "getAllRecords", _numOfIteratorItems, 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)) diff --git a/cli/smartcontract/testdata/nonepiter/iter.go b/cli/smartcontract/testdata/nonepiter/iter.go new file mode 100644 index 000000000..c7323fb56 --- /dev/null +++ b/cli/smartcontract/testdata/nonepiter/iter.go @@ -0,0 +1,60 @@ +// Package nonnepxxcontractwithiterators contains RPC wrappers for Non-NEPXX contract with iterators contract. +package nonnepxxcontractwithiterators + +import ( + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "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" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) + CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) + TerminateSession(sessionID uuid.UUID) error + TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, 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} +} + + +// Tokens invokes `tokens` method of contract. +func (c *ContractReader) Tokens() (uuid.UUID, result.Iterator, error) { + return unwrap.SessionIterator(c.invoker.Call(Hash, "tokens")) +} + +// TokensExpanded is similar to Tokens (uses the same contract +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get the specified +// number of result items from the iterator right in the VM and return them to +// you. It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) TokensExpanded(_numOfIteratorItems int) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "tokens", _numOfIteratorItems)) +} + +// GetAllRecords invokes `getAllRecords` method of contract. +func (c *ContractReader) GetAllRecords(name string) (uuid.UUID, result.Iterator, error) { + return unwrap.SessionIterator(c.invoker.Call(Hash, "getAllRecords", name)) +} + +// GetAllRecordsExpanded is similar to GetAllRecords (uses the same contract +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get the specified +// number of result items from the iterator right in the VM and return them to +// you. It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) GetAllRecordsExpanded(name string, _numOfIteratorItems int) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "getAllRecords", _numOfIteratorItems, name)) +} diff --git a/cli/smartcontract/testdata/nonepiter/iter.manifest.json b/cli/smartcontract/testdata/nonepiter/iter.manifest.json new file mode 100644 index 000000000..9f33e27d3 --- /dev/null +++ b/cli/smartcontract/testdata/nonepiter/iter.manifest.json @@ -0,0 +1,33 @@ +{ + "groups" : [], + "abi" : { + "events" : [], + "methods" : [ + { + "parameters" : [], + "safe" : true, + "name" : "tokens", + "offset" : 0, + "returntype" : "InteropInterface" + }, + { + "offset" : 1, + "returntype" : "InteropInterface", + "safe" : true, + "parameters" : [ + { + "type" : "String", + "name" : "name" + } + ], + "name" : "getAllRecords" + } + ] + }, + "supportedstandards" : [], + "trusts" : [], + "extra" : {}, + "permissions" : [], + "name" : "Non-NEPXX contract with iterators", + "features" : {} +} diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index 8fdc4d26a..89550646f 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -25,6 +25,17 @@ func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}} {{- range $arg := .Arguments -}}, {{.Name}}{{end}}) {{- end}} } +{{- if eq .CallFlag "SessionIterator"}} + +// {{.Name}}Expanded is similar to {{.Name}} (uses the same contract +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get the specified +// number of result items from the iterator right in the VM and return them to +// you. It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) {{.Name}}Expanded({{range $index, $arg := .Arguments}}{{.Name}} {{.Type}}, {{end}}_numOfIteratorItems int) ([]stackitem.Item, error) { + return unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "{{.NameABI}}", _numOfIteratorItems{{range $arg := .Arguments}}, {{.Name}}{{end}})) +} +{{- end -}} {{- end -}} {{- define "METHOD" -}} {{- if eq .ReturnType "bool"}}func scriptFor{{.Name}}({{range $index, $arg := .Arguments -}} @@ -93,9 +104,15 @@ var Hash = {{ .Hash }} {{if .HasReader}}// Invoker is used by ContractReader to call various safe methods. type Invoker interface { {{if or .IsNep11D .IsNep11ND}} nep11.Invoker -{{else if .IsNep17}} nep17.Invoker +{{else -}} +{{ if .IsNep17}} nep17.Invoker {{else if len .SafeMethods}} Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) {{end -}} +{{if .HasIterator}} CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) + TerminateSession(sessionID uuid.UUID) error + TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) +{{end -}} +{{end -}} } {{end -}} @@ -196,8 +213,9 @@ type ( IsNep11ND bool IsNep17 bool - HasReader bool - HasWriter bool + HasReader bool + HasWriter bool + HasIterator bool } ) @@ -337,9 +355,11 @@ func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]st abim := cfg.Manifest.ABI.GetMethod(ctr.SafeMethods[i].NameABI, len(ctr.SafeMethods[i].Arguments)) if abim.ReturnType == smartcontract.InteropInterfaceType { imports["github.com/google/uuid"] = struct{}{} + imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{} ctr.SafeMethods[i].ReturnType = "uuid.UUID, result.Iterator" ctr.SafeMethods[i].CallFlag = "SessionIterator" + ctr.HasIterator = true } else { imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} ctr.SafeMethods[i].ReturnType = "stackitem.Item" From be02eea7b159ca22637d2ea7a833c6d72b0064af Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 9 Nov 2022 12:33:20 +0300 Subject: [PATCH 3/5] binding: precompile template, remove useless error condition --- pkg/smartcontract/binding/generate.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index b3a735917..e2f3aeb66 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -79,6 +79,8 @@ type ( } ) +var srcTemplate = template.Must(template.New("generate").Parse(srcTmpl)) + // NewConfig initializes and returns a new config instance. func NewConfig() Config { return Config{ @@ -97,12 +99,7 @@ func Generate(cfg Config) error { ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal") sort.Strings(ctr.Imports) - tmp, err := template.New("generate").Parse(srcTmpl) - if err != nil { - return err - } - - return tmp.Execute(cfg.Output, ctr) + return srcTemplate.Execute(cfg.Output, ctr) } func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]Override) (string, string) { From 145ebad90e0d32bcafe435993b429fb745aab331 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 9 Nov 2022 12:44:14 +0300 Subject: [PATCH 4/5] binding: drop the only error condition from TemplateFromManifest Simplify the interface, we do IsValid() check anyway in the CLI and it covers this condition as well. --- pkg/smartcontract/binding/generate.go | 18 +++++++----------- pkg/smartcontract/rpcbinding/binding.go | 8 +++----- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index e2f3aeb66..97b1fdbfb 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -90,11 +90,10 @@ func NewConfig() Config { } // Generate writes Go file containing smartcontract bindings to the `cfg.Output`. +// It doesn't check manifest from Config for validity, incorrect manifest can +// lead to unexpected results. func Generate(cfg Config) error { - ctr, err := TemplateFromManifest(cfg, scTypeToGo) - if err != nil { - return err - } + ctr := TemplateFromManifest(cfg, scTypeToGo) 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) @@ -140,8 +139,9 @@ func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]O } // 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) { +// and type conversion function. It assumes manifest to be present in the +// configuration and assumes it to be correct (passing IsValid check). +func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract.ParamType, map[string]Override) (string, string)) ContractTmpl { hStr := "" for _, b := range cfg.Hash.BytesBE() { hStr += fmt.Sprintf("\\x%02x", b) @@ -203,10 +203,6 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract 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) - } - typeStr, pkg := scTypeConverter(m.Name+"."+name, m.Parameters[i].Type, cfg.Overrides) if pkg != "" { imports[pkg] = struct{}{} @@ -236,7 +232,7 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract ctr.Imports = append(ctr.Imports, imp) } - return ctr, nil + return ctr } func upperFirst(s string) string { diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index 89550646f..be121c828 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -225,6 +225,8 @@ func NewConfig() binding.Config { } // Generate writes Go file containing smartcontract bindings to the `cfg.Output`. +// It doesn't check manifest from Config for validity, incorrect manifest can +// lead to unexpected results. func Generate(cfg binding.Config) error { // Avoid changing *cfg.Manifest. mfst := *cfg.Manifest @@ -264,11 +266,7 @@ func Generate(cfg binding.Config) error { mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep17Payable) } - bctr, err := binding.TemplateFromManifest(cfg, scTypeToGo) - if err != nil { - return err - } - ctr.ContractTmpl = bctr + ctr.ContractTmpl = binding.TemplateFromManifest(cfg, scTypeToGo) ctr = scTemplateToRPC(cfg, ctr, imports) return srcTemplate.Execute(cfg.Output, ctr) From ea44367c97c1909067a4ad1d952b9e327f2042e3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 9 Nov 2022 12:50:57 +0300 Subject: [PATCH 5/5] cli/smartcontract: generate bindings also for examples We do compilation test for them, just to check that the compiler doesn't choke for any reason, but we can do the same for wrapper generators. --- cli/smartcontract/contract_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cli/smartcontract/contract_test.go b/cli/smartcontract/contract_test.go index 5d3984520..4d9a8c81c 100644 --- a/cli/smartcontract/contract_test.go +++ b/cli/smartcontract/contract_test.go @@ -1004,6 +1004,9 @@ func TestCompileExamples(t *testing.T) { outF := filepath.Join(tmpDir, info.Name()+".nef") manifestF := filepath.Join(tmpDir, info.Name()+".manifest.json") + bindingF := filepath.Join(tmpDir, info.Name()+".binding.yml") + wrapperF := filepath.Join(tmpDir, info.Name()+".go") + rpcWrapperF := filepath.Join(tmpDir, info.Name()+".rpc.go") cfgName := filterFilename(infos, ".yml") opts := []string{ @@ -1012,6 +1015,7 @@ func TestCompileExamples(t *testing.T) { "--out", outF, "--manifest", manifestF, "--config", filepath.Join(examplePath, info.Name(), cfgName), + "--bindings", bindingF, } e.Run(t, opts...) @@ -1030,6 +1034,16 @@ func TestCompileExamples(t *testing.T) { require.NotNil(t, m.ABI.GetMethod("put", 1)) require.NotNil(t, m.ABI.GetMethod("put", 2)) } + e.Run(t, "neo-go", "contract", "generate-wrapper", + "--manifest", manifestF, + "--config", bindingF, + "--out", wrapperF, + "--hash", "0x00112233445566778899aabbccddeeff00112233") + e.Run(t, "neo-go", "contract", "generate-rpcwrapper", + "--manifest", manifestF, + "--config", bindingF, + "--out", rpcWrapperF, + "--hash", "0x00112233445566778899aabbccddeeff00112233") }) }