Merge pull request #1918 from nspcc-dev/cli/nep11

cli: support NEP11-related commands
This commit is contained in:
Roman Khimov 2021-04-30 17:10:29 +03:00 committed by GitHub
commit 3acdbbd603
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1191 additions and 127 deletions

View file

@ -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

View file

@ -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) {

284
cli/nep11_test.go Normal file
View file

@ -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)
}

View file

@ -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")

390
cli/wallet/nep11.go Normal file
View file

@ -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 <path> --rpc-endpoint <node> [--timeout <time>] [--address <address>] --token <hash-or-name> [--id <token-id>]",
Action: getNEP11Balance,
Flags: balanceFlags,
},
{
Name: "import",
Usage: "import NEP11 token to a wallet",
UsageText: "import --wallet <path> --rpc-endpoint <node> --timeout <time> --token <hash>",
Action: importNEP11Token,
Flags: importFlags,
},
{
Name: "info",
Usage: "print imported NEP11 token info",
UsageText: "print --wallet <path> [--token <hash-or-name>]",
Action: printNEP11Info,
Flags: []cli.Flag{
walletPathFlag,
tokenFlag,
},
},
{
Name: "remove",
Usage: "remove NEP11 token from the wallet",
UsageText: "remove --wallet <path> --token <hash-or-name>",
Action: removeNEP11Token,
Flags: []cli.Flag{
walletPathFlag,
tokenFlag,
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.
`,
},
{
Name: "properties",
Usage: "print properties of NEP11 token",
UsageText: "properties --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11Properties,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
}, options.RPC...),
},
{
Name: "ownerOf",
Usage: "print owner of non-divisible NEP11 token with the specified ID",
UsageText: "ownerOf --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11Owner,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
}, options.RPC...),
},
{
Name: "tokensOf",
Usage: "print list of tokens IDs for the specified divisible NFT owner",
UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>",
Action: printNEP11TokensOf,
Flags: append([]cli.Flag{
tokenAddressFlag,
ownerAddressFlag,
}, options.RPC...),
},
{
Name: "tokens",
Usage: "print list of tokens IDs minted by the specified NFT (optional method)",
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
Action: printNEP11Tokens,
Flags: append([]cli.Flag{
tokenAddressFlag,
}, options.RPC...),
},
}
}
func importNEP11Token(ctx *cli.Context) error {
return importNEPToken(ctx, manifest.NEP11StandardName)
}
func printNEP11Info(ctx *cli.Context) error {
return printNEPInfo(ctx, manifest.NEP11StandardName)
}
func removeNEP11Token(ctx *cli.Context) error {
return removeNEPToken(ctx, manifest.NEP11StandardName)
}
func getNEP11Balance(ctx *cli.Context) error {
var accounts []*wallet.Account
wall, err := openWallet(ctx.String("wallet"))
if err != nil {
return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1)
}
defer wall.Close()
addrFlag := ctx.Generic("address").(*flags.Address)
if addrFlag.IsSet {
addrHash := addrFlag.Uint160()
acc := wall.GetAccount(addrHash)
if acc == nil {
return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addrHash)), 1)
}
accounts = append(accounts, acc)
} else {
if len(wall.Accounts) == 0 {
return cli.NewExitError(errors.New("no accounts in the wallet"), 1)
}
accounts = wall.Accounts
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
name := ctx.String("token")
if name == "" {
return cli.NewExitError("token hash or name should be specified", 1)
}
token, err := getMatchingToken(ctx, wall, name, manifest.NEP11StandardName)
if err != nil {
fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for token info.")
tokenHash, err := flags.ParseAddress(name)
if err != nil {
return cli.NewExitError(fmt.Sprintf("valid token adress or hash in LE should be specified for RPC-node request: %s", err.Error()), 1)
}
token, err = c.NEP11TokenInfo(tokenHash)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
}
tokenID := ctx.String("id")
for k, acc := range accounts {
addrHash, err := address.StringToUint160(acc.Address)
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid account address: %w", err), 1)
}
if k != 0 {
fmt.Fprintln(ctx.App.Writer)
}
fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)
var amount int64
if tokenID == "" {
amount, err = c.NEP11BalanceOf(token.Hash, addrHash)
} else {
amount, err = c.NEP11DBalanceOf(token.Hash, addrHash, tokenID)
}
if err != nil {
continue
}
amountStr := fixedn.ToString(big.NewInt(amount), int(token.Decimals))
format := "%s: %s (%s)\n"
formatArgs := []interface{}{token.Symbol, token.Name, token.Hash.StringLE()}
if tokenID != "" {
format = "%s: %s (%s, %s)\n"
formatArgs = append(formatArgs, tokenID)
}
fmt.Fprintf(ctx.App.Writer, format, formatArgs...)
fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", amountStr)
}
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
}
func printNEP11Owner(ctx *cli.Context) error {
var err error
tokenHash := ctx.Generic("token").(*flags.Address)
if !tokenHash.IsSet {
return cli.NewExitError("token contract hash was not set", 1)
}
tokenID := ctx.String("id")
if tokenID == "" {
return cli.NewExitError(errors.New("token ID should be specified"), 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
result, err := c.NEP11NDOwnerOf(tokenHash.Uint160(), tokenID)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP11 `ownerOf` method: %s", err.Error()), 1)
}
fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(result))
return nil
}
func printNEP11TokensOf(ctx *cli.Context) error {
var err error
tokenHash := ctx.Generic("token").(*flags.Address)
if !tokenHash.IsSet {
return cli.NewExitError("token contract hash was not set", 1)
}
acc := ctx.Generic("address").(*flags.Address)
if !acc.IsSet {
return cli.NewExitError("owner address flag was not set", 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
result, err := c.NEP11TokensOf(tokenHash.Uint160(), acc.Uint160())
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP11 `tokensOf` method: %s", err.Error()), 1)
}
for i := range result {
fmt.Fprintln(ctx.App.Writer, result[i])
}
return nil
}
func printNEP11Tokens(ctx *cli.Context) error {
var err error
tokenHash := ctx.Generic("token").(*flags.Address)
if !tokenHash.IsSet {
return cli.NewExitError("token contract hash was not set", 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
result, err := c.NEP11Tokens(tokenHash.Uint160())
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call optional NEP11 `tokens` method: %s", err.Error()), 1)
}
for i := range result {
fmt.Fprintln(ctx.App.Writer, result[i])
}
return nil
}
func printNEP11Properties(ctx *cli.Context) error {
var err error
tokenHash := ctx.Generic("token").(*flags.Address)
if !tokenHash.IsSet {
return cli.NewExitError("token contract hash was not set", 1)
}
tokenID := ctx.String("id")
if tokenID == "" {
return cli.NewExitError(errors.New("token ID should be specified"), 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
result, err := c.NEP11Properties(tokenHash.Uint160(), tokenID)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP11 `properties` method: %s", err.Error()), 1)
}
bytes, err := stackitem.ToJSON(result)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to convert result to JSON: %s", err), 1)
}
fmt.Fprintln(ctx.App.Writer, string(bytes))
return nil
}

View file

@ -13,6 +13,7 @@ import (
"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"
@ -27,10 +28,7 @@ var (
Name: "gas",
Usage: "Amount of GAS to attach to a tx",
}
)
func newNEP17Commands() []cli.Command {
balanceFlags := []cli.Flag{
baseBalanceFlags = []cli.Flag{
walletPathFlag,
tokenFlag,
flags.AddressFlag{
@ -38,16 +36,14 @@ func newNEP17Commands() []cli.Command {
Usage: "Address to use",
},
}
balanceFlags = append(balanceFlags, options.RPC...)
importFlags := []cli.Flag{
importFlags = append([]cli.Flag{
walletPathFlag,
flags.AddressFlag{
Name: "token",
Usage: "Token contract address or hash in LE",
},
}
importFlags = append(importFlags, options.RPC...)
transferFlags := []cli.Flag{
}, options.RPC...)
baseTransferFlags = []cli.Flag{
walletPathFlag,
outFlag,
fromAddrFlag,
@ -59,14 +55,21 @@ func newNEP17Commands() []cli.Command {
Usage: "Amount of asset to send",
},
}
transferFlags = append(transferFlags, options.RPC...)
multiTransferFlags := []cli.Flag{
multiTransferFlags = append([]cli.Flag{
walletPathFlag,
outFlag,
fromAddrFlag,
gasFlag,
}
multiTransferFlags = append(multiTransferFlags, options.RPC...)
}, options.RPC...)
)
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",
@ -89,10 +92,7 @@ func newNEP17Commands() []cli.Command {
Action: printNEP17Info,
Flags: []cli.Flag{
walletPathFlag,
cli.StringFlag{
Name: "token",
Usage: "Token name or hash",
},
tokenFlag,
},
},
{
@ -102,17 +102,14 @@ func newNEP17Commands() []cli.Command {
Action: removeNEP17Token,
Flags: []cli.Flag{
walletPathFlag,
cli.StringFlag{
Name: "token",
Usage: "Token name or hash",
},
tokenFlag,
forceFlag,
},
},
{
Name: "transfer",
Usage: "transfer NEP17 tokens",
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
Action: transferNEP17,
Flags: transferFlags,
Description: `Transfers specified NEP17 token amount with optional 'data' parameter and cosigners
@ -186,7 +183,7 @@ func getNEP17Balance(ctx *cli.Context) error {
var tokenName, tokenSymbol string
tokenDecimals := 0
asset := balances.Balances[i].Asset
token, err := getMatchingToken(ctx, wall, asset.StringLE())
token, err := getMatchingToken(ctx, wall, asset.StringLE(), manifest.NEP17StandardName)
if err != nil {
token, err = c.NEP17TokenInfo(asset)
}
@ -218,46 +215,65 @@ func getNEP17Balance(ctx *cli.Context) error {
return nil
}
func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string) (*wallet.Token, error) {
func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard string) (*wallet.Token, error) {
return getMatchingTokenAux(ctx, func(i int) *wallet.Token {
return w.Extra.Tokens[i]
}, len(w.Extra.Tokens), name)
}, len(w.Extra.Tokens), name, standard)
}
func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160, name string) (*wallet.Token, error) {
bs, err := c.GetNEP17Balances(addr)
if err != nil {
return nil, err
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
}
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)
}
func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string) (*wallet.Token, error) {
func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string, standard string) (*wallet.Token, error) {
var token *wallet.Token
var count int
for i := 0; i < n; i++ {
t := get(i)
if t != nil && (t.Hash.StringLE() == name || t.Address() == name || t.Symbol == name || t.Name == name) {
if t != nil && (t.Hash.StringLE() == name || t.Address() == name || t.Symbol == name || t.Name == name) && t.Standard == standard {
if count == 1 {
printTokenInfo(ctx, token)
printTokenInfo(ctx, t)
return nil, errors.New("multiple matching tokens found")
return nil, fmt.Errorf("multiple matching %s tokens found", standard)
}
count++
token = t
}
}
if count == 0 {
return nil, errors.New("token was not found")
return nil, fmt.Errorf("%s token was not found", standard)
}
return token, nil
}
func importNEP17Token(ctx *cli.Context) error {
return importNEPToken(ctx, manifest.NEP17StandardName)
}
func importNEPToken(ctx *cli.Context, standard string) error {
wall, err := openWallet(ctx.String("wallet"))
if err != nil {
return cli.NewExitError(err, 1)
@ -271,9 +287,9 @@ func importNEP17Token(ctx *cli.Context) error {
tokenHash := tokenHashFlag.Uint160()
for _, t := range wall.Extra.Tokens {
if t.Hash.Equals(tokenHash) {
if t.Hash.Equals(tokenHash) && t.Standard == standard {
printTokenInfo(ctx, t)
return cli.NewExitError("token already exists", 1)
return cli.NewExitError(fmt.Errorf("%s token already exists", standard), 1)
}
}
@ -285,7 +301,15 @@ func importNEP17Token(ctx *cli.Context) error {
return cli.NewExitError(err, 1)
}
tok, err := c.NEP17TokenInfo(tokenHash)
var tok *wallet.Token
switch standard {
case manifest.NEP17StandardName:
tok, err = c.NEP17TokenInfo(tokenHash)
case manifest.NEP11StandardName:
tok, err = c.NEP11TokenInfo(tokenHash)
default:
return cli.NewExitError(fmt.Sprintf("unsupported token standard: %s", standard), 1)
}
if err != nil {
return cli.NewExitError(fmt.Errorf("can't receive token info: %w", err), 1)
}
@ -305,9 +329,14 @@ func printTokenInfo(ctx *cli.Context, tok *wallet.Token) {
fmt.Fprintf(w, "Hash:\t%s\n", tok.Hash.StringLE())
fmt.Fprintf(w, "Decimals: %d\n", tok.Decimals)
fmt.Fprintf(w, "Address: %s\n", tok.Address())
fmt.Fprintf(w, "Standard:\t%s\n", tok.Standard)
}
func printNEP17Info(ctx *cli.Context) error {
return printNEPInfo(ctx, manifest.NEP17StandardName)
}
func printNEPInfo(ctx *cli.Context, standard string) error {
wall, err := openWallet(ctx.String("wallet"))
if err != nil {
return cli.NewExitError(err, 1)
@ -315,7 +344,7 @@ func printNEP17Info(ctx *cli.Context) error {
defer wall.Close()
if name := ctx.String("token"); name != "" {
token, err := getMatchingToken(ctx, wall, name)
token, err := getMatchingToken(ctx, wall, name, standard)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -323,23 +352,31 @@ func printNEP17Info(ctx *cli.Context) error {
return nil
}
for i, t := range wall.Extra.Tokens {
if i > 0 {
var count int
for _, t := range wall.Extra.Tokens {
if count > 0 {
fmt.Fprintln(ctx.App.Writer)
}
printTokenInfo(ctx, t)
if t.Standard == standard {
printTokenInfo(ctx, t)
count++
}
}
return nil
}
func removeNEP17Token(ctx *cli.Context) error {
return removeNEPToken(ctx, manifest.NEP17StandardName)
}
func removeNEPToken(ctx *cli.Context, standard string) error {
wall, err := openWallet(ctx.String("wallet"))
if err != nil {
return cli.NewExitError(err, 1)
}
defer wall.Close()
token, err := getMatchingToken(ctx, wall, ctx.String("token"))
token, err := getMatchingToken(ctx, wall, ctx.String("token"), standard)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -401,10 +438,10 @@ func multiTransferNEP17(ctx *cli.Context) error {
}
token, ok := cache[ss[0]]
if !ok {
token, err = getMatchingToken(ctx, wall, ss[0])
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)
}
@ -436,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)
@ -466,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"))
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
@ -494,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{{
Token: token.Hash,
Address: to,
Amount: amount.Int64(),
Data: data,
}}, cosignersAccounts)
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)

View file

@ -234,6 +234,11 @@ func NewCommands() []cli.Command {
Usage: "work with NEP17 contracts",
Subcommands: newNEP17Commands(),
},
{
Name: "nep11",
Usage: "work with NEP11 contracts",
Subcommands: newNEP11Commands(),
},
{
Name: "candidate",
Usage: "work with candidates",

View file

@ -25,6 +25,7 @@ const (
totalSupplyPrefix = "s"
accountPrefix = "a"
tokenPrefix = "t"
tokensPrefix = "ts"
)
var (
@ -76,6 +77,10 @@ func mkTokenKey(token []byte) []byte {
return append(res, token...)
}
func mkTokensKey() []byte {
return []byte(tokensPrefix)
}
// BalanceOf returns the number of tokens owned by specified address.
func BalanceOf(holder interop.Hash160) int {
if len(holder) != 20 {
@ -112,6 +117,36 @@ func setTokensOf(ctx storage.Context, holder interop.Hash160, tokens []string) {
}
}
// setTokens saves minted token if it is not saved yet.
func setTokens(ctx storage.Context, newToken string) {
key := mkTokensKey()
var tokens = []string{}
val := storage.Get(ctx, key)
if val != nil {
tokens = std.Deserialize(val.([]byte)).([]string)
}
for i := 0; i < len(tokens); i++ {
if util.Equals(tokens[i], newToken) {
return
}
}
tokens = append(tokens, newToken)
val = std.Serialize(tokens)
storage.Put(ctx, key, val)
}
// Tokens returns an iterator that contains all of the tokens minted by the contract.
func Tokens() iterator.Iterator {
ctx := storage.GetReadOnlyContext()
var arr = []string{}
key := mkTokensKey()
val := storage.Get(ctx, key)
if val != nil {
arr = std.Deserialize(val.([]byte)).([]string)
}
return iterator.Create(arr)
}
// TokensOf returns an iterator with all tokens held by specified address.
func TokensOf(holder interop.Hash160) iterator.Iterator {
if len(holder) != 20 {
@ -219,6 +254,7 @@ func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) {
toksOf = append(toksOf, token)
setTokensOf(ctx, from, toksOf)
setOwnerOf(ctx, []byte(token), from)
setTokens(ctx, token)
total++
storage.Put(ctx, []byte(totalSupplyPrefix), total)
@ -248,3 +284,28 @@ func Update(nef, manifest []byte) {
}
management.Update(nef, manifest)
}
// Properties returns properties of the given NFT.
func Properties(id []byte) map[string]string {
ctx := storage.GetReadOnlyContext()
var tokens = []string{}
key := mkTokensKey()
val := storage.Get(ctx, key)
if val != nil {
tokens = std.Deserialize(val.([]byte)).([]string)
}
var exists bool
for i := 0; i < len(tokens); i++ {
if util.Equals(tokens[i], id) {
exists = true
break
}
}
if !exists {
panic("unknown token")
}
result := map[string]string{
"name": "HASHY " + string(id),
}
return result
}

View file

@ -1,6 +1,6 @@
name: "HASHY NFT"
supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties"]
events:
- name: Transfer
parameters:

View file

@ -7,6 +7,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/rpc"
"gopkg.in/yaml.v2"
)
@ -49,6 +50,9 @@ func LoadFile(configPath string) (Config, error) {
ApplicationConfiguration: ApplicationConfiguration{
PingInterval: 30,
PingTimeout: 90,
RPC: rpc.Config{
MaxIteratorResultItems: 100,
},
},
}

View file

@ -95,3 +95,38 @@ func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) {
}
return st[index].(*stackitem.Map), nil
}
// topIterableFromStack returns top list of elements of `resultItemType` type from stack.
func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]interface{}, error) {
index := len(st) - 1 // top stack element is last in the array
if t := st[index].Type(); t != stackitem.InteropT {
return nil, fmt.Errorf("invalid return stackitem type: %s (InteropInterface expected)", t.String())
}
iter, ok := st[index].Value().(result.Iterator)
if !ok {
return nil, fmt.Errorf("failed to deserialize iterable from interop stackitem: invalid value type (Array expected)")
}
result := make([]interface{}, len(iter.Values))
for i := range iter.Values {
switch resultItemType.(type) {
case string:
bytes, err := iter.Values[i].TryBytes()
if err != nil {
return nil, fmt.Errorf("failed to deserialize string from stackitem #%d: %w", i, err)
}
result[i] = string(bytes)
case util.Uint160:
bytes, err := iter.Values[i].TryBytes()
if err != nil {
return nil, fmt.Errorf("failed to deserialize uint160 from stackitem #%d: %w", i, err)
}
result[i], err = util.Uint160DecodeBytesBE(bytes)
if err != nil {
return nil, fmt.Errorf("failed to decode uint160 from stackitem #%d: %w", i, err)
}
default:
return nil, errors.New("unsupported iterable type")
}
}
return result, nil
}

View file

@ -1,8 +1,11 @@
package client
import (
"fmt"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
// nepDecimals invokes `decimals` NEP* method on a specified contract.
@ -70,3 +73,30 @@ func (c *Client) nepBalanceOf(tokenHash, acc util.Uint160, tokenID *string) (int
return topIntFromStack(result.Stack)
}
// nepTokenInfo returns full NEP* token info.
func (c *Client) nepTokenInfo(tokenHash util.Uint160, standard string) (*wallet.Token, error) {
cs, err := c.GetContractStateByHash(tokenHash)
if err != nil {
return nil, err
}
var isStandardOK bool
for _, st := range cs.Manifest.SupportedStandards {
if st == standard {
isStandardOK = true
break
}
}
if !isStandardOK {
return nil, fmt.Errorf("token %s does not support %s standard", tokenHash.StringLE(), standard)
}
symbol, err := c.nepSymbol(tokenHash)
if err != nil {
return nil, err
}
decimals, err := c.nepDecimals(tokenHash)
if err != nil {
return nil, err
}
return wallet.NewToken(tokenHash, cs.Manifest.Name, symbol, decimals, standard), nil
}

View file

@ -8,6 +8,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
@ -35,6 +36,11 @@ func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) {
return c.nepBalanceOf(tokenHash, owner, nil)
}
// NEP11TokenInfo returns full NEP11 token info.
func (c *Client) NEP11TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
return c.nepTokenInfo(tokenHash, manifest.NEP11StandardName)
}
// TransferNEP11 creates an invocation transaction that invokes 'transfer' method
// on a given token to move the whole NEP11 token with the specified token ID to
// given account and sends it to the network returning just a hash of it.
@ -43,7 +49,7 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized
}
tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID)
tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID)
if err != nil {
return util.Uint256{}, err
}
@ -51,14 +57,14 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
return c.SignAndPushTx(tx, acc, cosigners)
}
// createNEP11TransferTx is an internal helper for TransferNEP11 and
// TransferNEP11D which creates an invocation transaction for the
// 'transfer' method of a given contract (token) to move the whole (or the
// specified amount of) NEP11 token with the specified token ID to given account
// and returns it. The returned transaction is not signed.
// CreateNEP11TransferTx creates an invocation transaction for the 'transfer'
// method of a given contract (token) to move the whole (or the specified amount
// of) NEP11 token with the specified token ID to given account and returns it.
// The returned transaction is not signed. CreateNEP11TransferTx is also a
// helper for TransferNEP11 and TransferNEP11D.
// `args` for TransferNEP11: to util.Uint160, tokenID string;
// `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string.
func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160,
func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160,
gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) {
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...)
@ -79,6 +85,33 @@ func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
}}, cosigners...))
}
// NEP11TokensOf returns an array of token IDs for the specified owner of the specified NFT token.
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) ([]string, error) {
result, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{
{
Type: smartcontract.Hash160Type,
Value: owner,
},
}, nil)
if err != nil {
return nil, err
}
err = getInvocationError(result)
if err != nil {
return nil, err
}
arr, err := topIterableFromStack(result.Stack, string(""))
if err != nil {
return nil, fmt.Errorf("failed to get token IDs from stack: %w", err)
}
ids := make([]string, len(arr))
for i := range ids {
ids[i] = arr[i].(string)
}
return ids, nil
}
// Non-divisible NFT methods section start.
// NEP11NDOwnerOf invokes `ownerOf` non-devisible NEP11 method with the
@ -118,7 +151,7 @@ func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160,
if err != nil {
return util.Uint256{}, fmt.Errorf("bad account address: %w", err)
}
tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, acc.Address, from, to, amount, tokenID)
tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, from, to, amount, tokenID)
if err != nil {
return util.Uint256{}, err
}
@ -132,6 +165,33 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID string)
return c.nepBalanceOf(tokenHash, owner, &tokenID)
}
// NEP11DOwnerOf returns list of the specified NEP11 divisible token owners.
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID string) ([]util.Uint160, error) {
result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: tokenID,
},
}, nil)
if err != nil {
return nil, err
}
err = getInvocationError(result)
if err != nil {
return nil, err
}
arr, err := topIterableFromStack(result.Stack, util.Uint160{})
if err != nil {
return nil, fmt.Errorf("failed to get token IDs from stack: %w", err)
}
owners := make([]util.Uint160, len(arr))
for i := range owners {
owners[i] = arr[i].(util.Uint160)
}
return owners, nil
}
// Divisible NFT methods section end.
// Optional NFT methods section start.
@ -154,4 +214,26 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID string) (*stack
return topMapFromStack(result.Stack)
}
// NEP11Tokens returns list of the tokens minted by the contract.
func (c *Client) NEP11Tokens(tokenHash util.Uint160) ([]string, error) {
result, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil)
if err != nil {
return nil, err
}
err = getInvocationError(result)
if err != nil {
return nil, err
}
arr, err := topIterableFromStack(result.Stack, string(""))
if err != nil {
return nil, fmt.Errorf("failed to get token IDs from stack: %w", err)
}
tokens := make([]string, len(arr))
for i := range tokens {
tokens[i] = arr[i].(string)
}
return tokens, nil
}
// Optional NFT methods section end.

