Merge pull request #3659 from nspcc-dev/make-balance-work-without-wallet

cli: make nep1X balance commands work without a wallet, fix #3275
This commit is contained in:
Anna Shaleva 2024-11-05 13:24:20 +03:00 committed by GitHub
commit cee296eb92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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)