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) b, _ = e.Chain.GetGoverningTokenBalance(sh)
require.Equal(t, big.NewInt(41), b) 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) { func TestNEP17MultiTransfer(t *testing.T) {

View file

@ -538,7 +538,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
paramsStart++ paramsStart++
if len(args) > paramsStart { if len(args) > paramsStart {
cosignersOffset, params, err = parseParams(args[paramsStart:], true) cosignersOffset, params, err = ParseParams(args[paramsStart:], true)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -623,12 +623,12 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
return nil 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. // returns the number of handled words, the array itself and an error.
// `calledFromMain` denotes whether the method was called from the outside or // `calledFromMain` denotes whether the method was called from the outside or
// recursively and used to check if cosignersSeparator and closing bracket are // recursively and used to check if cosignersSeparator and closing bracket are
// allowed to be in `args` sequence. // 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{} res := []smartcontract.Parameter{}
for k := 0; k < len(args); { for k := 0; k < len(args); {
s := args[k] 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") return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
case arrayStartSeparator: case arrayStartSeparator:
numWordsRead, array, err := parseParams(args[k+1:], false) numWordsRead, array, err := ParseParams(args[k+1:], false)
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("failed to parse array: %w", err) return 0, nil, fmt.Errorf("failed to parse array: %w", err)
} }
@ -887,6 +887,26 @@ func contractDeploy(ctx *cli.Context) error {
return nil 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. // ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig.
func ParseContractConfig(confFile string) (ProjectConfig, error) { func ParseContractConfig(confFile string) (ProjectConfig, error) {
conf := ProjectConfig{} conf := ProjectConfig{}

View file

@ -202,7 +202,7 @@ func TestParseParams_CalledFromItself(t *testing.T) {
for str, expected := range testCases { for str, expected := range testCases {
input := strings.Split(str, " ") input := strings.Split(str, " ")
offset, actual, err := parseParams(input, false) offset, actual, err := ParseParams(input, false)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset) require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Value, actual) require.Equal(t, expected.Value, actual)
@ -218,7 +218,7 @@ func TestParseParams_CalledFromItself(t *testing.T) {
for _, str := range errorCases { for _, str := range errorCases {
input := strings.Split(str, " ") input := strings.Split(str, " ")
_, _, err := parseParams(input, false) _, _, err := ParseParams(input, false)
require.Error(t, err) require.Error(t, err)
} }
} }
@ -400,7 +400,7 @@ func TestParseParams_CalledFromOutside(t *testing.T) {
} }
for str, expected := range testCases { for str, expected := range testCases {
input := strings.Split(str, " ") input := strings.Split(str, " ")
offset, arr, err := parseParams(input, true) offset, arr, err := ParseParams(input, true)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset) require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Parameters, arr) require.Equal(t, expected.Parameters, arr)
@ -415,7 +415,7 @@ func TestParseParams_CalledFromOutside(t *testing.T) {
} }
for _, str := range errorCases { for _, str := range errorCases {
input := strings.Split(str, " ") input := strings.Split(str, " ")
_, _, err := parseParams(input, true) _, _, err := ParseParams(input, true)
require.Error(t, err) 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/flags"
"github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext" "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/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/client"
@ -111,9 +112,12 @@ func newNEP17Commands() []cli.Command {
{ {
Name: "transfer", Name: "transfer",
Usage: "transfer NEP17 tokens", 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, Action: transferNEP17,
Flags: transferFlags, 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", Name: "multitransfer",
@ -409,6 +413,7 @@ func multiTransferNEP17(ctx *cli.Context) error {
Token: token.Hash, Token: token.Hash,
Address: addr, Address: addr,
Amount: amount.Int64(), 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) 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{{ return signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{
Token: token.Hash, Token: token.Hash,
Address: to, Address: to,
Amount: amount.Int64(), Amount: amount.Int64(),
Data: data,
}}) }})
} }
func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget) error { func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget) error {
gas := flags.Fixed8FromContext(ctx, "gas") 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 { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }

View file

@ -8,7 +8,7 @@ ProtocolConfiguration:
ValidatorsCount: 1 ValidatorsCount: 1
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: false P2PSigExtensions: true
NativeActivations: NativeActivations:
ContractManagement: [0] ContractManagement: [0]
StdLib: [0] StdLib: [0]
@ -20,6 +20,7 @@ ProtocolConfiguration:
RoleManagement: [0] RoleManagement: [0]
OracleContract: [0] OracleContract: [0]
NameService: [0] NameService: [0]
Notary: [0]
ApplicationConfiguration: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # 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" "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 { type TransferTarget struct {
Token util.Uint160 Token util.Uint160
Address util.Uint160 Address util.Uint160
Amount int64 Amount int64
Data interface{}
} }
// SignerAccount represents combination of the transaction.Signer and the // 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, {Token: token,
Address: to, Address: to,
Amount: amount, Amount: amount,
Data: data,
}, },
}, []interface{}{data}) })
} }
// CreateNEP17MultiTransferTx creates an invocation transaction for performing NEP17 transfers // CreateNEP17MultiTransferTx creates an invocation transaction for performing NEP17 transfers
// from a single sender to multiple recipients with the given data. // 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) from, err := address.StringToUint160(acc.Address)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad account address: %w", err) 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() w := io.NewBufBinWriter()
for i := range recipients { for i := range recipients {
emit.AppCall(w.BinWriter, recipients[i].Token, "transfer", callflag.All, 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) emit.Opcodes(w.BinWriter, opcode.ASSERT)
} }
if w.Err != nil { 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. // 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 { if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized return util.Uint256{}, errNetworkNotInitialized
} }
tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, data) tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients)
if err != nil { if err != nil {
return util.Uint256{}, err return util.Uint256{}, err
} }

View file

@ -14,6 +14,7 @@ import (
"strings" "strings"
"unicode/utf8" "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/io"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
) )
@ -437,3 +438,28 @@ func NewParameterFromString(in string) (*Parameter, error) {
} }
return res, nil 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" "testing"
"github.com/nspcc-dev/neo-go/internal/testserdes" "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/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -517,3 +520,85 @@ func hexToBase64(s string) string {
b, _ := hex.DecodeString(s) b, _ := hex.DecodeString(s)
return base64.StdEncoding.EncodeToString(b) 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) String(w, e)
case util.Uint160: case util.Uint160:
Bytes(w, e.BytesBE()) Bytes(w, e.BytesBE())
case util.Uint256:
Bytes(w, e.BytesBE())
case []byte: case []byte:
Bytes(w, e) Bytes(w, e)
case bool: case bool: