rpcclient: add management wrapper for ContractManagement

This commit is contained in:
Roman Khimov 2022-08-16 18:17:07 +03:00
parent f7c5ab4f43
commit 5c8f3a99dc
4 changed files with 464 additions and 15 deletions

View file

@ -17,7 +17,6 @@ import (
"github.com/nspcc-dev/neo-go/cli/paramcontext"
cliwallet "github.com/nspcc-dev/neo-go/cli/wallet"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"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/transaction"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
@ -25,6 +24,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
@ -971,19 +971,6 @@ func contractDeploy(ctx *cli.Context) error {
appCallParams = append(appCallParams, data[0])
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return err
}
mgmtHash, err := c.GetNativeContractHash(nativenames.Management)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to get management contract's hash: %w", err), 1)
}
acc, w, err := getAccFromContext(ctx)
if err != nil {
return cli.NewExitError(fmt.Errorf("can't get sender address: %w", err), 1)
@ -999,7 +986,7 @@ func contractDeploy(ctx *cli.Context) error {
}}
}
sender, extErr := invokeWithArgs(ctx, acc, w, mgmtHash, "deploy", appCallParams, cosigners)
sender, extErr := invokeWithArgs(ctx, acc, w, management.Hash, "deploy", appCallParams, cosigners)
if extErr != nil {
return extErr
}

View file

@ -0,0 +1,191 @@
/*
Package management provides an RPC wrapper for the native ContractManagement contract.
Safe methods are encapsulated in the ContractReader structure while Contract provides
various methods to perform state-changing calls.
*/
package management
import (
"encoding/json"
"fmt"
"math/big"
"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/transaction"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// Invoker is used by ContractReader to call various methods.
type Invoker interface {
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
}
// Actor is used by Contract to create and send transactions.
type Actor interface {
Invoker
MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error)
MakeRun(script []byte) (*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)
SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error)
SendRun(script []byte) (util.Uint256, uint32, error)
}
// ContractReader provides an interface to call read-only ContractManagement
// contract's methods.
type ContractReader struct {
invoker Invoker
}
// Contract represents a ContractManagement contract client that can be used to
// invoke all of its methods except 'update' and 'destroy' because they can be
// called successfully only from the contract itself (that is doing an update
// or self-destruction).
type Contract struct {
ContractReader
actor Actor
}
// Hash stores the hash of the native ContractManagement contract.
var Hash = state.CreateNativeContractHash(nativenames.Management)
// Event is the event emitted on contract deployment/update/destroy.
// Even though these events are different they all have the same field inside.
type Event struct {
Hash util.Uint160
}
const setMinFeeMethod = "setMinimumDeploymentFee"
// NewReader creates an instance of ContractReader that can be used to read
// data from the contract.
func NewReader(invoker Invoker) *ContractReader {
return &ContractReader{invoker}
}
// New creates an instance of Contract to perform actions using
// the given Actor.
func New(actor Actor) *Contract {
return &Contract{*NewReader(actor), actor}
}
// GetContract allows to get contract data from its hash. This method is mostly
// useful for historic invocations since for current contracts there is a direct
// getcontractstate RPC API that has more options and works faster than going
// via contract invocation.
func (c *ContractReader) GetContract(hash util.Uint160) (*state.Contract, error) {
itm, err := unwrap.Item(c.invoker.Call(Hash, "getContract", hash))
if err != nil {
return nil, err
}
res := new(state.Contract)
err = res.FromStackItem(itm)
if err != nil {
return nil, err
}
return res, nil
}
// GetMinimumDeploymentFee returns the minimal amount of GAS needed to deploy a
// contract on the network.
func (c *ContractReader) GetMinimumDeploymentFee() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(Hash, "getMinimumDeploymentFee"))
}
// HasMethod checks if the contract specified has a method with the given name
// and number of parameters.
func (c *ContractReader) HasMethod(hash util.Uint160, method string, pcount int) (bool, error) {
return unwrap.Bool(c.invoker.Call(Hash, "hasMethod", hash, method, pcount))
}
// Deploy creates and sends to the network a transaction that deploys the given
// contract (with the manifest provided), if data is not nil then it also added
// to the invocation and will be used for "_deploy" method invocation done by
// the ContractManagement contract. If successful, this method returns deployed
// contract state that can be retrieved from the stack after execution.
func (c *Contract) Deploy(exe *nef.File, manif *manifest.Manifest, data interface{}) (util.Uint256, uint32, error) {
script, err := mkDeployScript(exe, manif, data)
if err != nil {
return util.Uint256{}, 0, err
}
return c.actor.SendRun(script)
}
// DeployTransaction creates and returns a transaction that deploys the given
// contract (with the manifest provided), if data is not nil then it also added
// to the invocation and will be used for "_deploy" method invocation done by
// the ContractManagement contract. If successful, this method returns deployed
// contract state that can be retrieved from the stack after execution.
func (c *Contract) DeployTransaction(exe *nef.File, manif *manifest.Manifest, data interface{}) (*transaction.Transaction, error) {
script, err := mkDeployScript(exe, manif, data)
if err != nil {
return nil, err
}
return c.actor.MakeRun(script)
}
// DeployUnsigned creates and returns an unsigned transaction that deploys the given
// contract (with the manifest provided), if data is not nil then it also added
// to the invocation and will be used for "_deploy" method invocation done by
// the ContractManagement contract. If successful, this method returns deployed
// contract state that can be retrieved from the stack after execution.
func (c *Contract) DeployUnsigned(exe *nef.File, manif *manifest.Manifest, data interface{}) (*transaction.Transaction, error) {
script, err := mkDeployScript(exe, manif, data)
if err != nil {
return nil, err
}
return c.actor.MakeUnsignedRun(script, nil)
}
func mkDeployScript(exe *nef.File, manif *manifest.Manifest, data interface{}) ([]byte, error) {
exeB, err := exe.Bytes()
if err != nil {
return nil, fmt.Errorf("bad NEF: %w", err)
}
manifB, err := json.Marshal(manif)
if err != nil {
return nil, fmt.Errorf("bad manifest: %w", err)
}
if data != nil {
return smartcontract.CreateCallScript(Hash, "deploy", exeB, manifB, data)
}
return smartcontract.CreateCallScript(Hash, "deploy", exeB, manifB)
}
// SetMinimumDeploymentFee creates and sends a transaction that changes the
// minimum GAS amount required to deploy a contract. This method can be called
// successfully only by the network's committee, so make sure you're using an
// appropriate Actor. This invocation returns nothing and is successful when
// transactions ends up in the HALT state.
func (c *Contract) SetMinimumDeploymentFee(value *big.Int) (util.Uint256, uint32, error) {
return c.actor.SendCall(Hash, setMinFeeMethod, value)
}
// SetMinimumDeploymentFeeTransaction creates a transaction that changes the
// minimum GAS amount required to deploy a contract. This method can be called
// successfully only by the network's committee, so make sure you're using an
// appropriate Actor. This invocation returns nothing and is successful when
// transactions ends up in the HALT state. The transaction returned is signed,
// but not sent to the network.
func (c *Contract) SetMinimumDeploymentFeeTransaction(value *big.Int) (*transaction.Transaction, error) {
return c.actor.MakeCall(Hash, setMinFeeMethod, value)
}
// SetMinimumDeploymentFeeUnsigned creates a transaction that changes the
// minimum GAS amount required to deploy a contract. This method can be called
// successfully only by the network's committee, so make sure you're using an
// appropriate Actor. This invocation returns nothing and is successful when
// transactions ends up in the HALT state. The transaction returned is not
// signed.
func (c *Contract) SetMinimumDeploymentFeeUnsigned(value *big.Int) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(Hash, setMinFeeMethod, nil, value)
}

