diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index 252ca4cfc..a34a70397 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -76,12 +76,14 @@ func GetDataFromContext(ctx *cli.Context) (int, interface{}, *cli.ExitError) { if err != nil { return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1) } - if len(params) != 1 { + if len(params) > 1 { return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1) } - data, err = smartcontract.ExpandParameterToEmitable(params[0]) - if err != nil { - return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1) + if len(params) != 0 { + data, err = smartcontract.ExpandParameterToEmitable(params[0]) + if err != nil { + return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1) + } } } return offset, data, nil diff --git a/cli/contract_test.go b/cli/contract_test.go index 07c826755..0dd20b12a 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -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) { diff --git a/cli/nep11_test.go b/cli/nep11_test.go new file mode 100644 index 000000000..7a7b8ff1e --- /dev/null +++ b/cli/nep11_test.go @@ -0,0 +1,284 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "testing" + + "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) + + tmpDir := os.TempDir() + walletPath := path.Join(tmpDir, "walletForImport.json") + defer os.Remove(walletPath) + + nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService) + require.NoError(t, err) + neoContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Neo) + require.NoError(t, err) + e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) + + args := []string{ + "neo-go", "wallet", "nep11", "import", + "--rpc-endpoint", "http://" + e.RPC.Addr, + "--wallet", walletPath, + } + // missing token hash + e.RunWithError(t, args...) + + // good + e.Run(t, append(args, "--token", nnsContractHash.StringLE())...) + + // already exists + e.RunWithError(t, append(args, "--token", nnsContractHash.StringLE())...) + + // not a NEP11 token + e.RunWithError(t, append(args, "--token", neoContractHash.StringLE())...) + + t.Run("Info", func(t *testing.T) { + checkNNSInfo := func(t *testing.T) { + e.checkNextLine(t, "^Name:\\s*NameService") + e.checkNextLine(t, "^Symbol:\\s*NNS") + e.checkNextLine(t, "^Hash:\\s*"+nnsContractHash.StringLE()) + e.checkNextLine(t, "^Decimals:\\s*0") + e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(nnsContractHash)) + e.checkNextLine(t, "^Standard:\\s*"+string(manifest.NEP11StandardName)) + } + t.Run("WithToken", func(t *testing.T) { + e.Run(t, "neo-go", "wallet", "nep11", "info", + "--wallet", walletPath, "--token", nnsContractHash.StringLE()) + checkNNSInfo(t) + }) + t.Run("NoToken", func(t *testing.T) { + e.Run(t, "neo-go", "wallet", "nep11", "info", + "--wallet", walletPath) + checkNNSInfo(t) + }) + }) + + t.Run("Remove", func(t *testing.T) { + e.In.WriteString("y\r") + e.Run(t, "neo-go", "wallet", "nep11", "remove", + "--wallet", walletPath, "--token", nnsContractHash.StringLE()) + e.Run(t, "neo-go", "wallet", "nep11", "info", + "--wallet", walletPath) + _, err := e.Out.ReadString('\n') + require.Equal(t, err, io.EOF) + }) +} + +func TestNEP11_OwnerOf_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 := func(t *testing.T) []byte { + // 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) + return tokenID + } + + tokenID := mint(t) + + // 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()) + + // ownerOf: missing contract hash + cmdOwnerOf := []string{"neo-go", "wallet", "nep11", "ownerOf", + "--rpc-endpoint", "http://" + e.RPC.Addr, + } + e.RunWithError(t, cmdOwnerOf...) + cmdOwnerOf = append(cmdOwnerOf, "--token", h.StringLE()) + + // ownerOf: missing token ID + e.RunWithError(t, cmdOwnerOf...) + cmdOwnerOf = append(cmdOwnerOf, "--id", string(tokenID)) + + // ownerOf: good + e.Run(t, cmdOwnerOf...) + e.checkNextLine(t, nftOwnerAddr) + + // tokensOf: missing contract hash + cmdTokensOf := []string{"neo-go", "wallet", "nep11", "tokensOf", + "--rpc-endpoint", "http://" + e.RPC.Addr, + } + e.RunWithError(t, cmdTokensOf...) + cmdTokensOf = append(cmdTokensOf, "--token", h.StringLE()) + + // tokensOf: missing owner address + e.RunWithError(t, cmdTokensOf...) + cmdTokensOf = append(cmdTokensOf, "--address", nftOwnerAddr) + + // tokensOf: good + e.Run(t, cmdTokensOf...) + e.checkNextLine(t, string(tokenID)) + + // properties: no contract + cmdProperties := []string{ + "neo-go", "wallet", "nep11", "properties", + "--rpc-endpoint", "http://" + e.RPC.Addr, + } + e.RunWithError(t, cmdProperties...) + cmdProperties = append(cmdProperties, "--token", h.StringLE()) + + // properties: no token ID + e.RunWithError(t, cmdProperties...) + cmdProperties = append(cmdProperties, "--id", string(tokenID)) + + // properties: ok + e.Run(t, cmdProperties...) + e.checkNextLine(t, fmt.Sprintf(`{"name":"HASHY %s"}`, string(tokenID))) + + // tokensOf: good, several tokens + tokenID1 := mint(t) + e.Run(t, cmdTokensOf...) + e.checkNextLine(t, string(tokenID)) + e.checkNextLine(t, string(tokenID1)) + + // tokens: missing contract hash + cmdTokens := []string{"neo-go", "wallet", "nep11", "tokens", + "--rpc-endpoint", "http://" + e.RPC.Addr, + } + e.RunWithError(t, cmdTokens...) + cmdTokens = append(cmdTokens, "--token", h.StringLE()) + + // tokens: good, several tokens + e.Run(t, cmdTokens...) + e.checkNextLine(t, string(tokenID)) + e.checkNextLine(t, string(tokenID1)) + + // balance check: several tokens, ok + e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...) + checkBalanceResult(t, nftOwnerAddr, "2") + + 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, "1") // tokenID1 +} + +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) +} diff --git a/cli/nep17_test.go b/cli/nep17_test.go index b614682d5..07a496a97 100644 --- a/cli/nep17_test.go +++ b/cli/nep17_test.go @@ -12,6 +12,7 @@ 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/encoding/fixedn" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -133,9 +134,9 @@ func TestNEP17Transfer(t *testing.T) { require.Equal(t, big.NewInt(1), b) hVerify := deployVerifyContract(t, e) + const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo" t.Run("default address", func(t *testing.T) { - const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo" e.In.WriteString("one\r") e.Run(t, "neo-go", "wallet", "nep17", "multitransfer", "--rpc-endpoint", "http://"+e.RPC.Addr, @@ -161,6 +162,18 @@ func TestNEP17Transfer(t *testing.T) { require.Equal(t, big.NewInt(41), b) }) + t.Run("with signers", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "nep17", "multitransfer", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--from", validatorAddr, + "NEO:"+validatorDefault+":42", + "GAS:"+validatorDefault+":7", + "--", validatorAddr+":Global") + e.checkTxPersisted(t) + }) + validTil := e.Chain.BlockHeight() + 100 cmd := []string{ "neo-go", "wallet", "nep17", "transfer", @@ -256,6 +269,8 @@ func TestNEP17ImportToken(t *testing.T) { require.NoError(t, err) gasContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Gas) require.NoError(t, err) + nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService) + require.NoError(t, err) e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) // missing token hash @@ -272,6 +287,12 @@ func TestNEP17ImportToken(t *testing.T) { "--wallet", walletPath, "--token", address.Uint160ToString(neoContractHash)) // try address instead of sh + // not a NEP17 token + e.RunWithError(t, "neo-go", "wallet", "nep17", "import", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", walletPath, + "--token", nnsContractHash.StringLE()) + t.Run("Info", func(t *testing.T) { checkGASInfo := func(t *testing.T) { e.checkNextLine(t, "^Name:\\s*GasToken") @@ -279,6 +300,7 @@ func TestNEP17ImportToken(t *testing.T) { e.checkNextLine(t, "^Hash:\\s*"+gasContractHash.StringLE()) e.checkNextLine(t, "^Decimals:\\s*8") e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(gasContractHash)) + e.checkNextLine(t, "^Standard:\\s*"+string(manifest.NEP17StandardName)) } t.Run("WithToken", func(t *testing.T) { e.Run(t, "neo-go", "wallet", "nep17", "info", @@ -296,6 +318,7 @@ func TestNEP17ImportToken(t *testing.T) { e.checkNextLine(t, "^Hash:\\s*"+neoContractHash.StringLE()) e.checkNextLine(t, "^Decimals:\\s*0") e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(neoContractHash)) + e.checkNextLine(t, "^Standard:\\s*"+string(manifest.NEP17StandardName)) }) t.Run("Remove", func(t *testing.T) { e.In.WriteString("y\r") diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go new file mode 100644 index 000000000..ee20157b7 --- /dev/null +++ b/cli/wallet/nep11.go @@ -0,0 +1,390 @@ +package wallet + +import ( + "errors" + "fmt" + "math/big" + + "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/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/urfave/cli" +) + +func newNEP11Commands() []cli.Command { + tokenAddressFlag := flags.AddressFlag{ + Name: "token", + Usage: "Token contract address or hash in LE", + } + ownerAddressFlag := flags.AddressFlag{ + Name: "address", + Usage: "NFT owner address or hash in LE", + } + tokenID := cli.StringFlag{ + Name: "id", + Usage: "Token ID", + } + + balanceFlags := make([]cli.Flag, len(baseBalanceFlags)) + 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", + Usage: "get address balance", + UsageText: "balance --wallet --rpc-endpoint [--timeout