Merge pull request #1906 from nspcc-dev/cli/transfer_param

cli: allow to pass 'data' for nep17 transfer command
This commit is contained in:
Roman Khimov 2021-04-19 10:43:42 +03:00 committed by GitHub
commit 8f14c61c34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 24 deletions

View file

@ -158,6 +158,22 @@ func TestNEP17Transfer(t *testing.T) {
b, _ = e.Chain.GetGoverningTokenBalance(sh)
require.Equal(t, big.NewInt(41), b)
})
t.Run("with data", func(t *testing.T) {
e.In.WriteString("one\r")
validTil := e.Chain.BlockHeight() + 100
e.Run(t, []string{
"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", validatorWallet,
"--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()),
"--token", "GAS",
"--amount", "1",
"--from", validatorAddr,
"[", validatorAddr, strconv.Itoa(int(validTil)), "]",
}...)
e.checkTxPersisted(t)
})
}
func TestNEP17MultiTransfer(t *testing.T) {

View file

@ -538,7 +538,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
paramsStart++
if len(args) > paramsStart {
cosignersOffset, params, err = parseParams(args[paramsStart:], true)
cosignersOffset, params, err = ParseParams(args[paramsStart:], true)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -623,12 +623,12 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
return nil
}
// parseParams extracts array of smartcontract.Parameter from the given args and
// ParseParams extracts array of smartcontract.Parameter from the given args and
// returns the number of handled words, the array itself and an error.
// `calledFromMain` denotes whether the method was called from the outside or
// recursively and used to check if cosignersSeparator and closing bracket are
// allowed to be in `args` sequence.
func parseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
res := []smartcontract.Parameter{}
for k := 0; k < len(args); {
s := args[k]
@ -639,7 +639,7 @@ func parseParams(args []string, calledFromMain bool) (int, []smartcontract.Param
}
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
case arrayStartSeparator:
numWordsRead, array, err := parseParams(args[k+1:], false)
numWordsRead, array, err := ParseParams(args[k+1:], false)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse array: %w", err)
}
@ -887,6 +887,26 @@ func contractDeploy(ctx *cli.Context) error {
return nil
}
// GetDataFromContext returns data parameter from context args.
func GetDataFromContext(ctx *cli.Context) (interface{}, *cli.ExitError) {
var data interface{}
args := ctx.Args()
if args.Present() {
_, params, err := ParseParams(args, true)
if err != nil {
return nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)
}
if len(params) != 1 {
return nil, cli.NewExitError("'data' should be represented as a single parameter", 1)
}
data, err = smartcontract.ExpandParameterToEmitable(params[0])
if err != nil {
return nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
}
}
return data, nil
}
// ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig.
func ParseContractConfig(confFile string) (ProjectConfig, error) {
conf := ProjectConfig{}

View file

@ -202,7 +202,7 @@ func TestParseParams_CalledFromItself(t *testing.T) {
for str, expected := range testCases {
input := strings.Split(str, " ")
offset, actual, err := parseParams(input, false)
offset, actual, err := ParseParams(input, false)
require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Value, actual)
@ -218,7 +218,7 @@ func TestParseParams_CalledFromItself(t *testing.T) {
for _, str := range errorCases {
input := strings.Split(str, " ")
_, _, err := parseParams(input, false)
_, _, err := ParseParams(input, false)
require.Error(t, err)
}
}
@ -400,7 +400,7 @@ func TestParseParams_CalledFromOutside(t *testing.T) {
}
for str, expected := range testCases {
input := strings.Split(str, " ")
offset, arr, err := parseParams(input, true)
offset, arr, err := ParseParams(input, true)
require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Parameters, arr)
@ -415,7 +415,7 @@ func TestParseParams_CalledFromOutside(t *testing.T) {
}
for _, str := range errorCases {
input := strings.Split(str, " ")
_, _, err := parseParams(input, true)
_, _, err := ParseParams(input, true)
require.Error(t, err)
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext"
smartcontractcli "github.com/nspcc-dev/neo-go/cli/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
@ -111,9 +112,12 @@ func newNEP17Commands() []cli.Command {
{
Name: "transfer",
Usage: "transfer NEP17 tokens",
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash> --amount string",
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash> --amount string [data]",
Action: transferNEP17,
Flags: transferFlags,
Description: `Transfers specified NEP17 token amount with optional 'data' parameter attached to the transfer.
See 'contract testinvokefunction' documentation for the details about 'data'
parameter. If no 'data' is given then default nil value will be used`,
},
{
Name: "multitransfer",
@ -409,6 +413,7 @@ func multiTransferNEP17(ctx *cli.Context) error {
Token: token.Hash,
Address: addr,
Amount: amount.Int64(),
Data: nil,
})
}
@ -456,17 +461,23 @@ func transferNEP17(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
}
data, extErr := smartcontractcli.GetDataFromContext(ctx)
if extErr != nil {
return extErr
}
return signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{
Token: token.Hash,
Address: to,
Amount: amount.Int64(),
Data: data,
}})
}
func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget) error {
gas := flags.Fixed8FromContext(ctx, "gas")
tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, nil)
tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients)
if err != nil {
return cli.NewExitError(err, 1)
}

View file

