cli: add nep11 transfer command

This commit is contained in:
Anna Shaleva 2021-04-23 19:00:45 +03:00
parent 35ba3d97e6
commit 7ea15f02c6
4 changed files with 278 additions and 38 deletions

View file

@ -227,30 +227,33 @@ func TestContractDeployWithData(t *testing.T) {
} }
func deployVerifyContract(t *testing.T, e *executor) util.Uint160 { func deployVerifyContract(t *testing.T, e *executor) util.Uint160 {
tmpDir := path.Join(os.TempDir(), "neogo.test.deployverifycontract") return deployContract(t, e, "testdata/verify.go", "testdata/verify.yml", validatorWallet, validatorAddr, "one")
require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) }
func deployContract(t *testing.T, e *executor, inPath, configPath, wallet, address, pass string) util.Uint160 {
tmpDir, err := ioutil.TempDir(os.TempDir(), "neogo.test.deploycontract*")
require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
os.RemoveAll(tmpDir) os.RemoveAll(tmpDir)
}) })
// deploy verification contract nefName := path.Join(tmpDir, "contract.nef")
nefName := path.Join(tmpDir, "verify.nef") manifestName := path.Join(tmpDir, "contract.manifest.json")
manifestName := path.Join(tmpDir, "verify.manifest.json")
e.Run(t, "neo-go", "contract", "compile", e.Run(t, "neo-go", "contract", "compile",
"--in", "testdata/verify.go", "--in", inPath,
"--config", "testdata/verify.yml", "--config", configPath,
"--out", nefName, "--manifest", manifestName) "--out", nefName, "--manifest", manifestName)
e.In.WriteString("one\r") e.In.WriteString(pass + "\r")
e.Run(t, "neo-go", "contract", "deploy", e.Run(t, "neo-go", "contract", "deploy",
"--rpc-endpoint", "http://"+e.RPC.Addr, "--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", validatorWallet, "--address", validatorAddr, "--wallet", wallet, "--address", address,
"--in", nefName, "--manifest", manifestName) "--in", nefName, "--manifest", manifestName)
e.checkTxPersisted(t, "Sent invocation transaction ") e.checkTxPersisted(t, "Sent invocation transaction ")
line, err := e.Out.ReadString('\n') line, err := e.Out.ReadString('\n')
require.NoError(t, err) require.NoError(t, err)
line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: ")) line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: "))
hVerify, err := util.Uint160DecodeStringLE(line) h, err := util.Uint160DecodeStringLE(line)
require.NoError(t, err) require.NoError(t, err)
return hVerify return h
} }
func TestComlileAndInvokeFunction(t *testing.T) { func TestComlileAndInvokeFunction(t *testing.T) {

View file

@ -2,6 +2,7 @@ package main
import ( import (
"io" "io"
"io/ioutil"
"os" "os"
"path" "path"
"testing" "testing"
@ -9,9 +10,19 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"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/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const (
// nftOwnerAddr is the owner of NFT-ND HASHY token (../examples/nft-nd/nft.go)
nftOwnerAddr = "NX1yL5wDx3inK2qUVLRVaqCLUxYnAbv85S"
nftOwnerWallet = "../examples/my_wallet.json"
nftOwnerPass = "qwerty"
)
func TestNEP11Import(t *testing.T) { func TestNEP11Import(t *testing.T) {
e := newExecutor(t, true) e := newExecutor(t, true)
@ -73,3 +84,127 @@ func TestNEP11Import(t *testing.T) {
require.Equal(t, err, io.EOF) require.Equal(t, err, io.EOF)
}) })
} }
func TestNEP11_BalanceOf_Transfer(t *testing.T) {
e := newExecutor(t, true)
tmpDir, err := ioutil.TempDir(os.TempDir(), "neogo.test.nftwallet*")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(tmpDir)
})
// copy wallet to temp dir in order not to overwrite the original file
bytesRead, err := ioutil.ReadFile(nftOwnerWallet)
require.NoError(t, err)
wall := path.Join(tmpDir, "my_wallet.json")
err = ioutil.WriteFile(wall, bytesRead, 0755)
require.NoError(t, err)
// transfer funds to contract owner
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", validatorWallet,
"--to", nftOwnerAddr,
"--token", "GAS",
"--amount", "10000",
"--from", validatorAddr)
e.checkTxPersisted(t)
// deploy NFT HASHY contract
h := deployNFTContract(t, e)
// mint 1 HASHY token by transferring 10 GAS to HASHY contract
e.In.WriteString(nftOwnerPass + "\r")
e.Run(t, "neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", wall,
"--to", h.StringLE(),
"--token", "GAS",
"--amount", "10",
"--from", nftOwnerAddr)
txMint, _ := e.checkTxPersisted(t)
// get NFT ID from AER
aer, err := e.Chain.GetAppExecResults(txMint.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, 1, len(aer))
require.Equal(t, 2, len(aer[0].Events))
hashyMintEvent := aer[0].Events[1]
require.Equal(t, "Transfer", hashyMintEvent.Name)
tokenID, err := hashyMintEvent.Item.Value().([]stackitem.Item)[3].TryBytes()
require.NoError(t, err)
require.NotNil(t, tokenID)
// check the balance
cmdCheckBalance := []string{"neo-go", "wallet", "nep11", "balance",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", wall,
"--address", nftOwnerAddr}
checkBalanceResult := func(t *testing.T, acc string, amount string) {
e.checkNextLine(t, "^\\s*Account\\s+"+acc)
e.checkNextLine(t, "^\\s*HASHY:\\s+HASHY NFT \\("+h.StringLE()+"\\)")
e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+amount+"$")
e.checkEOF(t)
}
// balance check: by symbol, token is not imported
e.RunWithError(t, append(cmdCheckBalance, "--token", "HASHY")...)
// balance check: by hash, ok
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, "1")
// import token
e.Run(t, "neo-go", "wallet", "nep11", "import",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", wall,
"--token", h.StringLE())
// balance check: by symbol, ok
e.Run(t, append(cmdCheckBalance, "--token", "HASHY")...)
checkBalanceResult(t, nftOwnerAddr, "1")
// balance check: all accounts
e.Run(t, "neo-go", "wallet", "nep11", "balance",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", wall,
"--token", h.StringLE())
checkBalanceResult(t, nftOwnerAddr, "1")
// remove token from wallet
e.In.WriteString("y\r")
e.Run(t, "neo-go", "wallet", "nep11", "remove",
"--wallet", wall, "--token", h.StringLE())
cmdTransfer := []string{
"neo-go", "wallet", "nep11", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", wall,
"--to", validatorAddr,
"--from", nftOwnerAddr,
}
// transfer: unimported token with symbol id specified
e.In.WriteString(nftOwnerPass + "\r")
e.RunWithError(t, append(cmdTransfer,
"--token", "HASHY")...)
cmdTransfer = append(cmdTransfer, "--token", h.StringLE())
// transfer: no id specified
e.In.WriteString(nftOwnerPass + "\r")
e.RunWithError(t, cmdTransfer...)
cmdTransfer = append(cmdTransfer, "--id", string(tokenID))
// transfer: good
e.In.WriteString(nftOwnerPass + "\r")
e.Run(t, cmdTransfer...)
e.checkTxPersisted(t)
// check balance after transfer
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, "0")
}
func deployNFTContract(t *testing.T, e *executor) util.Uint160 {
return deployContract(t, e, "../examples/nft-nd/nft.go", "../examples/nft-nd/nft.yml", nftOwnerWallet, nftOwnerAddr, nftOwnerPass)
}

