Merge pull request #1906 from nspcc-dev/cli/transfer_param
cli: allow to pass 'data' for nep17 transfer command
This commit is contained in:
commit
8f14c61c34
9 changed files with 180 additions and 24 deletions
|
@ -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) {
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue