cli/wallet: modernize nep11 balances command, unify with nep17

Make NEP-11 code use getnep11balances the same way NEP-17 code uses
getnep17balances. This command was introduced well before getnep11balances
appeared, so it required always specifying contract explicitly. Now this
constraint can be relaxed somewhat in most cases.
This commit is contained in:
Roman Khimov 2022-08-29 22:39:13 +03:00
parent 7cfcf072b8
commit 1da4b333f6
4 changed files with 175 additions and 156 deletions

View file

@ -167,19 +167,35 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", wall,
"--address", nftOwnerAddr}
checkBalanceResult := func(t *testing.T, acc string, amount string) {
checkBalanceResult := func(t *testing.T, acc string, ids ...[]byte) {
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+"$")
// Hashes can be ordered in any way, so make a regexp for them.
var tokstring = "("
for i, id := range ids {
if i > 0 {
tokstring += "|"
}
tokstring += hex.EncodeToString(id)
}
tokstring += ")"
for range ids {
e.checkNextLine(t, "^\\s*Token: "+tokstring+"\\s*$")
e.checkNextLine(t, "^\\s*Amount: 1\\s*$")
e.checkNextLine(t, "^\\s*Updated: [0-9]+\\s*$")
}
e.checkEOF(t)
}
// balance check: by symbol, token is not imported
e.RunWithError(t, append(cmdCheckBalance, "--token", "HASHY")...)
e.Run(t, append(cmdCheckBalance, "--token", "HASHY")...)
checkBalanceResult(t, nftOwnerAddr, tokenID)
// balance check: excessive parameters
e.RunWithError(t, append(cmdCheckBalance, "--token", h.StringLE(), "neo-go")...)
// balance check: by hash, ok
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, "1")
checkBalanceResult(t, nftOwnerAddr, tokenID)
// import token
e.Run(t, "neo-go", "wallet", "nep11", "import",
@ -189,14 +205,14 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// balance check: by symbol, ok
e.Run(t, append(cmdCheckBalance, "--token", "HASHY")...)
checkBalanceResult(t, nftOwnerAddr, "1")
checkBalanceResult(t, nftOwnerAddr, tokenID)
// 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")
checkBalanceResult(t, nftOwnerAddr, tokenID)
// remove token from wallet
e.In.WriteString("y\r")
@ -276,7 +292,7 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// balance check: several tokens, ok
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, "2")
checkBalanceResult(t, nftOwnerAddr, tokenID, tokenID1)
cmdTransfer := []string{
"neo-go", "wallet", "nep11", "transfer",
@ -304,7 +320,7 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// check balance after transfer
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, "1") // tokenID1
checkBalanceResult(t, nftOwnerAddr, tokenID1)
// transfer: good, to NEP-11-Payable contract, with data
verifyH := deployVerifyContract(t, e)
@ -341,7 +357,7 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// check balance after transfer
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, "0")
checkBalanceResult(t, nftOwnerAddr)
}
func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {
@ -406,31 +422,42 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {
require.Equal(t, base64.StdEncoding.EncodeToString(object1ID.BytesBE()), props["objectID"])
e.checkEOF(t)
type idAmount struct {
id string
amount string
}
// check the balance
cmdCheckBalance := []string{"neo-go", "wallet", "nep11", "balance",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", wall,
"--address", validatorAddr}
checkBalanceResult := func(t *testing.T, acc string, amount string, id []byte) {
checkBalanceResult := func(t *testing.T, acc string, objs ...idAmount) {
e.checkNextLine(t, "^\\s*Account\\s+"+acc)
if id == nil {
e.checkNextLine(t, "^\\s*NFSO:\\s+NeoFS Object NFT \\("+h.StringLE()+"\\)")
} else {
e.checkNextLine(t, "^\\s*NFSO:\\s+NeoFS Object NFT \\("+h.StringLE()+", "+hex.EncodeToString(id)+"\\)")
e.checkNextLine(t, "^\\s*NFSO:\\s+NeoFS Object NFT \\("+h.StringLE()+"\\)")
for _, o := range objs {
e.checkNextLine(t, "^\\s*Token: "+o.id+"\\s*$")
e.checkNextLine(t, "^\\s*Amount: "+o.amount+"\\s*$")
e.checkNextLine(t, "^\\s*Updated: [0-9]+\\s*$")
}
e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+amount+"$")
e.checkEOF(t)
}
tokz := []idAmount{
{hex.EncodeToString(token1ID), "1"},
{hex.EncodeToString(token2ID), "1"},
}
// balance check: by symbol, token is not imported
e.RunWithError(t, append(cmdCheckBalance, "--token", "NFSO")...)
e.Run(t, append(cmdCheckBalance, "--token", "NFSO")...)
checkBalanceResult(t, validatorAddr, tokz...)
// overall NFSO balance check: by hash, ok
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, validatorAddr, "2", nil)
checkBalanceResult(t, validatorAddr, tokz...)
// particular NFSO balance check: by hash, ok
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE(), "--id", hex.EncodeToString(token2ID))...)
checkBalanceResult(t, validatorAddr, "1", token2ID)
checkBalanceResult(t, validatorAddr, tokz[1])
// import token
e.Run(t, "neo-go", "wallet", "nep11", "import",
@ -440,11 +467,11 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// overall balance check: by symbol, ok
e.Run(t, append(cmdCheckBalance, "--token", "NFSO")...)
checkBalanceResult(t, validatorAddr, "2", nil)
checkBalanceResult(t, validatorAddr, tokz...)
// particular balance check: by symbol, ok
e.Run(t, append(cmdCheckBalance, "--token", "NFSO", "--id", hex.EncodeToString(token1ID))...)
checkBalanceResult(t, validatorAddr, "1", token1ID)
checkBalanceResult(t, validatorAddr, tokz[0])
// remove token from wallet
e.In.WriteString("y\r")
@ -531,7 +558,7 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// balance check: several tokens, ok
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, validatorAddr, "2", nil)
checkBalanceResult(t, validatorAddr, tokz...)
cmdTransfer := []string{
"neo-go", "wallet", "nep11", "transfer",
@ -559,7 +586,7 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// check balance after transfer
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, validatorAddr, "1", nil) // only token2ID expected to be on the balance
checkBalanceResult(t, validatorAddr, tokz[1]) // only token2ID expected to be on the balance
// transfer: good, 1/4 of the balance, to NEP-11-Payable contract, with data
verifyH := deployVerifyContract(t, e)
@ -597,7 +624,8 @@ func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// check balance after transfer
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, validatorAddr, "0.75", nil)
tokz[1].amount = "0.75"
checkBalanceResult(t, validatorAddr, tokz[1])
}
func deployNFSContract(t *testing.T, e *executor) util.Uint160 {

View file

@ -4,7 +4,6 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"strconv"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
@ -12,10 +11,12 @@ import (
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/config"
"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/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"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"
@ -48,9 +49,24 @@ func newNEP11Commands() []cli.Command {
{
Name: "balance",
Usage: "get address balance",
UsageText: "balance -w wallet [--wallet-config path] --rpc-endpoint <node> [--timeout <time>] [--address <address>] --token <hash-or-name> [--id <token-id>]",
Action: getNEP11Balance,
Flags: balanceFlags,
UsageText: "balance -w wallet [--wallet-config path] --rpc-endpoint <node> [--timeout <time>] [--address <address>] [--token <hash-or-name>] [--id <token-id>]",
Description: `Prints NEP-11 balances for address and assets/IDs specified. By default (no
address or token parameter) all tokens (NFT contracts) for all accounts in
the specified wallet are listed with all tokens (actual NFTs) insied. A
single account can be chosen with the address option and/or a single NFT
contract can be selected with the token option. Further, you can specify a
particular NFT ID (hex-encoded) to display (which is mostly useful for
divisible NFTs). Tokens can be specified by hash, address, name or symbol.
Hashes and addresses always work (as long as they belong to a correct NEP-11
contract), while names or symbols are matched against the token data
stored in the wallet (see import command) or balance data returned from the
server. If the token is not specified directly (with hash/address) and is
not found in the wallet then depending on the balances data from the server
this command can print no data at all or print multiple tokens for one
account (if they use the same names/symbols).
`,
Action: getNEP11Balance,
Flags: balanceFlags,
},
{
Name: "import",
@ -162,95 +178,48 @@ func removeNEP11Token(ctx *cli.Context) error {
}
func getNEP11Balance(ctx *cli.Context) error {
var accounts []*wallet.Account
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
wall, _, err := readWallet(ctx)
if err != nil {
return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1)
}
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 {
tokenHash, err := flags.ParseAddress(name)
return getNEPBalance(ctx, manifest.NEP11StandardName, func(ctx *cli.Context, c *rpcclient.Client, addrHash util.Uint160, name string, token *wallet.Token, nftID string) error {
balances, err := c.GetNEP11Balances(addrHash)
if err != nil {
return cli.NewExitError(fmt.Errorf("can't fetch matching token from RPC-node: %w", err), 1)
return err
}
token, err = getTokenWithStandard(c, tokenHash, manifest.NEP11StandardName)
if err != nil {
return cli.NewExitError(err.Error(), 1)
var tokenFound bool
for i := range balances.Balances {
curToken := tokenFromNEP11Balance(&balances.Balances[i])
if tokenMatch(curToken, token, name) {
printNFTBalance(ctx, balances.Balances[i], nftID)
tokenFound = true
}
}
}
// Always initialize divisible token to be able to use both balanceOf methods.
n11 := nep11.NewDivisibleReader(invoker.New(c, nil), token.Hash)
tokenID := ctx.String("id")
tokenIDBytes, err := hex.DecodeString(tokenID)
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid tokenID bytes: %w", err), 1)
}
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 name == "" || tokenFound {
return nil
}
if k != 0 {
fmt.Fprintln(ctx.App.Writer)
}
fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)
var amount *big.Int
if len(tokenIDBytes) == 0 {
amount, err = n11.BalanceOf(addrHash)
if token != nil {
// We have an exact token, but there is no balance data for it -> print without NFTs.
printNFTBalance(ctx, result.NEP11AssetBalance{
Asset: token.Hash,
Decimals: int(token.Decimals),
Name: token.Name,
Symbol: token.Symbol,
}, "")
} else {
amount, err = n11.BalanceOfD(addrHash, tokenIDBytes)
// We have no data for this token at all, maybe it's not even correct -> complain.
fmt.Fprintf(ctx.App.Writer, "Can't find data for %q token\n", name)
}
if err != nil {
return nil
})
}
func printNFTBalance(ctx *cli.Context, balance result.NEP11AssetBalance, nftID string) {
fmt.Fprintf(ctx.App.Writer, "%s: %s (%s)\n", balance.Symbol, balance.Name, balance.Asset.StringLE())
for _, tok := range balance.Tokens {
if len(nftID) > 0 && nftID != tok.ID {
continue
}
amountStr := fixedn.ToString(amount, int(token.Decimals))
format := "%s: %s (%s)\n"
formatArgs := []interface{}{token.Symbol, token.Name, token.Hash.StringLE()}
if len(tokenIDBytes) != 0 {
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)
fmt.Fprintf(ctx.App.Writer, "\tToken: %s\n", tok.ID)
fmt.Fprintf(ctx.App.Writer, "\t\tAmount: %s\n", decimalAmount(tok.Amount, balance.Decimals))
fmt.Fprintf(ctx.App.Writer, "\t\tUpdated: %d\n", tok.LastUpdated)
}
return nil
}
func transferNEP11(ctx *cli.Context) error {

View file

@ -170,7 +170,49 @@ func newNEP17Commands() []cli.Command {
}
}
func tokenMatch(curToken *wallet.Token, expToken *wallet.Token, name string) bool {
return name == "" || // No specification at all, everything matches.
(expToken != nil && expToken.Hash == curToken.Hash) || // Exact token specification, matches perfectly.
(expToken == nil && name != "" && (curToken.Name == name || curToken.Symbol == name)) // Loose (named non-native) token specification, best-effort.
}
func getNEP17Balance(ctx *cli.Context) error {
return getNEPBalance(ctx, manifest.NEP17StandardName, func(ctx *cli.Context, c *rpcclient.Client, addrHash util.Uint160, name string, token *wallet.Token, _ string) error {
balances, err := c.GetNEP17Balances(addrHash)
if err != nil {
return err
}
var tokenFound bool
for i := range balances.Balances {
curToken := tokenFromNEP17Balance(&balances.Balances[i])
if tokenMatch(curToken, token, name) {
printAssetBalance(ctx, balances.Balances[i])
tokenFound = true
}
}
if name == "" || tokenFound {
return nil
}
if token != nil {
// We have an exact token, but there is no balance data for it -> print 0.
printAssetBalance(ctx, result.NEP17Balance{
Asset: token.Hash,
Amount: "0",
Decimals: int(token.Decimals),
LastUpdated: 0,
Name: token.Name,
Symbol: token.Symbol,
})
} else {
// We have no data for this token at all, maybe it's not even correct -> complain.
fmt.Fprintf(ctx.App.Writer, "Can't find data for %q token\n", name)
}
return nil
})
}
func getNEPBalance(ctx *cli.Context, standard string, accHandler func(*cli.Context, *rpcclient.Client, util.Uint160, string, *wallet.Token, string) error) error {
var accounts []*wallet.Account
if err := cmdargs.EnsureNone(ctx); err != nil {
@ -209,14 +251,14 @@ func getNEP17Balance(ctx *cli.Context) error {
if name != "" {
// Token was explicitly specified, let's try finding it, search in the wallet first.
token, err = getMatchingToken(ctx, wall, name, manifest.NEP17StandardName)
token, err = getMatchingToken(ctx, wall, name, standard)
if err != nil {
var h util.Uint160
// Well-known hardcoded names/symbols.
if name == nativenames.Neo || name == "NEO" {
if standard == manifest.NEP17StandardName && (name == nativenames.Neo || name == "NEO") {
h = neo.Hash
} else if name == nativenames.Gas || name == "GAS" {
} else if standard == manifest.NEP17StandardName && (name == nativenames.Gas || name == "GAS") {
h = gas.Hash
} else {
// The last resort, maybe it's a direct hash or address.
@ -227,74 +269,54 @@ func getNEP17Balance(ctx *cli.Context) error {
// in balances.
if !h.Equals(util.Uint160{}) {
// But if we have an exact hash, it must be correct.
token, err = getTokenWithStandard(c, h, manifest.NEP17StandardName)
token, err = getTokenWithStandard(c, h, standard)
if err != nil {
return cli.NewExitError(fmt.Errorf("%q is not a valid NEP-17 token: %w", name, err), 1)
}
}
}
}
tokenID := ctx.String("id")
if standard == manifest.NEP11StandardName {
if len(tokenID) > 0 {
_, err = hex.DecodeString(tokenID)
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid token ID: %w", err), 1)
}
}
}
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)
}
// We can't use nep17.BalanceOf() even if the token is known because of LastUpdated.
balances, err := c.GetNEP17Balances(addrHash)
if err != nil {
return cli.NewExitError(err, 1)
}
if k != 0 {
fmt.Fprintln(ctx.App.Writer)
}
fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)
var tokenFound bool
for i := range balances.Balances {
curToken := tokenFromNEP17Balance(&balances.Balances[i])
if token != nil && token.Hash != curToken.Hash {
continue
}
// Loose (named non-native) token specification, try our best.
if name != "" && !(curToken.Name == name || curToken.Symbol == name || curToken.Address() == name || curToken.Hash.StringLE() == name) {
continue
}
printAssetBalance(ctx, balances.Balances[i])
tokenFound = true
}
if name == "" || tokenFound {
continue
}
if token != nil {
// We have an exact token, but there is no balance data for it -> print 0.
printAssetBalance(ctx, result.NEP17Balance{
Asset: token.Hash,
Amount: "0",
Decimals: int(token.Decimals),
LastUpdated: 0,
Name: token.Name,
Symbol: token.Symbol,
})
} else {
// We have no data for this token at all, maybe it's not even correct -> complain.
fmt.Fprintf(ctx.App.Writer, "Can't find data for %q token\n", name)
err = accHandler(ctx, c, addrHash, name, token, tokenID)
if err != nil {
return cli.NewExitError(err, 1)
}
}
return nil
}
func printAssetBalance(ctx *cli.Context, balance result.NEP17Balance) {
fmt.Fprintf(ctx.App.Writer, "%s: %s (%s)\n", balance.Symbol, balance.Name, balance.Asset.StringLE())
amount := balance.Amount
if balance.Decimals != 0 {
func decimalAmount(amount string, decimals int) string {
if decimals != 0 {
b, ok := new(big.Int).SetString(amount, 10)
if ok {
amount = fixedn.ToString(b, balance.Decimals)
amount = fixedn.ToString(b, decimals)
}
}
fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", amount)
return amount
}
func printAssetBalance(ctx *cli.Context, balance result.NEP17Balance) {
fmt.Fprintf(ctx.App.Writer, "%s: %s (%s)\n", balance.Symbol, balance.Name, balance.Asset.StringLE())
fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", decimalAmount(balance.Amount, balance.Decimals))
fmt.Fprintf(ctx.App.Writer, "\tUpdated: %d\n", balance.LastUpdated)
}

View file

@ -456,13 +456,13 @@ commands with the following adjustments.
#### Balance
Specify token ID via `--id` flag to call divisible NEP-11 `balanceOf` method:
Specify token ID via `--id` flag to get data for a particular NFT:
```
./bin/neo-go wallet nep11 balance -w /etc/neo-go/wallet.json --token 67ecb7766dba4acf7c877392207984d1b4d15731 --id R5OREI5BU+Uyd23/MuV/xzI3F+Q= -r http://localhost:20332
./bin/neo-go wallet nep11 balance -w /etc/neo-go/wallet.json --token 67ecb7766dba4acf7c877392207984d1b4d15731 --id 7e244ffd6aa85fb1579d2ed22e9b761ab62e3486 -r http://localhost:20332
```
By default, no token ID specified, i.e. common `balanceOf` method is called.
By default, no token ID specified, i.e all NFTs returned by the server are listed.
#### Transfers