View file

@ -7,9 +7,13 @@ 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/pkg/core/transaction"
"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/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -24,6 +28,10 @@ func newNEP11Commands() []cli.Command {
copy(balanceFlags, baseBalanceFlags) copy(balanceFlags, baseBalanceFlags)
balanceFlags = append(balanceFlags, tokenID) balanceFlags = append(balanceFlags, tokenID)
balanceFlags = append(balanceFlags, options.RPC...) balanceFlags = append(balanceFlags, options.RPC...)
transferFlags := make([]cli.Flag, len(baseTransferFlags))
copy(transferFlags, baseTransferFlags)
transferFlags = append(transferFlags, tokenID)
transferFlags = append(transferFlags, options.RPC...)
return []cli.Command{ return []cli.Command{
{ {
Name: "balance", Name: "balance",
@ -60,6 +68,21 @@ func newNEP11Commands() []cli.Command {
forceFlag, forceFlag,
}, },
}, },
{
Name: "transfer",
Usage: "transfer NEP11 tokens",
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
Action: transferNEP11,
Flags: transferFlags,
Description: `Transfers specified NEP11 token with optional cosigners list attached to
the transfer. Amount should be specified for divisible NEP11
tokens and omitted for non-divisible NEP11 tokens. See
'contract testinvokefunction' documentation for the details
about cosigners syntax. If no cosigners are given then the
sender with CalledByEntry scope will be used as the only
signer.
`,
},
} }
} }
@ -159,3 +182,42 @@ func getNEP11Balance(ctx *cli.Context) error {
} }
return nil return nil
} }
func transferNEP11(ctx *cli.Context) error {
return transferNEP(ctx, manifest.NEP11StandardName)
}
func signAndSendNEP11Transfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, token, to util.Uint160, tokenID string, amount *big.Int, cosigners []client.SignerAccount) error {
gas := flags.Fixed8FromContext(ctx, "gas")
var (
tx *transaction.Transaction
err error
)
if amount != nil {
from, err := address.StringToUint160(acc.Address)
if err != nil {
return cli.NewExitError(fmt.Errorf("bad account address: %w", err), 1)
}
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, from, to, amount, tokenID)
} else {
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID)
}
if err != nil {
return cli.NewExitError(err, 1)
}
if outFile := ctx.String("out"); outFile != "" {
if err := paramcontext.InitAndSave(c.GetNetwork(), tx, acc, outFile); err != nil {
return cli.NewExitError(err, 1)
}
} else {
_, err := c.SignAndPushTx(tx, acc, cosigners)
if err != nil {
return cli.NewExitError(err, 1)
}
}
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
return nil
}