View file

@ -0,0 +1,206 @@
package management
import (
"errors"
"math/big"
"testing"
"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/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
type testAct struct {
err error
res *result.Invoke
tx *transaction.Transaction
txh util.Uint256
vub uint32
}
func (t *testAct) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) {
return t.res, t.err
}
func (t *testAct) MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) {
return t.tx, t.err
}
func (t *testAct) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) {
return t.tx, t.err
}
func (t *testAct) SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) {
return t.txh, t.vub, t.err
}
func (t *testAct) MakeRun(script []byte) (*transaction.Transaction, error) {
return t.tx, t.err
}
func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) {
return t.tx, t.err
}
func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) {
return t.txh, t.vub, t.err
}
func TestReader(t *testing.T) {
ta := new(testAct)
man := NewReader(ta)
ta.err = errors.New("")
_, err := man.GetContract(util.Uint160{1, 2, 3})
require.Error(t, err)
_, err = man.GetMinimumDeploymentFee()
require.Error(t, err)
_, err = man.HasMethod(util.Uint160{1, 2, 3}, "method", 0)
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(42),
},
}
_, err = man.GetContract(util.Uint160{1, 2, 3})
require.Error(t, err)
fee, err := man.GetMinimumDeploymentFee()
require.NoError(t, err)
require.Equal(t, big.NewInt(42), fee)
hm, err := man.HasMethod(util.Uint160{1, 2, 3}, "method", 0)
require.NoError(t, err)
require.True(t, hm)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(false),
},
}
_, err = man.GetContract(util.Uint160{1, 2, 3})
require.Error(t, err)
hm, err = man.HasMethod(util.Uint160{1, 2, 3}, "method", 0)
require.NoError(t, err)
require.False(t, hm)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = man.GetContract(util.Uint160{1, 2, 3})
require.Error(t, err)
nefFile, _ := nef.NewFile([]byte{1, 2, 3})
nefBytes, _ := nefFile.Bytes()
manif := manifest.DefaultManifest("stack item")
manifItem, _ := manif.ToStackItem()
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{
stackitem.Make(1),
stackitem.Make(0),
stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()),
stackitem.Make(nefBytes),
manifItem,
}),
},
}
cs, err := man.GetContract(util.Uint160{1, 2, 3})
require.NoError(t, err)
require.Equal(t, int32(1), cs.ID)
require.Equal(t, uint16(0), cs.UpdateCounter)
require.Equal(t, util.Uint160{1, 2, 3}, cs.Hash)
}
func TestSetMinimumDeploymentFee(t *testing.T) {
ta := new(testAct)
man := New(ta)
ta.err = errors.New("")
_, _, err := man.SetMinimumDeploymentFee(big.NewInt(42))
require.Error(t, err)
for _, m := range []func(*big.Int) (*transaction.Transaction, error){
man.SetMinimumDeploymentFeeTransaction,
man.SetMinimumDeploymentFeeUnsigned,
} {
_, err = m(big.NewInt(100))
require.Error(t, err)
}
ta.err = nil
ta.txh = util.Uint256{1, 2, 3}
ta.vub = 42
h, vub, err := man.SetMinimumDeploymentFee(big.NewInt(42))
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
ta.tx = transaction.New([]byte{1, 2, 3}, 100500)
for _, m := range []func(*big.Int) (*transaction.Transaction, error){
man.SetMinimumDeploymentFeeTransaction,
man.SetMinimumDeploymentFeeUnsigned,
} {
tx, err := m(big.NewInt(100))
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
}
}
func TestDeploy(t *testing.T) {
ta := new(testAct)
man := New(ta)
nefFile, _ := nef.NewFile([]byte{1, 2, 3})
manif := manifest.DefaultManifest("stack item")
ta.err = errors.New("")
_, _, err := man.Deploy(nefFile, manif, nil)
require.Error(t, err)
for _, m := range []func(exe *nef.File, manif *manifest.Manifest, data interface{}) (*transaction.Transaction, error){
man.DeployTransaction,
man.DeployUnsigned,
} {
_, err = m(nefFile, manif, nil)
require.Error(t, err)
}
ta.err = nil
ta.txh = util.Uint256{1, 2, 3}
ta.vub = 42
h, vub, err := man.Deploy(nefFile, manif, nil)
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
ta.tx = transaction.New([]byte{1, 2, 3}, 100500)
for _, m := range []func(exe *nef.File, manif *manifest.Manifest, data interface{}) (*transaction.Transaction, error){
man.DeployTransaction,
man.DeployUnsigned,
} {
tx, err := m(nefFile, manif, nil)
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = m(nefFile, manif, map[int]int{})
require.Error(t, err)
}
_, _, err = man.Deploy(nefFile, manif, map[int]int{})
require.Error(t, err)
_, _, err = man.Deploy(nefFile, manif, 100500)
require.NoError(t, err)
nefFile.Compiler = "intentionally very long compiler string that will make NEF code explode on encoding"
_, _, err = man.Deploy(nefFile, manif, nil)
require.Error(t, err)
// Unfortunately, manifest _always_ marshals successfully (or panics).
}