@ -8,7 +8,7 @@ ProtocolConfiguration:
ValidatorsCount: 1
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
P2PSigExtensions: true
NativeActivations:
ContractManagement: [0]
StdLib: [0]
@ -20,6 +20,7 @@ ProtocolConfiguration:
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
Notary: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -13,11 +13,12 @@ import (
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
// TransferTarget represents target address and token amount for transfer.
// TransferTarget represents target address, token amount and data for transfer.
type TransferTarget struct {
Token util.Uint160
Address util.Uint160
Amount int64
Data interface{}
}
// SignerAccount represents combination of the transaction.Signer and the
@ -73,28 +74,22 @@ func (c *Client) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160, tok
{Token: token,
Address: to,
Amount: amount,
Data: data,
},
}, []interface{}{data})
})
}
// CreateNEP17MultiTransferTx creates an invocation transaction for performing NEP17 transfers
// from a single sender to multiple recipients with the given data.
func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, recipients []TransferTarget, data []interface{}) (*transaction.Transaction, error) {
func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, recipients []TransferTarget) (*transaction.Transaction, error) {
from, err := address.StringToUint160(acc.Address)
if err != nil {
return nil, fmt.Errorf("bad account address: %w", err)
}
if data == nil {
data = make([]interface{}, len(recipients))
} else {
if len(data) != len(recipients) {
return nil, fmt.Errorf("data and recipients number mismatch: %d vs %d", len(data), len(recipients))
}
}
w := io.NewBufBinWriter()
for i := range recipients {
emit.AppCall(w.BinWriter, recipients[i].Token, "transfer", callflag.All,
from, recipients[i].Address, recipients[i].Amount, data[i])
from, recipients[i].Address, recipients[i].Amount, recipients[i].Data)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
}
if w.Err != nil {
@ -167,12 +162,12 @@ func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util.
}
// MultiTransferNEP17 is similar to TransferNEP17, buf allows to have multiple recipients.
func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget, data []interface{}) (util.Uint256, error) {
func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget) (util.Uint256, error) {
if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized
}
tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, data)
tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients)
if err != nil {
return util.Uint256{}, err
}

View file

@ -14,6 +14,7 @@ import (
"strings"
"unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
)
@ -437,3 +438,28 @@ func NewParameterFromString(in string) (*Parameter, error) {
}
return res, nil
}
// ExpandParameterToEmitable converts parameter to a type which can be handled as
// an array item by emit.Array. It correlates with the way RPC server handles
// FuncParams for invoke* calls inside the request.ExpandArrayIntoScript function.
func ExpandParameterToEmitable(param Parameter) (interface{}, error) {
var err error
switch t := param.Type; t {
case PublicKeyType:
return param.Value.(*keys.PublicKey).Bytes(), nil
case ArrayType:
arr := param.Value.([]Parameter)
res := make([]interface{}, len(arr))
for i := range arr {
res[i], err = ExpandParameterToEmitable(arr[i])
if err != nil {
return nil, err
}
}
return res, nil
case MapType, InteropInterfaceType, UnknownType, AnyType, VoidType:
return nil, fmt.Errorf("unsupported parameter type: %s", t.String())
default:
return param.Value, nil
}
}

View file

@ -9,7 +9,10 @@ import (
"testing"
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -517,3 +520,85 @@ func hexToBase64(s string) string {
b, _ := hex.DecodeString(s)
return base64.StdEncoding.EncodeToString(b)
}
func TestExpandParameterToEmitable(t *testing.T) {
pk, _ := keys.NewPrivateKey()
testCases := []struct {
In Parameter
Expected interface{}
}{
{
In: Parameter{Type: BoolType, Value: true},
Expected: true,
},
{
In: Parameter{Type: IntegerType, Value: int64(123)},
Expected: int64(123),
},
{
In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}},
Expected: []byte{1, 2, 3},
},
{
In: Parameter{Type: StringType, Value: "writing's on the wall"},
Expected: "writing's on the wall",
},
{
In: Parameter{Type: Hash160Type, Value: util.Uint160{1, 2, 3}},
Expected: util.Uint160{1, 2, 3},
},
{
In: Parameter{Type: Hash256Type, Value: util.Uint256{1, 2, 3}},
Expected: util.Uint256{1, 2, 3},
},
{
In: Parameter{Type: PublicKeyType, Value: pk.PublicKey()},
Expected: pk.PublicKey().Bytes(),
},
{
In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}},
Expected: []byte{1, 2, 3},
},
{
In: Parameter{Type: ArrayType, Value: []Parameter{
{
Type: IntegerType,
Value: int64(123),
},
{
Type: ByteArrayType,
Value: []byte{1, 2, 3},
},
{
Type: ArrayType,
Value: []Parameter{
{
Type: BoolType,
Value: true,
},
},
},
}},
Expected: []interface{}{int64(123), []byte{1, 2, 3}, []interface{}{true}},
},
}
bw := io.NewBufBinWriter()
for _, testCase := range testCases {
actual, err := ExpandParameterToEmitable(testCase.In)
require.NoError(t, err)
require.Equal(t, testCase.Expected, actual)
emit.Array(bw.BinWriter, actual)
require.NoError(t, bw.Err)
}
errCases := []Parameter{
{Type: AnyType},
{Type: UnknownType},
{Type: MapType},
{Type: InteropInterfaceType},
}
for _, errCase := range errCases {
_, err := ExpandParameterToEmitable(errCase)
require.Error(t, err)
}
}

View file

@ -88,6 +88,8 @@ func Array(w *io.BinWriter, es ...interface{}) {
String(w, e)
case util.Uint160:
Bytes(w, e.BytesBE())
case util.Uint256:
Bytes(w, e.BytesBE())
case []byte:
Bytes(w, e)
case bool: