mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-22 19:29:39 +00:00
rpcbinding: add GAS testcase, fix methodless wrappers
* strip NEP-XX methods before going into generator to avoid unused imports * nepXX.Invoker types already include Call * always import util, it's used for Hash
This commit is contained in:
parent
aeb61fb61d
commit
df29008a50
6 changed files with 122 additions and 66 deletions
|
@ -294,6 +294,7 @@ package myspacecontract
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
"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/rpcclient/unwrap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"math/big"
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -328,34 +329,34 @@ func TestGenerateRPCBindings(t *testing.T) {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Commands = []cli.Command{generateWrapperCmd, generateRPCWrapperCmd}
|
app.Commands = []cli.Command{generateWrapperCmd, generateRPCWrapperCmd}
|
||||||
|
|
||||||
outFile := filepath.Join(tmpDir, "out.go")
|
var checkBinding = func(manifest string, hash string, good string) {
|
||||||
require.NoError(t, app.Run([]string{"", "generate-rpcwrapper",
|
t.Run(manifest, func(t *testing.T) {
|
||||||
"--manifest", filepath.Join("testdata", "nex", "nex.manifest.json"),
|
outFile := filepath.Join(tmpDir, "out.go")
|
||||||
"--out", outFile,
|
require.NoError(t, app.Run([]string{"", "generate-rpcwrapper",
|
||||||
"--hash", "0xa2a67f09e8cf22c6bfd5cea24adc0f4bf0a11aa8",
|
"--manifest", manifest,
|
||||||
}))
|
"--out", outFile,
|
||||||
|
"--hash", hash,
|
||||||
|
}))
|
||||||
|
|
||||||
data, err := os.ReadFile(outFile)
|
data, err := os.ReadFile(outFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
|
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
|
||||||
expected, err := os.ReadFile(filepath.Join("testdata", "nex", "nex.go"))
|
expected, err := os.ReadFile(good)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
|
expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
|
||||||
require.Equal(t, string(expected), string(data))
|
require.Equal(t, string(expected), string(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
require.NoError(t, app.Run([]string{"", "generate-rpcwrapper",
|
checkBinding(filepath.Join("testdata", "nex", "nex.manifest.json"),
|
||||||
"--manifest", filepath.Join("testdata", "nameservice", "nns.manifest.json"),
|
"0xa2a67f09e8cf22c6bfd5cea24adc0f4bf0a11aa8",
|
||||||
"--out", outFile,
|
filepath.Join("testdata", "nex", "nex.go"))
|
||||||
"--hash", "0x50ac1c37690cc2cfc594472833cf57505d5f46de",
|
checkBinding(filepath.Join("testdata", "nameservice", "nns.manifest.json"),
|
||||||
}))
|
"0x50ac1c37690cc2cfc594472833cf57505d5f46de",
|
||||||
|
filepath.Join("testdata", "nameservice", "nns.go"))
|
||||||
data, err = os.ReadFile(outFile)
|
checkBinding(filepath.Join("testdata", "gas", "gas.manifest.json"),
|
||||||
require.NoError(t, err)
|
"0xd2a4cff31913016155e38e474a2c06d08be276cf",
|
||||||
data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
|
filepath.Join("testdata", "gas", "gas.go"))
|
||||||
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) {
|
func TestGenerate_Errors(t *testing.T) {
|
||||||
|
|
46
cli/smartcontract/testdata/gas/gas.go
vendored
Normal file
46
cli/smartcontract/testdata/gas/gas.go
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Package gastoken contains RPC wrappers for GasToken contract.
|
||||||
|
package gastoken
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash contains contract hash.
|
||||||
|
var Hash = util.Uint160{0xcf, 0x76, 0xe2, 0x8b, 0xd0, 0x6, 0x2c, 0x4a, 0x47, 0x8e, 0xe3, 0x55, 0x61, 0x1, 0x13, 0x19, 0xf3, 0xcf, 0xa4, 0xd2}
|
||||||
|
|
||||||
|
// Invoker is used by ContractReader to call various safe methods.
|
||||||
|
type Invoker interface {
|
||||||
|
nep17.Invoker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actor is used by Contract to call state-changing methods.
|
||||||
|
type Actor interface {
|
||||||
|
Invoker
|
||||||
|
nep17.Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContractReader implements safe contract methods.
|
||||||
|
type ContractReader struct {
|
||||||
|
nep17.TokenReader
|
||||||
|
invoker Invoker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contract implements all contract methods.
|
||||||
|
type Contract struct {
|
||||||
|
ContractReader
|
||||||
|
nep17.TokenWriter
|
||||||
|
actor Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader creates an instance of ContractReader using Hash and the given Invoker.
|
||||||
|
func NewReader(invoker Invoker) *ContractReader {
|
||||||
|
return &ContractReader{*nep17.NewReader(invoker, Hash), invoker}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates an instance of Contract using Hash and the given Actor.
|
||||||
|
func New(actor Actor) *Contract {
|
||||||
|
var nep17t = nep17.New(actor, Hash)
|
||||||
|
return &Contract{ContractReader{nep17t.TokenReader, actor},nep17t.TokenWriter, actor}
|
||||||
|
}
|
||||||
|
|
1
cli/smartcontract/testdata/gas/gas.manifest.json
vendored
Normal file
1
cli/smartcontract/testdata/gas/gas.manifest.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"name":"GasToken","groups":[],"features":{},"supportedstandards":["NEP-17"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"symbol","parameters":[],"returntype":"String","offset":14,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":28,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}
|
|
@ -3,7 +3,6 @@ package nameservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"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/nep11"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
@ -18,13 +17,13 @@ var Hash = util.Uint160{0xde, 0x46, 0x5f, 0x5d, 0x50, 0x57, 0xcf, 0x33, 0x28, 0x
|
||||||
// Invoker is used by ContractReader to call various safe methods.
|
// Invoker is used by ContractReader to call various safe methods.
|
||||||
type Invoker interface {
|
type Invoker interface {
|
||||||
nep11.Invoker
|
nep11.Invoker
|
||||||
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actor is used by Contract to call state-changing methods.
|
// Actor is used by Contract to call state-changing methods.
|
||||||
type Actor interface {
|
type Actor interface {
|
||||||
Invoker
|
Invoker
|
||||||
nep11.Actor
|
nep11.Actor
|
||||||
|
|
||||||
MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error)
|
MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error)
|
||||||
MakeRun(script []byte) (*transaction.Transaction, error)
|
MakeRun(script []byte) (*transaction.Transaction, error)
|
||||||
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error)
|
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error)
|
||||||
|
|
2
cli/smartcontract/testdata/nex/nex.go
vendored
2
cli/smartcontract/testdata/nex/nex.go
vendored
|
@ -4,7 +4,6 @@ package nextoken
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"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/nep17"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
@ -17,7 +16,6 @@ var Hash = util.Uint160{0xa8, 0x1a, 0xa1, 0xf0, 0x4b, 0xf, 0xdc, 0x4a, 0xa2, 0xc
|
||||||
// Invoker is used by ContractReader to call various safe methods.
|
// Invoker is used by ContractReader to call various safe methods.
|
||||||
type Invoker interface {
|
type Invoker interface {
|
||||||
nep17.Invoker
|
nep17.Invoker
|
||||||
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actor is used by Contract to call state-changing methods.
|
// Actor is used by Contract to call state-changing methods.
|
||||||
|
|
|
@ -92,20 +92,18 @@ var Hash = {{ .Hash }}
|
||||||
|
|
||||||
// Invoker is used by ContractReader to call various safe methods.
|
// Invoker is used by ContractReader to call various safe methods.
|
||||||
type Invoker interface {
|
type Invoker interface {
|
||||||
{{if or .IsNep11D .IsNep11ND}}nep11.Invoker
|
{{if or .IsNep11D .IsNep11ND}} nep11.Invoker
|
||||||
{{end -}}
|
{{else if .IsNep17}} nep17.Invoker
|
||||||
{{if .IsNep17}}nep17.Invoker
|
{{else if len .SafeMethods}} Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
|
||||||
{{end -}}
|
{{end -}}
|
||||||
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{{if .HasWriter}}// Actor is used by Contract to call state-changing methods.
|
{{if .HasWriter}}// Actor is used by Contract to call state-changing methods.
|
||||||
type Actor interface {
|
type Actor interface {
|
||||||
Invoker
|
Invoker
|
||||||
{{if or .IsNep11D .IsNep11ND}}nep11.Actor{{end -}}
|
{{if or .IsNep11D .IsNep11ND}} nep11.Actor
|
||||||
{{if .IsNep17}}nep17.Actor{{end -}}
|
{{else if .IsNep17}} nep17.Actor{{end}}
|
||||||
{{if len .Methods}}
|
{{if len .Methods}} MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error)
|
||||||
MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error)
|
|
||||||
MakeRun(script []byte) (*transaction.Transaction, error)
|
MakeRun(script []byte) (*transaction.Transaction, error)
|
||||||
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error)
|
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error)
|
||||||
MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error)
|
MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error)
|
||||||
|
@ -198,19 +196,50 @@ func NewConfig() binding.Config {
|
||||||
|
|
||||||
// Generate writes Go file containing smartcontract bindings to the `cfg.Output`.
|
// Generate writes Go file containing smartcontract bindings to the `cfg.Output`.
|
||||||
func Generate(cfg binding.Config) error {
|
func Generate(cfg binding.Config) error {
|
||||||
|
// Avoid changing *cfg.Manifest.
|
||||||
|
mfst := *cfg.Manifest
|
||||||
|
mfst.ABI.Methods = make([]manifest.Method, len(mfst.ABI.Methods))
|
||||||
|
copy(mfst.ABI.Methods, cfg.Manifest.ABI.Methods)
|
||||||
|
cfg.Manifest = &mfst
|
||||||
|
|
||||||
|
var imports = make(map[string]struct{})
|
||||||
|
var ctr ContractTmpl
|
||||||
|
|
||||||
|
// Strip standard methods from NEP-XX packages.
|
||||||
|
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 {
|
||||||
|
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11Divisible)
|
||||||
|
ctr.IsNep11D = true
|
||||||
|
} else if standard.ComplyABI(cfg.Manifest, standard.Nep11NonDivisible) == nil {
|
||||||
|
mfst.ABI.Methods = dropStdMethods(mfst.ABI.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 {
|
||||||
|
mfst.ABI.Methods = dropStdMethods(mfst.ABI.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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bctr, err := binding.TemplateFromManifest(cfg, scTypeToGo)
|
bctr, err := binding.TemplateFromManifest(cfg, scTypeToGo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctr := scTemplateToRPC(cfg, bctr)
|
ctr.ContractTmpl = bctr
|
||||||
|
ctr = scTemplateToRPC(cfg, ctr, imports)
|
||||||
|
|
||||||
return srcTemplate.Execute(cfg.Output, ctr)
|
return srcTemplate.Execute(cfg.Output, ctr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dropManifestMethods(meths []binding.MethodTmpl, manifested []manifest.Method) []binding.MethodTmpl {
|
func dropManifestMethods(meths []manifest.Method, manifested []manifest.Method) []manifest.Method {
|
||||||
for _, m := range manifested {
|
for _, m := range manifested {
|
||||||
for i := 0; i < len(meths); i++ {
|
for i := 0; i < len(meths); i++ {
|
||||||
if meths[i].NameABI == m.Name && len(meths[i].Arguments) == len(m.Parameters) {
|
if meths[i].Name == m.Name && len(meths[i].Parameters) == len(m.Parameters) {
|
||||||
meths = append(meths[:i], meths[i+1:]...)
|
meths = append(meths[:i], meths[i+1:]...)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
|
@ -219,7 +248,7 @@ func dropManifestMethods(meths []binding.MethodTmpl, manifested []manifest.Metho
|
||||||
return meths
|
return meths
|
||||||
}
|
}
|
||||||
|
|
||||||
func dropStdMethods(meths []binding.MethodTmpl, std *standard.Standard) []binding.MethodTmpl {
|
func dropStdMethods(meths []manifest.Method, std *standard.Standard) []manifest.Method {
|
||||||
meths = dropManifestMethods(meths, std.Manifest.ABI.Methods)
|
meths = dropManifestMethods(meths, std.Manifest.ABI.Methods)
|
||||||
if std.Optional != nil {
|
if std.Optional != nil {
|
||||||
meths = dropManifestMethods(meths, std.Optional)
|
meths = dropManifestMethods(meths, std.Optional)
|
||||||
|
@ -263,32 +292,11 @@ func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scTemplateToRPC(cfg binding.Config, bctr binding.ContractTmpl) ContractTmpl {
|
func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]struct{}) ContractTmpl {
|
||||||
var imports = make(map[string]struct{})
|
|
||||||
var ctr = ContractTmpl{ContractTmpl: bctr}
|
|
||||||
for i := range ctr.Imports {
|
for i := range ctr.Imports {
|
||||||
imports[ctr.Imports[i]] = struct{}{}
|
imports[ctr.Imports[i]] = struct{}{}
|
||||||
}
|
}
|
||||||
ctr.Hash = fmt.Sprintf("%#v", cfg.Hash)
|
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++ {
|
for i := 0; i < len(ctr.Methods); i++ {
|
||||||
abim := cfg.Manifest.ABI.GetMethod(ctr.Methods[i].NameABI, len(ctr.Methods[i].Arguments))
|
abim := cfg.Manifest.ABI.GetMethod(ctr.Methods[i].NameABI, len(ctr.Methods[i].Arguments))
|
||||||
if abim.Safe {
|
if abim.Safe {
|
||||||
|
@ -332,9 +340,12 @@ func scTemplateToRPC(cfg binding.Config, bctr binding.ContractTmpl) ContractTmpl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{}
|
imports["github.com/nspcc-dev/neo-go/pkg/util"] = struct{}{}
|
||||||
if len(ctr.SafeMethods) > 0 {
|
if len(ctr.SafeMethods) > 0 {
|
||||||
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"] = struct{}{}
|
imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"] = struct{}{}
|
||||||
|
if !(ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND) {
|
||||||
|
imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(ctr.Methods) > 0 {
|
if len(ctr.Methods) > 0 {
|
||||||
imports["github.com/nspcc-dev/neo-go/pkg/core/transaction"] = struct{}{}
|
imports["github.com/nspcc-dev/neo-go/pkg/core/transaction"] = struct{}{}
|
||||||
|
|
Loading…
Reference in a new issue