forked from TrueCloudLab/neoneo-go
cli: add nep11 transfer
command
This commit is contained in:
parent
35ba3d97e6
commit
7ea15f02c6
4 changed files with 278 additions and 38 deletions
|
@ -227,30 +227,33 @@ func TestContractDeployWithData(t *testing.T) {
|
|||
}
|
||||
|
||||
func deployVerifyContract(t *testing.T, e *executor) util.Uint160 {
|
||||
tmpDir := path.Join(os.TempDir(), "neogo.test.deployverifycontract")
|
||||
require.NoError(t, os.Mkdir(tmpDir, os.ModePerm))
|
||||
return deployContract(t, e, "testdata/verify.go", "testdata/verify.yml", validatorWallet, validatorAddr, "one")
|
||||
}
|
||||
|
||||
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() {
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
// deploy verification contract
|
||||
nefName := path.Join(tmpDir, "verify.nef")
|
||||
manifestName := path.Join(tmpDir, "verify.manifest.json")
|
||||
nefName := path.Join(tmpDir, "contract.nef")
|
||||
manifestName := path.Join(tmpDir, "contract.manifest.json")
|
||||
e.Run(t, "neo-go", "contract", "compile",
|
||||
"--in", "testdata/verify.go",
|
||||
"--config", "testdata/verify.yml",
|
||||
"--in", inPath,
|
||||
"--config", configPath,
|
||||
"--out", nefName, "--manifest", manifestName)
|
||||
e.In.WriteString("one\r")
|
||||
e.In.WriteString(pass + "\r")
|
||||
e.Run(t, "neo-go", "contract", "deploy",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", validatorWallet, "--address", validatorAddr,
|
||||
"--wallet", wallet, "--address", address,
|
||||
"--in", nefName, "--manifest", manifestName)
|
||||
e.checkTxPersisted(t, "Sent invocation transaction ")
|
||||
line, err := e.Out.ReadString('\n')
|
||||
require.NoError(t, err)
|
||||
line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: "))
|
||||
hVerify, err := util.Uint160DecodeStringLE(line)
|
||||
h, err := util.Uint160DecodeStringLE(line)
|
||||
require.NoError(t, err)
|
||||
return hVerify
|
||||
return h
|
||||
}
|
||||
|
||||
func TestComlileAndInvokeFunction(t *testing.T) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
@ -9,9 +10,19 @@ import (
|
|||
"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/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"
|
||||
)
|
||||
|
||||
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) {
|
||||
e := newExecutor(t, true)
|
||||
|
||||
|
@ -73,3 +84,127 @@ func TestNEP11Import(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -7,9 +7,13 @@ 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"
|
||||
"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/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/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
@ -24,6 +28,10 @@ func newNEP11Commands() []cli.Command {
|
|||
copy(balanceFlags, baseBalanceFlags)
|
||||
balanceFlags = append(balanceFlags, tokenID)
|
||||
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{
|
||||
{
|
||||
Name: "balance",
|
||||
|
@ -60,6 +68,21 @@ func newNEP11Commands() []cli.Command {
|
|||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ var (
|
|||
Usage: "Token contract address or hash in LE",
|
||||
},
|
||||
}, options.RPC...)
|
||||
transferFlags = append([]cli.Flag{
|
||||
baseTransferFlags = []cli.Flag{
|
||||
walletPathFlag,
|
||||
outFlag,
|
||||
fromAddrFlag,
|
||||
|
@ -54,7 +54,7 @@ var (
|
|||
Name: "amount",
|
||||
Usage: "Amount of asset to send",
|
||||
},
|
||||
}, options.RPC...)
|
||||
}
|
||||
multiTransferFlags = append([]cli.Flag{
|
||||
walletPathFlag,
|
||||
outFlag,
|
||||
|
@ -67,6 +67,9 @@ func newNEP17Commands() []cli.Command {
|
|||
balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
|
||||
copy(balanceFlags, baseBalanceFlags)
|
||||
balanceFlags = append(balanceFlags, options.RPC...)
|
||||
transferFlags := make([]cli.Flag, len(baseTransferFlags))
|
||||
copy(transferFlags, baseTransferFlags)
|
||||
transferFlags = append(transferFlags, options.RPC...)
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "balance",
|
||||
|
@ -218,7 +221,9 @@ func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, 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) {
|
||||
switch standard {
|
||||
case manifest.NEP17StandardName:
|
||||
bs, err := c.GetNEP17Balances(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -227,7 +232,20 @@ func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160,
|
|||
t, _ := c.NEP17TokenInfo(bs.Balances[i].Asset)
|
||||
return t
|
||||
}
|
||||
return getMatchingTokenAux(ctx, get, len(bs.Balances), name, manifest.NEP17StandardName)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 signAndSendTransfer(ctx, c, acc, recipients, cosignersAccounts)
|
||||
return signAndSendNEP17Transfer(ctx, c, acc, recipients, cosignersAccounts)
|
||||
}
|
||||
|
||||
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"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
|
@ -485,20 +507,15 @@ func transferNEP17(ctx *cli.Context) error {
|
|||
|
||||
toFlag := ctx.Generic("to").(*flags.Address)
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
if extErr != nil {
|
||||
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 signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{
|
||||
amountArg := ctx.String("amount")
|
||||
switch standard {
|
||||
case manifest.NEP17StandardName:
|
||||
amount, err := fixedn.FromString(amountArg, int(token.Decimals))
|
||||
if err != nil {
|
||||
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")
|
||||
|
||||
tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, cosigners)
|
||||
|
|
Loading…
Reference in a new issue