View file

@ -43,7 +43,7 @@ var (
Usage: "Token contract address or hash in LE", Usage: "Token contract address or hash in LE",
}, },
}, options.RPC...) }, options.RPC...)
transferFlags = append([]cli.Flag{ baseTransferFlags = []cli.Flag{
walletPathFlag, walletPathFlag,
outFlag, outFlag,
fromAddrFlag, fromAddrFlag,
@ -54,7 +54,7 @@ var (
Name: "amount", Name: "amount",
Usage: "Amount of asset to send", Usage: "Amount of asset to send",
}, },
}, options.RPC...) }
multiTransferFlags = append([]cli.Flag{ multiTransferFlags = append([]cli.Flag{
walletPathFlag, walletPathFlag,
outFlag, outFlag,
@ -67,6 +67,9 @@ func newNEP17Commands() []cli.Command {
balanceFlags := make([]cli.Flag, len(baseBalanceFlags)) balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
copy(balanceFlags, baseBalanceFlags) copy(balanceFlags, baseBalanceFlags)
balanceFlags = append(balanceFlags, options.RPC...) balanceFlags = append(balanceFlags, options.RPC...)
transferFlags := make([]cli.Flag, len(baseTransferFlags))
copy(transferFlags, baseTransferFlags)
transferFlags = append(transferFlags, options.RPC...)
return []cli.Command{ return []cli.Command{
{ {
Name: "balance", Name: "balance",
@ -218,16 +221,31 @@ func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard
}, len(w.Extra.Tokens), name, standard) }, len(w.Extra.Tokens), name, standard)
} }
func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160, name string) (*wallet.Token, error) { func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160, name string, standard string) (*wallet.Token, error) {
bs, err := c.GetNEP17Balances(addr) switch standard {
if err != nil { case manifest.NEP17StandardName:
return nil, err bs, err := c.GetNEP17Balances(addr)
if err != nil {
return nil, err
}
get := func(i int) *wallet.Token {
t, _ := c.NEP17TokenInfo(bs.Balances[i].Asset)
return t
}
return getMatchingTokenAux(ctx, get, len(bs.Balances), name, standard)
case manifest.NEP11StandardName:
tokenHash, err := flags.ParseAddress(name)
if err != nil {
return nil, fmt.Errorf("valid token adress or hash in LE should be specified for %s RPC-node request: %s", standard, err.Error())
}
get := func(i int) *wallet.Token {
t, _ := c.NEP11TokenInfo(tokenHash)
return t
}
return getMatchingTokenAux(ctx, get, 1, name, standard)
default:
return nil, fmt.Errorf("unsupported %s token", standard)
} }
get := func(i int) *wallet.Token {
t, _ := c.NEP17TokenInfo(bs.Balances[i].Asset)
return t
}
return getMatchingTokenAux(ctx, get, len(bs.Balances), name, manifest.NEP17StandardName)
} }
func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string, standard string) (*wallet.Token, error) { func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string, standard string) (*wallet.Token, error) {
@ -423,7 +441,7 @@ func multiTransferNEP17(ctx *cli.Context) error {
token, err = getMatchingToken(ctx, wall, ss[0], manifest.NEP17StandardName) token, err = getMatchingToken(ctx, wall, ss[0], manifest.NEP17StandardName)
if err != nil { if err != nil {
fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.") fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.")
token, err = getMatchingTokenRPC(ctx, c, from, ss[0]) token, err = getMatchingTokenRPC(ctx, c, from, ss[0], manifest.NEP17StandardName)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -455,10 +473,14 @@ func multiTransferNEP17(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("failed to create NEP17 multitransfer transaction: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to create NEP17 multitransfer transaction: %w", err), 1)
} }
return signAndSendTransfer(ctx, c, acc, recipients, cosignersAccounts) return signAndSendNEP17Transfer(ctx, c, acc, recipients, cosignersAccounts)
} }
func transferNEP17(ctx *cli.Context) error { func transferNEP17(ctx *cli.Context) error {
return transferNEP(ctx, manifest.NEP17StandardName)
}
func transferNEP(ctx *cli.Context, standard string) error {
wall, err := openWallet(ctx.String("wallet")) wall, err := openWallet(ctx.String("wallet"))
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
@ -485,20 +507,15 @@ func transferNEP17(ctx *cli.Context) error {
toFlag := ctx.Generic("to").(*flags.Address) toFlag := ctx.Generic("to").(*flags.Address)
to := toFlag.Uint160() to := toFlag.Uint160()
token, err := getMatchingToken(ctx, wall, ctx.String("token"), manifest.NEP17StandardName) token, err := getMatchingToken(ctx, wall, ctx.String("token"), standard)
if err != nil { if err != nil {
fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.") fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.")
token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token")) token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token"), standard)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to get matching token: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to get matching token: %w", err), 1)
} }
} }
amount, err := fixedn.FromString(ctx.String("amount"), int(token.Decimals))
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
}
cosignersOffset, data, extErr := cmdargs.GetDataFromContext(ctx) cosignersOffset, data, extErr := cmdargs.GetDataFromContext(ctx)
if extErr != nil { if extErr != nil {
return extErr return extErr
@ -513,15 +530,38 @@ func transferNEP17(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("failed to create NEP17 transfer transaction: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to create NEP17 transfer transaction: %w", err), 1)
} }
return signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{ amountArg := ctx.String("amount")
Token: token.Hash, switch standard {
Address: to, case manifest.NEP17StandardName:
Amount: amount.Int64(), amount, err := fixedn.FromString(amountArg, int(token.Decimals))
Data: data, if err != nil {
}}, cosignersAccounts) return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
}
return signAndSendNEP17Transfer(ctx, c, acc, []client.TransferTarget{{
Token: token.Hash,
Address: to,
Amount: amount.Int64(),
Data: data,
}}, cosignersAccounts)
case manifest.NEP11StandardName:
tokenID := ctx.String("id")
if tokenID == "" {
return cli.NewExitError(errors.New("token ID should be specified"), 1)
}
if amountArg == "" {
return signAndSendNEP11Transfer(ctx, c, acc, token.Hash, to, tokenID, nil, cosignersAccounts)
}
amount, err := fixedn.FromString(amountArg, int(token.Decimals))
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
}
return signAndSendNEP11Transfer(ctx, c, acc, token.Hash, to, tokenID, amount, cosignersAccounts)
default:
return cli.NewExitError(fmt.Errorf("unsupported token standard %s", standard), 1)
}
} }
func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget, cosigners []client.SignerAccount) error { func signAndSendNEP17Transfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget, cosigners []client.SignerAccount) error {
gas := flags.Fixed8FromContext(ctx, "gas") gas := flags.Fixed8FromContext(ctx, "gas")
tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, cosigners) tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, cosigners)