Merge pull request #1669 from nspcc-dev/native-contract-by-id
Call native contracts by ID
This commit is contained in:
commit
7d937827e3
20 changed files with 188 additions and 191 deletions
BIN
cli/testdata/chain50x2.acc
vendored
BIN
cli/testdata/chain50x2.acc
vendored
Binary file not shown.
|
@ -20,7 +20,6 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
@ -557,7 +556,7 @@ func TestVerifyTx(t *testing.T) {
|
||||||
})
|
})
|
||||||
tx.Scripts = append(tx.Scripts, transaction.Witness{})
|
tx.Scripts = append(tx.Scripts, transaction.Witness{})
|
||||||
t.Run("NonZeroVerification", func(t *testing.T) {
|
t.Run("NonZeroVerification", func(t *testing.T) {
|
||||||
script, _ := state.CreateNativeContractHash(nativenames.Oracle)
|
script, _ := state.CreateNativeContractHash(-6) //oracleContractID
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.Opcodes(w.BinWriter, opcode.ABORT)
|
emit.Opcodes(w.BinWriter, opcode.ABORT)
|
||||||
emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE())
|
emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE())
|
||||||
|
@ -975,7 +974,7 @@ func TestVerifyTx(t *testing.T) {
|
||||||
fee.Opcode(bc.GetBaseExecFee(), // Notary verification script
|
fee.Opcode(bc.GetBaseExecFee(), // Notary verification script
|
||||||
opcode.PUSHDATA1, opcode.RET, // invocation script
|
opcode.PUSHDATA1, opcode.RET, // invocation script
|
||||||
opcode.PUSHDATA1, opcode.RET, // arguments for native verification call
|
opcode.PUSHDATA1, opcode.RET, // arguments for native verification call
|
||||||
opcode.PUSHDATA1, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call
|
opcode.PUSHINT8, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call
|
||||||
native.NotaryVerificationPrice // Notary witness verification price
|
native.NotaryVerificationPrice // Notary witness verification price
|
||||||
tx.Scripts = []transaction.Witness{
|
tx.Scripts = []transaction.Witness{
|
||||||
{
|
{
|
||||||
|
|
|
@ -108,9 +108,10 @@ type ContractMD struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContractMD returns Contract with the specified list of methods.
|
// NewContractMD returns Contract with the specified list of methods.
|
||||||
func NewContractMD(name string) *ContractMD {
|
func NewContractMD(name string, id int32) *ContractMD {
|
||||||
c := &ContractMD{
|
c := &ContractMD{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
ContractID: id,
|
||||||
Methods: make(map[string]MethodAndPrice),
|
Methods: make(map[string]MethodAndPrice),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +120,7 @@ func NewContractMD(name string) *ContractMD {
|
||||||
c.NEF.Header.Compiler = "ScriptBuilder"
|
c.NEF.Header.Compiler = "ScriptBuilder"
|
||||||
c.NEF.Header.Magic = nef.Magic
|
c.NEF.Header.Magic = nef.Magic
|
||||||
c.NEF.Header.Version = "3.0"
|
c.NEF.Header.Version = "3.0"
|
||||||
c.NEF.Script, c.Hash = state.CreateNativeContractHash(c.Name)
|
c.NEF.Script, c.Hash = state.CreateNativeContractHash(id)
|
||||||
c.NEF.Checksum = c.NEF.CalculateChecksum()
|
c.NEF.Checksum = c.NEF.CalculateChecksum()
|
||||||
c.Manifest = *manifest.DefaultManifest(name)
|
c.Manifest = *manifest.DefaultManifest(name)
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,12 @@ import (
|
||||||
|
|
||||||
// Compatibility test. hashes are taken directly from C# node.
|
// Compatibility test. hashes are taken directly from C# node.
|
||||||
func TestNativeHashes(t *testing.T) {
|
func TestNativeHashes(t *testing.T) {
|
||||||
require.Equal(t, "136ec44854ad9a714901eb7d714714f1791203f2", newDesignate(false).Hash.StringLE())
|
require.Equal(t, "bee421fdbb3e791265d2104cb34934f53fcc0e45", newManagement().Hash.StringLE())
|
||||||
require.Equal(t, "a6a6c15dcdc9b997dac448b6926522d22efeedfb", newGAS().Hash.StringLE())
|
require.Equal(t, "4961bf0ab79370b23dc45cde29f568d0e0fa6e93", newNEO().Hash.StringLE())
|
||||||
require.Equal(t, "081514120c7894779309255b7fb18b376cec731a", newManagement().Hash.StringLE())
|
require.Equal(t, "9ac04cf223f646de5f7faccafe34e30e5d4382a2", newGAS().Hash.StringLE())
|
||||||
require.Equal(t, "0a46e2e37c9987f570b4af253fb77e7eef0f72b6", newNEO().Hash.StringLE())
|
require.Equal(t, "c939a4af1c762e5edca36d4b61c06ba82c4c6ff5", newPolicy().Hash.StringLE())
|
||||||
|
require.Equal(t, "f4bbd95569e8dda2cb84eb609a1488ddd0d9fa91", newDesignate(false).Hash.StringLE())
|
||||||
|
require.Equal(t, "8cd3889136056b3304ec59f6d424b8767710ed79", newOracle().Hash.StringLE())
|
||||||
// Not yet a part of NEO.
|
// Not yet a part of NEO.
|
||||||
//require.Equal(t, "", newNotary().Hash.StringLE()())
|
//require.Equal(t, "", newNotary().Hash.StringLE()())
|
||||||
require.Equal(t, "b1c37d5847c2ae36bdde31d0cc833a7ad9667f8f", newOracle().Hash.StringLE())
|
|
||||||
require.Equal(t, "dde31084c0fdbebc7f5ed5f53a38905305ccee14", newPolicy().Hash.StringLE())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ type oraclesData struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
designateContractID = -4
|
designateContractID = -5
|
||||||
|
|
||||||
// maxNodeCount is the maximum number of nodes to set the role for.
|
// maxNodeCount is the maximum number of nodes to set the role for.
|
||||||
maxNodeCount = 32
|
maxNodeCount = 32
|
||||||
|
@ -72,8 +72,7 @@ func (s *Designate) isValidRole(r Role) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDesignate(p2pSigExtensionsEnabled bool) *Designate {
|
func newDesignate(p2pSigExtensionsEnabled bool) *Designate {
|
||||||
s := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation)}
|
s := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation, designateContractID)}
|
||||||
s.ContractID = designateContractID
|
|
||||||
s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled
|
s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled
|
||||||
|
|
||||||
desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType,
|
desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType,
|
||||||
|
|
|
@ -12,16 +12,16 @@ import (
|
||||||
|
|
||||||
// Call calls specified native contract method.
|
// Call calls specified native contract method.
|
||||||
func Call(ic *interop.Context) error {
|
func Call(ic *interop.Context) error {
|
||||||
name := ic.VM.Estack().Pop().String()
|
id := int32(ic.VM.Estack().Pop().BigInt().Int64())
|
||||||
var c interop.Contract
|
var c interop.Contract
|
||||||
for _, ctr := range ic.Natives {
|
for _, ctr := range ic.Natives {
|
||||||
if ctr.Metadata().Name == name {
|
if ctr.Metadata().ContractID == id {
|
||||||
c = ctr
|
c = ctr
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return fmt.Errorf("native contract %s not found", name)
|
return fmt.Errorf("native contract %d not found", id)
|
||||||
}
|
}
|
||||||
h := ic.VM.GetCurrentScriptHash()
|
h := ic.VM.GetCurrentScriptHash()
|
||||||
if !h.Equals(c.Metadata().Hash) {
|
if !h.Equals(c.Metadata().Hash) {
|
||||||
|
@ -33,7 +33,7 @@ func Call(ic *interop.Context) error {
|
||||||
return fmt.Errorf("method %s not found", operation)
|
return fmt.Errorf("method %s not found", operation)
|
||||||
}
|
}
|
||||||
if !ic.VM.Context().GetCallFlags().Has(m.RequiredFlags) {
|
if !ic.VM.Context().GetCallFlags().Has(m.RequiredFlags) {
|
||||||
return fmt.Errorf("missing call flags for native %s `%s` operation call: %05b vs %05b", name, operation, ic.VM.Context().GetCallFlags(), m.RequiredFlags)
|
return fmt.Errorf("missing call flags for native %d `%s` operation call: %05b vs %05b", id, operation, ic.VM.Context().GetCallFlags(), m.RequiredFlags)
|
||||||
}
|
}
|
||||||
// Native contract prices are not multiplied by `BaseExecFee`.
|
// Native contract prices are not multiplied by `BaseExecFee`.
|
||||||
if !ic.VM.AddGas(m.Price) {
|
if !ic.VM.AddGas(m.Price) {
|
||||||
|
|
|
@ -36,6 +36,8 @@ type Management struct {
|
||||||
const StoragePrice = 100000
|
const StoragePrice = 100000
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
managementContractID = -1
|
||||||
|
|
||||||
prefixContract = 8
|
prefixContract = 8
|
||||||
|
|
||||||
defaultMinimumDeploymentFee = 10_00000000
|
defaultMinimumDeploymentFee = 10_00000000
|
||||||
|
@ -59,7 +61,7 @@ func makeContractKey(h util.Uint160) []byte {
|
||||||
// newManagement creates new Management native contract.
|
// newManagement creates new Management native contract.
|
||||||
func newManagement() *Management {
|
func newManagement() *Management {
|
||||||
var m = &Management{
|
var m = &Management{
|
||||||
ContractMD: *interop.NewContractMD(nativenames.Management),
|
ContractMD: *interop.NewContractMD(nativenames.Management, managementContractID),
|
||||||
contracts: make(map[util.Uint160]*state.Contract),
|
contracts: make(map[util.Uint160]*state.Contract),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ type GAS struct {
|
||||||
NEO *NEO
|
NEO *NEO
|
||||||
}
|
}
|
||||||
|
|
||||||
const gasContractID = -2
|
const gasContractID = -3
|
||||||
|
|
||||||
// GASFactor is a divisor for finding GAS integral value.
|
// GASFactor is a divisor for finding GAS integral value.
|
||||||
const GASFactor = NEOTotalSupply
|
const GASFactor = NEOTotalSupply
|
||||||
|
@ -27,12 +27,11 @@ const initialGAS = 30000000
|
||||||
// newGAS returns GAS native contract.
|
// newGAS returns GAS native contract.
|
||||||
func newGAS() *GAS {
|
func newGAS() *GAS {
|
||||||
g := &GAS{}
|
g := &GAS{}
|
||||||
nep17 := newNEP17Native(nativenames.Gas)
|
nep17 := newNEP17Native(nativenames.Gas, gasContractID)
|
||||||
nep17.symbol = "GAS"
|
nep17.symbol = "GAS"
|
||||||
nep17.decimals = 8
|
nep17.decimals = 8
|
||||||
nep17.factor = GASFactor
|
nep17.factor = GASFactor
|
||||||
nep17.incBalance = g.increaseBalance
|
nep17.incBalance = g.increaseBalance
|
||||||
nep17.ContractID = gasContractID
|
|
||||||
|
|
||||||
g.nep17TokenNative = *nep17
|
g.nep17TokenNative = *nep17
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ type NEO struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
neoContractID = -1
|
neoContractID = -2
|
||||||
// NEOTotalSupply is the total amount of NEO in the system.
|
// NEOTotalSupply is the total amount of NEO in the system.
|
||||||
NEOTotalSupply = 100000000
|
NEOTotalSupply = 100000000
|
||||||
// prefixCandidate is a prefix used to store validator's data.
|
// prefixCandidate is a prefix used to store validator's data.
|
||||||
|
@ -94,12 +94,11 @@ func makeValidatorKey(key *keys.PublicKey) []byte {
|
||||||
// newNEO returns NEO native contract.
|
// newNEO returns NEO native contract.
|
||||||
func newNEO() *NEO {
|
func newNEO() *NEO {
|
||||||
n := &NEO{}
|
n := &NEO{}
|
||||||
nep17 := newNEP17Native(nativenames.Neo)
|
nep17 := newNEP17Native(nativenames.Neo, neoContractID)
|
||||||
nep17.symbol = "NEO"
|
nep17.symbol = "NEO"
|
||||||
nep17.decimals = 0
|
nep17.decimals = 0
|
||||||
nep17.factor = 1
|
nep17.factor = 1
|
||||||
nep17.incBalance = n.increaseBalance
|
nep17.incBalance = n.increaseBalance
|
||||||
nep17.ContractID = neoContractID
|
|
||||||
|
|
||||||
n.nep17TokenNative = *nep17
|
n.nep17TokenNative = *nep17
|
||||||
n.votesChanged.Store(true)
|
n.votesChanged.Store(true)
|
||||||
|
|
|
@ -42,8 +42,8 @@ func (c *nep17TokenNative) Metadata() *interop.ContractMD {
|
||||||
return &c.ContractMD
|
return &c.ContractMD
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNEP17Native(name string) *nep17TokenNative {
|
func newNEP17Native(name string, id int32) *nep17TokenNative {
|
||||||
n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name)}
|
n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name, id)}
|
||||||
n.Manifest.SupportedStandards = []string{manifest.NEP17StandardName}
|
n.Manifest.SupportedStandards = []string{manifest.NEP17StandardName}
|
||||||
|
|
||||||
desc := newDescriptor("symbol", smartcontract.StringType)
|
desc := newDescriptor("symbol", smartcontract.StringType)
|
||||||
|
|
|
@ -52,8 +52,7 @@ var maxNotValidBeforeDeltaKey = []byte{10}
|
||||||
|
|
||||||
// newNotary returns Notary native contract.
|
// newNotary returns Notary native contract.
|
||||||
func newNotary() *Notary {
|
func newNotary() *Notary {
|
||||||
n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary)}
|
n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)}
|
||||||
n.ContractID = notaryContractID
|
|
||||||
|
|
||||||
desc := newDescriptor("onPayment", smartcontract.VoidType,
|
desc := newDescriptor("onPayment", smartcontract.VoidType,
|
||||||
manifest.NewParameter("from", smartcontract.Hash160Type),
|
manifest.NewParameter("from", smartcontract.Hash160Type),
|
||||||
|
|
|
@ -38,7 +38,7 @@ type Oracle struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
oracleContractID = -5
|
oracleContractID = -6
|
||||||
maxURLLength = 256
|
maxURLLength = 256
|
||||||
maxFilterLength = 128
|
maxFilterLength = 128
|
||||||
maxCallbackLength = 32
|
maxCallbackLength = 32
|
||||||
|
@ -52,7 +52,7 @@ const (
|
||||||
var oracleScript []byte
|
var oracleScript []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
_, h := state.CreateNativeContractHash(nativenames.Oracle)
|
_, h := state.CreateNativeContractHash(oracleContractID)
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.Int(w.BinWriter, 0)
|
emit.Int(w.BinWriter, 0)
|
||||||
emit.Opcodes(w.BinWriter, opcode.NEWARRAY)
|
emit.Opcodes(w.BinWriter, opcode.NEWARRAY)
|
||||||
|
@ -86,8 +86,7 @@ func GetOracleResponseScript() []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOracle() *Oracle {
|
func newOracle() *Oracle {
|
||||||
o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle)}
|
o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID)}
|
||||||
o.ContractID = oracleContractID
|
|
||||||
|
|
||||||
desc := newDescriptor("request", smartcontract.VoidType,
|
desc := newDescriptor("request", smartcontract.VoidType,
|
||||||
manifest.NewParameter("url", smartcontract.StringType),
|
manifest.NewParameter("url", smartcontract.StringType),
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
policyContractID = -3
|
policyContractID = -4
|
||||||
|
|
||||||
defaultMaxBlockSize = 1024 * 256
|
defaultMaxBlockSize = 1024 * 256
|
||||||
defaultMaxTransactionsPerBlock = 512
|
defaultMaxTransactionsPerBlock = 512
|
||||||
|
@ -81,9 +81,7 @@ var _ interop.Contract = (*Policy)(nil)
|
||||||
|
|
||||||
// newPolicy returns Policy native contract.
|
// newPolicy returns Policy native contract.
|
||||||
func newPolicy() *Policy {
|
func newPolicy() *Policy {
|
||||||
p := &Policy{ContractMD: *interop.NewContractMD(nativenames.Policy)}
|
p := &Policy{ContractMD: *interop.NewContractMD(nativenames.Policy, policyContractID)}
|
||||||
|
|
||||||
p.ContractID = policyContractID
|
|
||||||
|
|
||||||
desc := newDescriptor("getMaxTransactionsPerBlock", smartcontract.IntegerType)
|
desc := newDescriptor("getMaxTransactionsPerBlock", smartcontract.IntegerType)
|
||||||
md := newMethodAndPrice(p.getMaxTransactionsPerBlock, 1000000, callflag.ReadStates)
|
md := newMethodAndPrice(p.getMaxTransactionsPerBlock, 1000000, callflag.ReadStates)
|
||||||
|
|
|
@ -61,7 +61,7 @@ const testSumPrice = 1 << 15 * interop.DefaultBaseExecFee // same as contract.Ca
|
||||||
|
|
||||||
func newTestNative() *testNative {
|
func newTestNative() *testNative {
|
||||||
tn := &testNative{
|
tn := &testNative{
|
||||||
meta: *interop.NewContractMD("Test.Native.Sum"),
|
meta: *interop.NewContractMD("Test.Native.Sum", 0),
|
||||||
blocks: make(chan uint32, 1),
|
blocks: make(chan uint32, 1),
|
||||||
}
|
}
|
||||||
desc := &manifest.Method{
|
desc := &manifest.Method{
|
||||||
|
@ -181,10 +181,9 @@ func TestNativeContract_Invoke(t *testing.T) {
|
||||||
|
|
||||||
// System.Contract.Call + "sum" itself + opcodes for pushing arguments.
|
// System.Contract.Call + "sum" itself + opcodes for pushing arguments.
|
||||||
price := int64(testSumPrice * 2)
|
price := int64(testSumPrice * 2)
|
||||||
price += 3 * fee.Opcode(chain.GetBaseExecFee(), opcode.PUSHINT8, opcode.PUSHDATA1)
|
price += 3 * fee.Opcode(chain.GetBaseExecFee(), opcode.PUSHINT8)
|
||||||
price += 2 * fee.Opcode(chain.GetBaseExecFee(), opcode.SYSCALL)
|
price += 2 * fee.Opcode(chain.GetBaseExecFee(), opcode.SYSCALL, opcode.PUSHDATA1, opcode.PUSHINT8)
|
||||||
price += fee.Opcode(chain.GetBaseExecFee(), opcode.PACK)
|
price += fee.Opcode(chain.GetBaseExecFee(), opcode.PACK)
|
||||||
price += fee.Opcode(chain.GetBaseExecFee(), opcode.PUSHINT8)
|
|
||||||
res, err := invokeContractMethod(chain, price, tn.Metadata().Hash, "sum", int64(14), int64(28))
|
res, err := invokeContractMethod(chain, price, tn.Metadata().Hash, "sum", int64(14), int64(28))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.Make(42))
|
checkResult(t, res, stackitem.Make(42))
|
||||||
|
@ -237,7 +236,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
|
||||||
v.Estack().PushVal(14)
|
v.Estack().PushVal(14)
|
||||||
v.Estack().PushVal(28)
|
v.Estack().PushVal(28)
|
||||||
v.Estack().PushVal("sum")
|
v.Estack().PushVal("sum")
|
||||||
v.Estack().PushVal(tn.Metadata().Name)
|
v.Estack().PushVal(tn.Metadata().ContractID)
|
||||||
|
|
||||||
require.NoError(t, native.Call(ic))
|
require.NoError(t, native.Call(ic))
|
||||||
|
|
||||||
|
|
|
@ -124,9 +124,9 @@ func CreateContractHash(sender util.Uint160, script []byte) util.Uint160 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateNativeContractHash returns script and hash for the native contract.
|
// CreateNativeContractHash returns script and hash for the native contract.
|
||||||
func CreateNativeContractHash(name string) ([]byte, util.Uint160) {
|
func CreateNativeContractHash(id int32) ([]byte, util.Uint160) {
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.String(w.BinWriter, name)
|
emit.Int(w.BinWriter, int64(id))
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemContractCallNative)
|
emit.Syscall(w.BinWriter, interopnames.SystemContractCallNative)
|
||||||
if w.Err != nil {
|
if w.Err != nil {
|
||||||
panic(w.Err)
|
panic(w.Err)
|
||||||
|
|
|
@ -66,10 +66,10 @@ func TestCreateContractHash(t *testing.T) {
|
||||||
var sender util.Uint160
|
var sender util.Uint160
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
require.Equal(t, "b4b7417195feca1cdb5a99504ab641d8c220ae99", CreateContractHash(sender, script).StringLE())
|
require.Equal(t, "b8e95ff7b11c427c29355e3398722d97bd2ca069", CreateContractHash(sender, script).StringLE())
|
||||||
sender, err = util.Uint160DecodeStringLE("a400ff00ff00ff00ff00ff00ff00ff00ff00ff01")
|
sender, err = util.Uint160DecodeStringLE("a400ff00ff00ff00ff00ff00ff00ff00ff00ff01")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "e56e4ee87f89a70e9138432c387ad49f2ee5b55f", CreateContractHash(sender, script).StringLE())
|
require.Equal(t, "435c467b8e15cb9b1474ad7ee817ffdcfededef9", CreateContractHash(sender, script).StringLE())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContractFromStackItem(t *testing.T) {
|
func TestContractFromStackItem(t *testing.T) {
|
||||||
|
|
|
@ -56,11 +56,11 @@ type rpcTestCase struct {
|
||||||
check func(t *testing.T, e *executor, result interface{})
|
check func(t *testing.T, e *executor, result interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
const testContractHash = "743ed26f78e29ecd595535b74a943b1f9ccbc444"
|
const testContractHash = "0b3bc97e94ed99e32dda46c9ecd2d3626979af06"
|
||||||
const deploymentTxHash = "2aef5684c6cf60884cc00400b78c65c5105ed4261a4f614bce74c74927a66bf3"
|
const deploymentTxHash = "4288bb6ad12426a9e34f6af4c050bc291798a46958443d614f457a9a12f087c2"
|
||||||
const genesisBlockHash = "0542f4350c6e236d0509bcd98188b0034bfbecc1a0c7fcdb8e4295310d468b70"
|
const genesisBlockHash = "0542f4350c6e236d0509bcd98188b0034bfbecc1a0c7fcdb8e4295310d468b70"
|
||||||
|
|
||||||
const verifyContractHash = "a2eb22340979804cb10cc1add0b8822c201f4d8a"
|
const verifyContractHash = "d2da8ee8c0bf6c5bf3dda1ef671dbf5fef7226e9"
|
||||||
const verifyContractAVM = "570300412d51083021700c14aa8acf859d4fe402b34e673f2156821796a488ebdb30716813cedb2869db289740"
|
const verifyContractAVM = "570300412d51083021700c14aa8acf859d4fe402b34e673f2156821796a488ebdb30716813cedb2869db289740"
|
||||||
const testVerifyContractAVM = "VwcADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGgRVUH4J+yMIaonBwAAABFADBQNDwMCCQACAQMHAwQFAgEADgYMCdswcWkRVUH4J+yMIaonBwAAABJAE0A="
|
const testVerifyContractAVM = "VwcADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGgRVUH4J+yMIaonBwAAABFADBQNDwMCCQACAQMHAwQFAgEADgYMCdswcWkRVUH4J+yMIaonBwAAABJAE0A="
|
||||||
|
|
||||||
|
@ -155,12 +155,12 @@ var rpcTestCases = map[string][]rpcTestCase{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "positive, by id",
|
name: "positive, by id",
|
||||||
params: `[0]`,
|
params: `[1]`,
|
||||||
result: func(e *executor) interface{} { return &state.Contract{} },
|
result: func(e *executor) interface{} { return &state.Contract{} },
|
||||||
check: func(t *testing.T, e *executor, cs interface{}) {
|
check: func(t *testing.T, e *executor, cs interface{}) {
|
||||||
res, ok := cs.(*state.Contract)
|
res, ok := cs.(*state.Contract)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, int32(0), res.ID)
|
assert.Equal(t, int32(1), res.ID)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -180,7 +180,7 @@ var rpcTestCases = map[string][]rpcTestCase{
|
||||||
check: func(t *testing.T, e *executor, cs interface{}) {
|
check: func(t *testing.T, e *executor, cs interface{}) {
|
||||||
res, ok := cs.(*state.Contract)
|
res, ok := cs.(*state.Contract)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, int32(-3), res.ID)
|
assert.Equal(t, int32(-4), res.ID)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
Binary file not shown.
|
@ -66,10 +66,11 @@ const (
|
||||||
CALL Opcode = 0x34
|
CALL Opcode = 0x34
|
||||||
CALLL Opcode = 0x35 // CALL_L
|
CALLL Opcode = 0x35 // CALL_L
|
||||||
CALLA Opcode = 0x36
|
CALLA Opcode = 0x36
|
||||||
|
CALLT Opcode = 0x37
|
||||||
|
|
||||||
// Exceptions
|
// Exceptions
|
||||||
ABORT Opcode = 0x37
|
ABORT Opcode = 0x38
|
||||||
ASSERT Opcode = 0x38
|
ASSERT Opcode = 0x39
|
||||||
THROW Opcode = 0x3A
|
THROW Opcode = 0x3A
|
||||||
TRY Opcode = 0x3B
|
TRY Opcode = 0x3B
|
||||||
TRYL Opcode = 0x3C // TRY_L
|
TRYL Opcode = 0x3C // TRY_L
|
||||||
|
|
|
@ -61,8 +61,9 @@ func _() {
|
||||||
_ = x[CALL-52]
|
_ = x[CALL-52]
|
||||||
_ = x[CALLL-53]
|
_ = x[CALLL-53]
|
||||||
_ = x[CALLA-54]
|
_ = x[CALLA-54]
|
||||||
_ = x[ABORT-55]
|
_ = x[CALLT-55]
|
||||||
_ = x[ASSERT-56]
|
_ = x[ABORT-56]
|
||||||
|
_ = x[ASSERT-57]
|
||||||
_ = x[THROW-58]
|
_ = x[THROW-58]
|
||||||
_ = x[TRY-59]
|
_ = x[TRY-59]
|
||||||
_ = x[TRYL-60]
|
_ = x[TRYL-60]
|
||||||
|
@ -196,7 +197,7 @@ func _() {
|
||||||
_ = x[CONVERT-219]
|
_ = x[CONVERT-219]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _Opcode_name = "PUSHINT8PUSHINT16PUSHINT32PUSHINT64PUSHINT128PUSHINT256PUSHAPUSHNULLPUSHDATA1PUSHDATA2PUSHDATA4PUSHM1PUSH0PUSH1PUSH2PUSH3PUSH4PUSH5PUSH6PUSH7PUSH8PUSH9PUSH10PUSH11PUSH12PUSH13PUSH14PUSH15PUSH16NOPJMPJMP_LJMPIFJMPIF_LJMPIFNOTJMPIFNOT_LJMPEQJMPEQ_LJMPNEJMPNE_LJMPGTJMPGT_LJMPGEJMPGE_LJMPLTJMPLT_LJMPLEJMPLE_LCALLCALL_LCALLAABORTASSERTTHROWTRYTRY_LENDTRYENDTRY_LENDFINALLYRETSYSCALLDEPTHDROPNIPXDROPCLEARDUPOVERPICKTUCKSWAPROTROLLREVERSE3REVERSE4REVERSENINITSSLOTINITSLOTLDSFLD0LDSFLD1LDSFLD2LDSFLD3LDSFLD4LDSFLD5LDSFLD6LDSFLDSTSFLD0STSFLD1STSFLD2STSFLD3STSFLD4STSFLD5STSFLD6STSFLDLDLOC0LDLOC1LDLOC2LDLOC3LDLOC4LDLOC5LDLOC6LDLOCSTLOC0STLOC1STLOC2STLOC3STLOC4STLOC5STLOC6STLOCLDARG0LDARG1LDARG2LDARG3LDARG4LDARG5LDARG6LDARGSTARG0STARG1STARG2STARG3STARG4STARG5STARG6STARGNEWBUFFERMEMCPYCATSUBSTRLEFTRIGHTINVERTANDORXOREQUALNOTEQUALSIGNABSNEGATEINCDECADDSUBMULDIVMODSHLSHRNOTBOOLANDBOOLORNZNUMEQUALNUMNOTEQUALLTLTEGTGTEMINMAXWITHINPACKUNPACKNEWARRAY0NEWARRAYNEWARRAY_TNEWSTRUCT0NEWSTRUCTNEWMAPSIZEHASKEYKEYSVALUESPICKITEMAPPENDSETITEMREVERSEITEMSREMOVECLEARITEMSISNULLISTYPECONVERT"
|
const _Opcode_name = "PUSHINT8PUSHINT16PUSHINT32PUSHINT64PUSHINT128PUSHINT256PUSHAPUSHNULLPUSHDATA1PUSHDATA2PUSHDATA4PUSHM1PUSH0PUSH1PUSH2PUSH3PUSH4PUSH5PUSH6PUSH7PUSH8PUSH9PUSH10PUSH11PUSH12PUSH13PUSH14PUSH15PUSH16NOPJMPJMP_LJMPIFJMPIF_LJMPIFNOTJMPIFNOT_LJMPEQJMPEQ_LJMPNEJMPNE_LJMPGTJMPGT_LJMPGEJMPGE_LJMPLTJMPLT_LJMPLEJMPLE_LCALLCALL_LCALLACALLTABORTASSERTTHROWTRYTRY_LENDTRYENDTRY_LENDFINALLYRETSYSCALLDEPTHDROPNIPXDROPCLEARDUPOVERPICKTUCKSWAPROTROLLREVERSE3REVERSE4REVERSENINITSSLOTINITSLOTLDSFLD0LDSFLD1LDSFLD2LDSFLD3LDSFLD4LDSFLD5LDSFLD6LDSFLDSTSFLD0STSFLD1STSFLD2STSFLD3STSFLD4STSFLD5STSFLD6STSFLDLDLOC0LDLOC1LDLOC2LDLOC3LDLOC4LDLOC5LDLOC6LDLOCSTLOC0STLOC1STLOC2STLOC3STLOC4STLOC5STLOC6STLOCLDARG0LDARG1LDARG2LDARG3LDARG4LDARG5LDARG6LDARGSTARG0STARG1STARG2STARG3STARG4STARG5STARG6STARGNEWBUFFERMEMCPYCATSUBSTRLEFTRIGHTINVERTANDORXOREQUALNOTEQUALSIGNABSNEGATEINCDECADDSUBMULDIVMODSHLSHRNOTBOOLANDBOOLORNZNUMEQUALNUMNOTEQUALLTLTEGTGTEMINMAXWITHINPACKUNPACKNEWARRAY0NEWARRAYNEWARRAY_TNEWSTRUCT0NEWSTRUCTNEWMAPSIZEHASKEYKEYSVALUESPICKITEMAPPENDSETITEMREVERSEITEMSREMOVECLEARITEMSISNULLISTYPECONVERT"
|
||||||
|
|
||||||
var _Opcode_map = map[Opcode]string{
|
var _Opcode_map = map[Opcode]string{
|
||||||
0: _Opcode_name[0:8],
|
0: _Opcode_name[0:8],
|
||||||
|
@ -251,138 +252,139 @@ var _Opcode_map = map[Opcode]string{
|
||||||
53: _Opcode_name[310:316],
|
53: _Opcode_name[310:316],
|
||||||
54: _Opcode_name[316:321],
|
54: _Opcode_name[316:321],
|
||||||
55: _Opcode_name[321:326],
|
55: _Opcode_name[321:326],
|
||||||
56: _Opcode_name[326:332],
|
56: _Opcode_name[326:331],
|
||||||
58: _Opcode_name[332:337],
|
57: _Opcode_name[331:337],
|
||||||
59: _Opcode_name[337:340],
|
58: _Opcode_name[337:342],
|
||||||
60: _Opcode_name[340:345],
|
59: _Opcode_name[342:345],
|
||||||
61: _Opcode_name[345:351],
|
60: _Opcode_name[345:350],
|
||||||
62: _Opcode_name[351:359],
|
61: _Opcode_name[350:356],
|
||||||
63: _Opcode_name[359:369],
|
62: _Opcode_name[356:364],
|
||||||
64: _Opcode_name[369:372],
|
63: _Opcode_name[364:374],
|
||||||
65: _Opcode_name[372:379],
|
64: _Opcode_name[374:377],
|
||||||
67: _Opcode_name[379:384],
|
65: _Opcode_name[377:384],
|
||||||
69: _Opcode_name[384:388],
|
67: _Opcode_name[384:389],
|
||||||
70: _Opcode_name[388:391],
|
69: _Opcode_name[389:393],
|
||||||
72: _Opcode_name[391:396],
|
70: _Opcode_name[393:396],
|
||||||
73: _Opcode_name[396:401],
|
72: _Opcode_name[396:401],
|
||||||
74: _Opcode_name[401:404],
|
73: _Opcode_name[401:406],
|
||||||
75: _Opcode_name[404:408],
|
74: _Opcode_name[406:409],
|
||||||
77: _Opcode_name[408:412],
|
75: _Opcode_name[409:413],
|
||||||
78: _Opcode_name[412:416],
|
77: _Opcode_name[413:417],
|
||||||
80: _Opcode_name[416:420],
|
78: _Opcode_name[417:421],
|
||||||
81: _Opcode_name[420:423],
|
80: _Opcode_name[421:425],
|
||||||
82: _Opcode_name[423:427],
|
81: _Opcode_name[425:428],
|
||||||
83: _Opcode_name[427:435],
|
82: _Opcode_name[428:432],
|
||||||
84: _Opcode_name[435:443],
|
83: _Opcode_name[432:440],
|
||||||
85: _Opcode_name[443:451],
|
84: _Opcode_name[440:448],
|
||||||
86: _Opcode_name[451:460],
|
85: _Opcode_name[448:456],
|
||||||
87: _Opcode_name[460:468],
|
86: _Opcode_name[456:465],
|
||||||
88: _Opcode_name[468:475],
|
87: _Opcode_name[465:473],
|
||||||
89: _Opcode_name[475:482],
|
88: _Opcode_name[473:480],
|
||||||
90: _Opcode_name[482:489],
|
89: _Opcode_name[480:487],
|
||||||
91: _Opcode_name[489:496],
|
90: _Opcode_name[487:494],
|
||||||
92: _Opcode_name[496:503],
|
91: _Opcode_name[494:501],
|
||||||
93: _Opcode_name[503:510],
|
92: _Opcode_name[501:508],
|
||||||
94: _Opcode_name[510:517],
|
93: _Opcode_name[508:515],
|
||||||
95: _Opcode_name[517:523],
|
94: _Opcode_name[515:522],
|
||||||
96: _Opcode_name[523:530],
|
95: _Opcode_name[522:528],
|
||||||
97: _Opcode_name[530:537],
|
96: _Opcode_name[528:535],
|
||||||
98: _Opcode_name[537:544],
|
97: _Opcode_name[535:542],
|
||||||
99: _Opcode_name[544:551],
|
98: _Opcode_name[542:549],
|
||||||
100: _Opcode_name[551:558],
|
99: _Opcode_name[549:556],
|
||||||
101: _Opcode_name[558:565],
|
100: _Opcode_name[556:563],
|
||||||
102: _Opcode_name[565:572],
|
101: _Opcode_name[563:570],
|
||||||
103: _Opcode_name[572:578],
|
102: _Opcode_name[570:577],
|
||||||
104: _Opcode_name[578:584],
|
103: _Opcode_name[577:583],
|
||||||
105: _Opcode_name[584:590],
|
104: _Opcode_name[583:589],
|
||||||
106: _Opcode_name[590:596],
|
105: _Opcode_name[589:595],
|
||||||
107: _Opcode_name[596:602],
|
106: _Opcode_name[595:601],
|
||||||
108: _Opcode_name[602:608],
|
107: _Opcode_name[601:607],
|
||||||
109: _Opcode_name[608:614],
|
108: _Opcode_name[607:613],
|
||||||
110: _Opcode_name[614:620],
|
109: _Opcode_name[613:619],
|
||||||
111: _Opcode_name[620:625],
|
110: _Opcode_name[619:625],
|
||||||
112: _Opcode_name[625:631],
|
111: _Opcode_name[625:630],
|
||||||
113: _Opcode_name[631:637],
|
112: _Opcode_name[630:636],
|
||||||
114: _Opcode_name[637:643],
|
113: _Opcode_name[636:642],
|
||||||
115: _Opcode_name[643:649],
|
114: _Opcode_name[642:648],
|
||||||
116: _Opcode_name[649:655],
|
115: _Opcode_name[648:654],
|
||||||
117: _Opcode_name[655:661],
|
116: _Opcode_name[654:660],
|
||||||
118: _Opcode_name[661:667],
|
117: _Opcode_name[660:666],
|
||||||
119: _Opcode_name[667:672],
|
118: _Opcode_name[666:672],
|
||||||
120: _Opcode_name[672:678],
|
119: _Opcode_name[672:677],
|
||||||
121: _Opcode_name[678:684],
|
120: _Opcode_name[677:683],
|
||||||
122: _Opcode_name[684:690],
|
121: _Opcode_name[683:689],
|
||||||
123: _Opcode_name[690:696],
|
122: _Opcode_name[689:695],
|
||||||
124: _Opcode_name[696:702],
|
123: _Opcode_name[695:701],
|
||||||
125: _Opcode_name[702:708],
|
124: _Opcode_name[701:707],
|
||||||
126: _Opcode_name[708:714],
|
125: _Opcode_name[707:713],
|
||||||
127: _Opcode_name[714:719],
|
126: _Opcode_name[713:719],
|
||||||
128: _Opcode_name[719:725],
|
127: _Opcode_name[719:724],
|
||||||
129: _Opcode_name[725:731],
|
128: _Opcode_name[724:730],
|
||||||
130: _Opcode_name[731:737],
|
129: _Opcode_name[730:736],
|
||||||
131: _Opcode_name[737:743],
|
130: _Opcode_name[736:742],
|
||||||
132: _Opcode_name[743:749],
|
131: _Opcode_name[742:748],
|
||||||
133: _Opcode_name[749:755],
|
132: _Opcode_name[748:754],
|
||||||
134: _Opcode_name[755:761],
|
133: _Opcode_name[754:760],
|
||||||
135: _Opcode_name[761:766],
|
134: _Opcode_name[760:766],
|
||||||
136: _Opcode_name[766:775],
|
135: _Opcode_name[766:771],
|
||||||
137: _Opcode_name[775:781],
|
136: _Opcode_name[771:780],
|
||||||
139: _Opcode_name[781:784],
|
137: _Opcode_name[780:786],
|
||||||
140: _Opcode_name[784:790],
|
139: _Opcode_name[786:789],
|
||||||
141: _Opcode_name[790:794],
|
140: _Opcode_name[789:795],
|
||||||
142: _Opcode_name[794:799],
|
141: _Opcode_name[795:799],
|
||||||
144: _Opcode_name[799:805],
|
142: _Opcode_name[799:804],
|
||||||
145: _Opcode_name[805:808],
|
144: _Opcode_name[804:810],
|
||||||
146: _Opcode_name[808:810],
|
145: _Opcode_name[810:813],
|
||||||
147: _Opcode_name[810:813],
|
146: _Opcode_name[813:815],
|
||||||
151: _Opcode_name[813:818],
|
147: _Opcode_name[815:818],
|
||||||
152: _Opcode_name[818:826],
|
151: _Opcode_name[818:823],
|
||||||
153: _Opcode_name[826:830],
|
152: _Opcode_name[823:831],
|
||||||
154: _Opcode_name[830:833],
|
153: _Opcode_name[831:835],
|
||||||
155: _Opcode_name[833:839],
|
154: _Opcode_name[835:838],
|
||||||
156: _Opcode_name[839:842],
|
155: _Opcode_name[838:844],
|
||||||
157: _Opcode_name[842:845],
|
156: _Opcode_name[844:847],
|
||||||
158: _Opcode_name[845:848],
|
157: _Opcode_name[847:850],
|
||||||
159: _Opcode_name[848:851],
|
158: _Opcode_name[850:853],
|
||||||
160: _Opcode_name[851:854],
|
159: _Opcode_name[853:856],
|
||||||
161: _Opcode_name[854:857],
|
160: _Opcode_name[856:859],
|
||||||
162: _Opcode_name[857:860],
|
161: _Opcode_name[859:862],
|
||||||
168: _Opcode_name[860:863],
|
162: _Opcode_name[862:865],
|
||||||
169: _Opcode_name[863:866],
|
168: _Opcode_name[865:868],
|
||||||
170: _Opcode_name[866:869],
|
169: _Opcode_name[868:871],
|
||||||
171: _Opcode_name[869:876],
|
170: _Opcode_name[871:874],
|
||||||
172: _Opcode_name[876:882],
|
171: _Opcode_name[874:881],
|
||||||
177: _Opcode_name[882:884],
|
172: _Opcode_name[881:887],
|
||||||
179: _Opcode_name[884:892],
|
177: _Opcode_name[887:889],
|
||||||
180: _Opcode_name[892:903],
|
179: _Opcode_name[889:897],
|
||||||
181: _Opcode_name[903:905],
|
180: _Opcode_name[897:908],
|
||||||
182: _Opcode_name[905:908],
|
181: _Opcode_name[908:910],
|
||||||
183: _Opcode_name[908:910],
|
182: _Opcode_name[910:913],
|
||||||
184: _Opcode_name[910:913],
|
183: _Opcode_name[913:915],
|
||||||
185: _Opcode_name[913:916],
|
184: _Opcode_name[915:918],
|
||||||
186: _Opcode_name[916:919],
|
185: _Opcode_name[918:921],
|
||||||
187: _Opcode_name[919:925],
|
186: _Opcode_name[921:924],
|
||||||
192: _Opcode_name[925:929],
|
187: _Opcode_name[924:930],
|
||||||
193: _Opcode_name[929:935],
|
192: _Opcode_name[930:934],
|
||||||
194: _Opcode_name[935:944],
|
193: _Opcode_name[934:940],
|
||||||
195: _Opcode_name[944:952],
|
194: _Opcode_name[940:949],
|
||||||
196: _Opcode_name[952:962],
|
195: _Opcode_name[949:957],
|
||||||
197: _Opcode_name[962:972],
|
196: _Opcode_name[957:967],
|
||||||
198: _Opcode_name[972:981],
|
197: _Opcode_name[967:977],
|
||||||
200: _Opcode_name[981:987],
|
198: _Opcode_name[977:986],
|
||||||
202: _Opcode_name[987:991],
|
200: _Opcode_name[986:992],
|
||||||
203: _Opcode_name[991:997],
|
202: _Opcode_name[992:996],
|
||||||
204: _Opcode_name[997:1001],
|
203: _Opcode_name[996:1002],
|
||||||
205: _Opcode_name[1001:1007],
|
204: _Opcode_name[1002:1006],
|
||||||
206: _Opcode_name[1007:1015],
|
205: _Opcode_name[1006:1012],
|
||||||
207: _Opcode_name[1015:1021],
|
206: _Opcode_name[1012:1020],
|
||||||
208: _Opcode_name[1021:1028],
|
207: _Opcode_name[1020:1026],
|
||||||
209: _Opcode_name[1028:1040],
|
208: _Opcode_name[1026:1033],
|
||||||
210: _Opcode_name[1040:1046],
|
209: _Opcode_name[1033:1045],
|
||||||
211: _Opcode_name[1046:1056],
|
210: _Opcode_name[1045:1051],
|
||||||
216: _Opcode_name[1056:1062],
|
211: _Opcode_name[1051:1061],
|
||||||
217: _Opcode_name[1062:1068],
|
216: _Opcode_name[1061:1067],
|
||||||
219: _Opcode_name[1068:1075],
|
217: _Opcode_name[1067:1073],
|
||||||
|
219: _Opcode_name[1073:1080],
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i Opcode) String() string {
|
func (i Opcode) String() string {
|
||||||
|
|
Loading…
Reference in a new issue