cli: make nep1X balance commands work without a wallet, fix #3275

Checking the balance should be easy without any wallets involved, the data
is available anyway.

Tests are extended a bit as well.

Adding the command to "query nep1X" is not so trivial, so let's have it this
way for now.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
This commit is contained in:
Roman Khimov 2024-11-02 19:25:10 +03:00
parent 95098d4b25
commit ddaf9c01ab
3 changed files with 127 additions and 75 deletions

View file

@ -3,6 +3,7 @@ package nep_test
import ( import (
"io" "io"
"math/big" "math/big"
"os"
"path/filepath" "path/filepath"
"slices" "slices"
"strconv" "strconv"
@ -34,39 +35,99 @@ func TestNEP17Balance(t *testing.T) {
e.Run(t, args...) e.Run(t, args...)
e.CheckTxPersisted(t) e.CheckTxPersisted(t)
cmdbalance := []string{"neo-go", "wallet", "nep17", "balance"} var checkAcc1NEO = func(t *testing.T, e *testcli.Executor, line string) {
cmdbase := append(cmdbalance, if line == "" {
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0], line = e.GetNextLine(t)
"--wallet", testcli.TestWalletMultiPath,
)
cmd := append(cmdbase, "--address", testcli.TestWalletMultiAccount1)
t.Run("excessive parameters", func(t *testing.T) {
e.RunWithError(t, append(cmd, "--token", "NEO", "gas")...)
})
t.Run("NEO", func(t *testing.T) {
b, index := e.Chain.GetGoverningTokenBalance(testcli.TestWalletMultiAccount1Hash)
checkResult := func(t *testing.T) {
e.CheckNextLine(t, "^\\s*Account\\s+"+testcli.TestWalletMultiAccount1)
e.CheckNextLine(t, "^\\s*NEO:\\s+NeoToken \\("+e.Chain.GoverningTokenHash().StringLE()+"\\)")
e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+b.String()+"$")
e.CheckNextLine(t, "^\\s*Updated\\s*:\\s*"+strconv.FormatUint(uint64(index), 10))
e.CheckEOF(t)
} }
t.Run("Alias", func(t *testing.T) { balance, index := e.Chain.GetGoverningTokenBalance(testcli.TestWalletMultiAccount1Hash)
e.Run(t, append(cmd, "--token", "NEO")...) e.CheckLine(t, line, "^\\s*NEO:\\s+NeoToken \\("+e.Chain.GoverningTokenHash().StringLE()+"\\)")
checkResult(t) e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+balance.String()+"$")
}) e.CheckNextLine(t, "^\\s*Updated\\s*:\\s*"+strconv.FormatUint(uint64(index), 10))
t.Run("Hash", func(t *testing.T) { }
e.Run(t, append(cmd, "--token", e.Chain.GoverningTokenHash().StringLE())...) var checkAcc1GAS = func(t *testing.T, e *testcli.Executor, line string) {
checkResult(t) if line == "" {
}) line = e.GetNextLine(t)
}
e.CheckLine(t, line, "^\\s*GAS:\\s+GasToken \\("+e.Chain.UtilityTokenHash().StringLE()+"\\)")
balance := e.Chain.GetUtilityTokenBalance(testcli.TestWalletMultiAccount1Hash)
e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+fixedn.Fixed8(balance.Int64()).String()+"$")
e.CheckNextLine(t, "^\\s*Updated:")
}
var checkAcc1Assets = func(t *testing.T, e *testcli.Executor) {
e.CheckNextLine(t, "^Account "+testcli.TestWalletMultiAccount1)
// The order of assets is undefined.
for range 2 {
line := e.GetNextLine(t)
if strings.Contains(line, "GAS") {
checkAcc1GAS(t, e, line)
} else {
checkAcc1NEO(t, e, line)
}
}
}
var (
cmdbase = []string{"neo-go", "wallet", "nep17", "balance", "--rpc-endpoint", "http://" + e.RPC.Addresses()[0]}
addrparams = []string{"--address", testcli.TestWalletMultiAccount1}
walletparams = []string{"--wallet", testcli.TestWalletMultiPath}
)
t.Run("Bad wallet", func(t *testing.T) {
e.RunWithError(t, append(cmdbase, "--wallet", "/dev/null")...)
}) })
t.Run("GAS", func(t *testing.T) { t.Run("empty wallet", func(t *testing.T) {
e.Run(t, append(cmd, "--token", "GAS")...) tmpDir := t.TempDir()
e.CheckNextLine(t, "^\\s*Account\\s+"+testcli.TestWalletMultiAccount1) walletPath := filepath.Join(tmpDir, "emptywallet.json")
e.CheckNextLine(t, "^\\s*GAS:\\s+GasToken \\("+e.Chain.UtilityTokenHash().StringLE()+"\\)") require.NoError(t, os.WriteFile(walletPath, []byte("{}"), 0o644))
b := e.Chain.GetUtilityTokenBalance(testcli.TestWalletMultiAccount1Hash) e.RunWithError(t, append(cmdbase, "--wallet", walletPath)...)
e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+fixedn.Fixed8(b.Int64()).String()+"$") })
t.Run("no wallet or address", func(t *testing.T) {
e.RunWithError(t, cmdbase...)
})
for name, params := range map[string][]string{
"address only": addrparams,
"address with wallet": slices.Concat(walletparams, addrparams),
} {
var cmd = append(cmdbase, params...)
t.Run(name, func(t *testing.T) {
t.Run("all tokens", func(t *testing.T) {
e.Run(t, cmd...)
checkAcc1Assets(t, e)
e.CheckEOF(t)
})
t.Run("excessive parameters", func(t *testing.T) {
e.RunWithError(t, append(cmd, "--token", "NEO", "gas")...)
})
})
t.Run("NEO", func(t *testing.T) {
checkResult := func(t *testing.T) {
e.CheckNextLine(t, "^\\s*Account\\s+"+testcli.TestWalletMultiAccount1)
checkAcc1NEO(t, e, "")
e.CheckEOF(t)
}
t.Run("Alias", func(t *testing.T) {
e.Run(t, append(cmd, "--token", "NEO")...)
checkResult(t)
})
t.Run("Hash", func(t *testing.T) {
e.Run(t, append(cmd, "--token", e.Chain.GoverningTokenHash().StringLE())...)
checkResult(t)
})
})
t.Run("GAS", func(t *testing.T) {
e.Run(t, append(cmd, "--token", "GAS")...)
e.CheckNextLine(t, "^\\s*Account\\s+"+testcli.TestWalletMultiAccount1)
checkAcc1GAS(t, e, "")
})
t.Run("Bad token", func(t *testing.T) {
e.Run(t, append(cmd, "--token", "kek")...)
e.CheckNextLine(t, "^\\s*Account\\s+"+testcli.TestWalletMultiAccount1)
e.CheckNextLine(t, `^\s*Can't find data for "kek" token\s*`)
e.CheckEOF(t)
})
}
t.Run("inexistent wallet account", func(t *testing.T) {
var cmd = append(cmdbase, walletparams...)
e.RunWithError(t, append(cmd, "--address", "NSPCCpw8YmgNDYWiBfXJHRfz38NDjv6WW3")...)
}) })
t.Run("zero balance of known token", func(t *testing.T) { t.Run("zero balance of known token", func(t *testing.T) {
e.Run(t, append(cmdbase, []string{"--token", "NEO", "--address", testcli.TestWalletMultiAccount2}...)...) e.Run(t, append(cmdbase, []string{"--token", "NEO", "--address", testcli.TestWalletMultiAccount2}...)...)
@ -77,24 +138,9 @@ func TestNEP17Balance(t *testing.T) {
e.CheckEOF(t) e.CheckEOF(t)
}) })
t.Run("all accounts", func(t *testing.T) { t.Run("all accounts", func(t *testing.T) {
e.Run(t, cmdbase...) e.Run(t, append(cmdbase, walletparams...)...)
e.CheckNextLine(t, "^Account "+testcli.TestWalletMultiAccount1) checkAcc1Assets(t, e)
// The order of assets is undefined.
for range 2 {
line := e.GetNextLine(t)
if strings.Contains(line, "GAS") {
e.CheckLine(t, line, "^\\s*GAS:\\s+GasToken \\("+e.Chain.UtilityTokenHash().StringLE()+"\\)")
balance := e.Chain.GetUtilityTokenBalance(testcli.TestWalletMultiAccount1Hash)
e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+fixedn.Fixed8(balance.Int64()).String()+"$")
e.CheckNextLine(t, "^\\s*Updated:")
} else {
balance, index := e.Chain.GetGoverningTokenBalance(testcli.TestWalletMultiAccount1Hash)
e.CheckLine(t, line, "^\\s*NEO:\\s+NeoToken \\("+e.Chain.GoverningTokenHash().StringLE()+"\\)")
e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+balance.String()+"$")
e.CheckNextLine(t, "^\\s*Updated\\s*:\\s*"+strconv.FormatUint(uint64(index), 10))
}
}
e.CheckNextLine(t, "^\\s*$") e.CheckNextLine(t, "^\\s*$")
e.CheckNextLine(t, "^Account "+testcli.TestWalletMultiAccount2) e.CheckNextLine(t, "^Account "+testcli.TestWalletMultiAccount2)
@ -107,15 +153,6 @@ func TestNEP17Balance(t *testing.T) {
e.CheckNextLine(t, "^\\s*Updated:") e.CheckNextLine(t, "^\\s*Updated:")
e.CheckEOF(t) e.CheckEOF(t)
}) })
t.Run("Bad token", func(t *testing.T) {
e.Run(t, append(cmd, "--token", "kek")...)
e.CheckNextLine(t, "^\\s*Account\\s+"+testcli.TestWalletMultiAccount1)
e.CheckNextLine(t, `^\s*Can't find data for "kek" token\s*`)
e.CheckEOF(t)
})
t.Run("Bad wallet", func(t *testing.T) {
e.RunWithError(t, append(cmdbalance, "--wallet", "/dev/null", "-r", "test")...)
})
} }
func TestNEP17Transfer(t *testing.T) { func TestNEP17Transfer(t *testing.T) {

View file

@ -51,9 +51,10 @@ func newNEP11Commands() []*cli.Command {
{ {
Name: "balance", Name: "balance",
Usage: "Get address 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>]", 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 Description: `Prints NEP-11 balances for address and assets/IDs specified. One of wallet
address or token parameter) all tokens (NFT contracts) for all accounts in or address must be specified, passing both is valid too. If a wallet is
given without an address all tokens (NFT contracts) for all accounts in
the specified wallet are listed with all tokens (actual NFTs) insied. A 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 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 contract can be selected with the token option. Further, you can specify a

View file

@ -101,10 +101,11 @@ func newNEP17Commands() []*cli.Command {
{ {
Name: "balance", Name: "balance",
Usage: "Get address balance", Usage: "Get address balance",
UsageText: "balance -w wallet [--wallet-config path] --rpc-endpoint <node> [--timeout <time>] [--address <address>] [--token <hash-or-name>]", UsageText: "balance [-w wallet] [--wallet-config path] --rpc-endpoint <node> [--timeout <time>] [--address <address>] [--token <hash-or-name>]",
Description: `Prints NEP-17 balances for address and tokens specified. By default (no Description: `Prints NEP-17 balances for address and tokens specified. One of wallet
address or token parameter) all tokens for all accounts in the specified wallet or address must be specified, passing both is valid too. If a wallet is
are listed. A single account can be chosen with the address option and/or a given without an address all tokens for all accounts in this wallet are
listed. A single account can be chosen with the address option and/or a
single token can be selected with the token option. Tokens can be specified single token can be selected with the token option. Tokens can be specified
by hash, address, name or symbol. Hashes and addresses always work (as long by hash, address, name or symbol. Hashes and addresses always work (as long
as they belong to a correct NEP-17 contract), while names or symbols (if as they belong to a correct NEP-17 contract), while names or symbols (if
@ -217,30 +218,40 @@ func getNEP17Balance(ctx *cli.Context) error {
} }
func getNEPBalance(ctx *cli.Context, standard string, accHandler func(*cli.Context, *rpcclient.Client, util.Uint160, string, *wallet.Token, string) error) error { 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 var addresses []util.Uint160
if err := cmdargs.EnsureNone(ctx); err != nil { if err := cmdargs.EnsureNone(ctx); err != nil {
return err return err
} }
wall, _, err := readWallet(ctx) wall, _, err := readWallet(ctx)
if err != nil { if err != nil {
return cli.Exit(fmt.Errorf("bad wallet: %w", err), 1) if !errors.Is(err, errNoPath) {
return cli.Exit(fmt.Errorf("bad wallet: %w", err), 1)
}
} else {
defer wall.Close()
} }
defer wall.Close()
addrFlag := ctx.Generic("address").(*flags.Address) addrFlag := ctx.Generic("address").(*flags.Address)
if addrFlag.IsSet { if addrFlag.IsSet {
addrHash := addrFlag.Uint160() addrHash := addrFlag.Uint160()
acc := wall.GetAccount(addrHash) if wall != nil {
if acc == nil { acc := wall.GetAccount(addrHash)
return cli.Exit(fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addrHash)), 1) if acc == nil {
return cli.Exit(fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addrHash)), 1)
}
} }
accounts = append(accounts, acc) addresses = append(addresses, addrHash)
} else { } else {
if wall == nil {
return cli.Exit(errors.New("neither wallet nor address specified"), 1)
}
if len(wall.Accounts) == 0 { if len(wall.Accounts) == 0 {
return cli.Exit(errors.New("no accounts in the wallet"), 1) return cli.Exit(errors.New("no accounts in the wallet"), 1)
} }
accounts = wall.Accounts for _, acc := range wall.Accounts {
addresses = append(addresses, acc.ScriptHash())
}
} }
gctx, cancel := options.GetTimeoutContext(ctx) gctx, cancel := options.GetTimeoutContext(ctx)
@ -290,13 +301,13 @@ func getNEPBalance(ctx *cli.Context, standard string, accHandler func(*cli.Conte
} }
} }
} }
for k, acc := range accounts { for k, addr := range addresses {
if k != 0 { if k != 0 {
fmt.Fprintln(ctx.App.Writer) fmt.Fprintln(ctx.App.Writer)
} }
fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address) fmt.Fprintf(ctx.App.Writer, "Account %s\n", address.Uint160ToString(addr))
err = accHandler(ctx, c, acc.ScriptHash(), name, token, tokenID) err = accHandler(ctx, c, addr, name, token, tokenID)
if err != nil { if err != nil {
return cli.Exit(err, 1) return cli.Exit(err, 1)
} }
@ -321,6 +332,9 @@ func printAssetBalance(ctx *cli.Context, balance result.NEP17Balance) {
} }
func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard string) (*wallet.Token, error) { func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard string) (*wallet.Token, error) {
if w == nil {
return getMatchingTokenAux(ctx, nil, 0, name, standard)
}
return getMatchingTokenAux(ctx, func(i int) *wallet.Token { return getMatchingTokenAux(ctx, func(i int) *wallet.Token {
return w.Extra.Tokens[i] return w.Extra.Tokens[i]
}, len(w.Extra.Tokens), name, standard) }, len(w.Extra.Tokens), name, standard)