View file

@ -7,6 +7,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
@ -50,19 +51,7 @@ func (c *Client) NEP17BalanceOf(tokenHash, acc util.Uint160) (int64, error) {
// NEP17TokenInfo returns full NEP17 token info.
func (c *Client) NEP17TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
cs, err := c.GetContractStateByHash(tokenHash)
if err != nil {
return nil, err
}
symbol, err := c.NEP17Symbol(tokenHash)
if err != nil {
return nil, err
}
decimals, err := c.NEP17Decimals(tokenHash)
if err != nil {
return nil, err
}
return wallet.NewToken(tokenHash, cs.Manifest.Name, symbol, decimals), nil
return c.nepTokenInfo(tokenHash, manifest.NEP17StandardName)
}
// CreateNEP17TransferTx creates an invocation transaction for the 'transfer'

View file

@ -2,20 +2,35 @@ package result
import (
"encoding/json"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// Invoke represents code invocation result and is used by several RPC calls
// that invoke functions, scripts and generic bytecode.
type Invoke struct {
State string
GasConsumed int64
Script []byte
Stack []stackitem.Item
FaultException string
Transaction *transaction.Transaction
State string
GasConsumed int64
Script []byte
Stack []stackitem.Item
FaultException string
Transaction *transaction.Transaction
maxIteratorResultItems int
}
// NewInvoke returns new Invoke structure with the given fields set.
func NewInvoke(vm *vm.VM, script []byte, faultException string, maxIteratorResultItems int) *Invoke {
return &Invoke{
State: vm.State().String(),
GasConsumed: vm.GasConsumed(),
Script: script,
Stack: vm.Estack().ToArray(),
FaultException: faultException,
maxIteratorResultItems: maxIteratorResultItems,
}
}
type invokeAux struct {
@ -27,15 +42,51 @@ type invokeAux struct {
Transaction []byte `json:"tx,omitempty"`
}
type iteratorAux struct {
Type string `json:"type"`
Value []json.RawMessage `json:"iterator"`
Truncated bool `json:"truncated"`
}
// Iterator represents deserialized VM iterator values with truncated flag.
type Iterator struct {
Values []stackitem.Item
Truncated bool
}
// MarshalJSON implements json.Marshaler.
func (r Invoke) MarshalJSON() ([]byte, error) {
var st json.RawMessage
arr := make([]json.RawMessage, len(r.Stack))
for i := range arr {
data, err := stackitem.ToJSONWithTypes(r.Stack[i])
if err != nil {
st = []byte(`"error: recursive reference"`)
break
var (
data []byte
err error
)
if (r.Stack[i].Type() == stackitem.InteropT) && vm.IsIterator(r.Stack[i]) {
iteratorValues, truncated := vm.IteratorValues(r.Stack[i], r.maxIteratorResultItems)
value := make([]json.RawMessage, len(iteratorValues))
for j := range iteratorValues {
value[j], err = stackitem.ToJSONWithTypes(iteratorValues[j])
if err != nil {
st = []byte(`"error: recursive reference"`)
break
}
}
data, err = json.Marshal(iteratorAux{
Type: stackitem.InteropT.String(),
Value: value,
Truncated: truncated,
})
if err != nil {
return nil, fmt.Errorf("failed to marshal iterator: %w", err)
}
} else {
data, err = stackitem.ToJSONWithTypes(r.Stack[i])
if err != nil {
st = []byte(`"error: recursive reference"`)
break
}
}
arr[i] = data
}
@ -76,6 +127,26 @@ func (r *Invoke) UnmarshalJSON(data []byte) error {
if err != nil {
break
}
if st[i].Type() == stackitem.InteropT {
iteratorAux := new(iteratorAux)
if json.Unmarshal(arr[i], iteratorAux) == nil {
iteratorValues := make([]stackitem.Item, len(iteratorAux.Value))
for j := range iteratorValues {
iteratorValues[j], err = stackitem.FromJSONWithTypes(iteratorAux.Value[j])
if err != nil {
err = fmt.Errorf("failed to unmarshal iterator values: %w", err)
break
}
}
// it's impossible to restore initial iterator type; also iterator is almost
// useless outside of the VM, thus let's replace it with a special structure.
st[i] = stackitem.NewInterop(Iterator{
Values: iteratorValues,
Truncated: iteratorAux.Truncated,
})
}
}
}
if err == nil {
r.Stack = st

View file

@ -12,9 +12,10 @@ type (
EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"`
// MaxGasInvoke is a maximum amount of gas which
// can be spent during RPC call.
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
Port uint16 `yaml:"Port"`
TLSConfig TLSConfig `yaml:"TLSConfig"`
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"`
Port uint16 `yaml:"Port"`
TLSConfig TLSConfig `yaml:"TLSConfig"`
}
// TLSConfig describes SSL/TLS configuration.

View file

@ -17,6 +17,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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"
@ -800,6 +801,17 @@ func TestClient_NEP11(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "NNS", sym)
})
t.Run("TokenInfo", func(t *testing.T) {
tok, err := c.NEP11TokenInfo(h)
require.NoError(t, err)
require.Equal(t, &wallet.Token{
Name: nativenames.NameService,
Hash: h,
Decimals: 0,
Symbol: "NNS",
Standard: manifest.NEP11StandardName,
}, tok)
})
t.Run("BalanceOf", func(t *testing.T) {
b, err := c.NEP11BalanceOf(h, acc)
require.NoError(t, err)

View file

@ -1320,14 +1320,7 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
if err != nil {
faultException = err.Error()
}
result := &result.Invoke{
State: vm.State().String(),
GasConsumed: vm.GasConsumed(),
Script: script,
Stack: vm.Estack().ToArray(),
FaultException: faultException,
}
return result, nil
return result.NewInvoke(vm, script, faultException, s.config.MaxIteratorResultItems), nil
}
// submitBlock broadcasts a raw block over the NEO network.

View file

@ -14,8 +14,6 @@ const (
// MaxManifestSize is a max length for a valid contract manifest.
MaxManifestSize = math.MaxUint16
// NEP10StandardName represents the name of NEP10 smartcontract standard.
NEP10StandardName = "NEP-10"
// NEP11StandardName represents the name of NEP11 smartcontract standard.
NEP11StandardName = "NEP-11"
// NEP17StandardName represents the name of NEP17 smartcontract standard.

View file

@ -71,6 +71,12 @@ func init() {
})
}
// IsIterator returns whether stackitem implements iterator interface.
func IsIterator(item stackitem.Item) bool {
_, ok := item.Value().(iterator)
return ok
}
// IteratorNext handles syscall System.Enumerator.Next.
func IteratorNext(v *VM) error {
iop := v.Estack().Pop().Interop()
@ -89,6 +95,18 @@ func IteratorValue(v *VM) error {
return nil
}
// IteratorValues returns an array of up to `max` iterator values. The second
// return parameter denotes whether iterator is truncated.
func IteratorValues(item stackitem.Item, max int) ([]stackitem.Item, bool) {
var result []stackitem.Item
arr := item.Value().(iterator)
for arr.Next() && max > 0 {
result = append(result, arr.Value())
max--
}
return result, arr.Next()
}
// NewIterator creates new iterator from the provided stack item.
func NewIterator(item stackitem.Item) (stackitem.Item, error) {
switch t := item.(type) {

View file

@ -11,15 +11,17 @@ type Token struct {
Hash util.Uint160 `json:"script_hash"`
Decimals int64 `json:"decimals"`
Symbol string `json:"symbol"`
Standard string `json:"standard"`
}
// NewToken returns new token contract info.
func NewToken(tokenHash util.Uint160, name, symbol string, decimals int64) *Token {
func NewToken(tokenHash util.Uint160, name, symbol string, decimals int64, standardName string) *Token {
return &Token{
Name: name,
Hash: tokenHash,
Decimals: decimals,
Symbol: symbol,
Standard: standardName,
}
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"testing"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
@ -13,7 +14,7 @@ func TestToken_MarshalJSON(t *testing.T) {
h, err := util.Uint160DecodeStringLE("f8d448b227991cf07cb96a6f9c0322437f1599b9")
require.NoError(t, err)
tok := NewToken(h, "NEP17 Standard", "NEP17", 8)
tok := NewToken(h, "NEP17 Standard", "NEP17", 8, manifest.NEP17StandardName)
require.Equal(t, "NEP17 Standard", tok.Name)
require.Equal(t, "NEP17", tok.Symbol)
require.EqualValues(t, 8, tok.Decimals)

View file

@ -8,6 +8,7 @@ import (
"testing"
"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/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -146,7 +147,7 @@ func removeWallet(t *testing.T, walletPath string) {
func TestWallet_AddToken(t *testing.T) {
w := checkWalletConstructor(t)
tok := NewToken(util.Uint160{1, 2, 3}, "Rubl", "RUB", 2)
tok := NewToken(util.Uint160{1, 2, 3}, "Rubl", "RUB", 2, manifest.NEP17StandardName)
require.Equal(t, 0, len(w.Extra.Tokens))
w.AddToken(tok)
require.Equal(t, 1, len(w.Extra.Tokens))