View file

@ -32,6 +32,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/policy"
@ -225,6 +226,70 @@ func TestClientPolicyContract(t *testing.T) {
require.True(t, ret)
}
func TestClientManagementContract(t *testing.T) {
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
defer chain.Close()
defer rpcSrv.Shutdown()
c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{})
require.NoError(t, err)
require.NoError(t, c.Init())
manReader := management.NewReader(invoker.New(c, nil))
fee, err := manReader.GetMinimumDeploymentFee()
require.NoError(t, err)
require.Equal(t, big.NewInt(10*1_0000_0000), fee)
cs1, err := manReader.GetContract(gas.Hash)
require.NoError(t, err)
cs2, err := c.GetContractStateByHash(gas.Hash)
require.NoError(t, err)
require.Equal(t, cs2, cs1)
ret, err := manReader.HasMethod(gas.Hash, "transfer", 4)
require.NoError(t, err)
require.True(t, ret)
act, err := actor.New(c, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: &wallet.Account{
Address: testchain.CommitteeAddress(),
Contract: &wallet.Contract{
Script: testchain.CommitteeVerificationScript(),
},
},
}})
require.NoError(t, err)
man := management.New(act)
txfee, err := man.SetMinimumDeploymentFeeUnsigned(big.NewInt(1 * 1_0000_0000))
require.NoError(t, err)
txdepl, err := man.DeployUnsigned(&cs1.NEF, &cs1.Manifest, nil) // Redeploy from a different account.
require.NoError(t, err)
for _, tx := range []*transaction.Transaction{txfee, txdepl} {
tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx)
}
bl := testchain.NewBlock(t, chain, 1, 0, txfee, txdepl)
_, err = c.SubmitBlock(*bl)
require.NoError(t, err)
fee, err = manReader.GetMinimumDeploymentFee()
require.NoError(t, err)
require.Equal(t, big.NewInt(1_0000_0000), fee)
appLog, err := c.GetApplicationLog(txdepl.Hash(), nil)
require.NoError(t, err)
require.Equal(t, vmstate.Halt, appLog.Executions[0].VMState)
require.Equal(t, 1, len(appLog.Executions[0].Events))
}
func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) {
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
defer chain.Close()