Merge pull request #2667 from nspcc-dev/rpc-nep-token-info

Drop NEPXXTokenInfo from the RPC client
This commit is contained in:
Roman Khimov 2022-08-30 14:41:30 +03:00 committed by GitHub
commit 314cd3341b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 493 additions and 234 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)+"\\)")
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

@ -106,6 +106,7 @@ func TestNEP17Balance(t *testing.T) {
t.Run("Bad token", func(t *testing.T) {
e.Run(t, append(cmd, "--token", "kek")...)
e.checkNextLine(t, "^\\s*Account\\s+"+validatorAddr)
e.checkNextLine(t, `^\s*Can't find data for "kek" token\s*`)
e.checkEOF(t)
})
t.Run("Bad wallet", func(t *testing.T) {

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,7 +49,22 @@ 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>]",
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,
},
@ -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 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 err
}
wall, _, err := readWallet(ctx)
if err != nil {
return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 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
}
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)
if name == "" || tokenFound {
return nil
}
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 {
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)
if err != nil {
return cli.NewExitError(fmt.Errorf("can't fetch matching token from RPC-node: %w", err), 1)
}
token, err = c.NEP11TokenInfo(tokenHash)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
}
// 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 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)
} else {
amount, err = n11.BalanceOfD(addrHash, tokenIDBytes)
}
if err != nil {
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)
// 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 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
}
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)
}
}
func transferNEP11(ctx *cli.Context) error {

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"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"
@ -20,10 +21,10 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -99,6 +100,19 @@ func newNEP17Commands() []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>]",
Description: `Prints NEP-17 balances for address and tokens specified. By default (no
address or token parameter) all tokens for all accounts in the specified 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
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
they're not NEO or GAS names/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: getNEP17Balance,
Flags: balanceFlags,
},
@ -156,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 {
@ -191,107 +247,76 @@ func getNEP17Balance(ctx *cli.Context) error {
}
name := ctx.String("token")
var token *wallet.Token
if name != "" {
// Token was explicitly specified, let's try finding it, search in the wallet first.
token, err = getMatchingToken(ctx, wall, name, standard)
if err != nil {
var h util.Uint160
// Well-known hardcoded names/symbols.
if standard == manifest.NEP17StandardName && (name == nativenames.Neo || name == "NEO") {
h = neo.Hash
} 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.
h, _ = flags.ParseAddress(name)
}
// If the hash is not found then it's some kind of named token, there is
// no way for us to find it, but it's not an error, maybe we'll find it
// in balances.
if !h.Equals(util.Uint160{}) {
// But if we have an exact hash, it must be correct.
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)
}
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 {
var tokenName, tokenSymbol string
tokenDecimals := 0
asset := balances.Balances[i].Asset
token, err := getMatchingToken(ctx, wall, asset.StringLE(), manifest.NEP17StandardName)
err = accHandler(ctx, c, addrHash, name, token, tokenID)
if err != nil {
token, err = c.NEP17TokenInfo(asset)
return cli.NewExitError(err, 1)
}
if err == nil {
if name != "" && !(token.Name == name || token.Symbol == name || token.Address() == name || token.Hash.StringLE() == name) {
continue
}
tokenName = token.Name
tokenSymbol = token.Symbol
tokenDecimals = int(token.Decimals)
tokenFound = true
} else {
if name != "" {
continue
}
tokenSymbol = "UNKNOWN"
}
printAssetBalance(ctx, asset, tokenName, tokenSymbol, tokenDecimals, balances.Balances[i])
}
if name == "" || tokenFound {
continue
}
// Token was explicitly specified, but was not found among balances, thus either balance is 0
// or the token doesn't exist. Try to find token by its address/hash/name/symbol and print zero
// balance if found. Search into wallet first.
token, err := getMatchingToken(ctx, wall, name, manifest.NEP17StandardName)
if err != nil {
// The wallet doesn't contain specified token, so try to ask chain.
h, err := flags.ParseAddress(name)
if err != nil {
h, err = c.GetNativeContractHash(name)
if err != nil {
// Try to get native NEP17 with matching symbol.
var gasSymbol, neoSymbol string
g := gas.NewReader(invoker.New(c, nil))
gasSymbol, err = g.Symbol()
if err != nil {
continue
}
if gasSymbol != name {
n := neo.NewReader(invoker.New(c, nil))
neoSymbol, err = n.Symbol()
if err != nil {
continue
}
if neoSymbol != name {
continue
} else {
h = neo.Hash
}
} else {
h = gas.Hash
}
}
}
token, err = c.NEP17TokenInfo(h)
if err != nil {
continue
}
}
printAssetBalance(ctx, token.Hash, token.Name, token.Symbol, int(token.Decimals), result.NEP17Balance{
Asset: token.Hash,
Amount: "0",
LastUpdated: 0,
})
}
return nil
}
func printAssetBalance(ctx *cli.Context, asset util.Uint160, tokenName, tokenSymbol string, tokenDecimals int, balance result.NEP17Balance) {
fmt.Fprintf(ctx.App.Writer, "%s: %s (%s)\n", tokenSymbol, tokenName, asset.StringLE())
amount := balance.Amount
if tokenDecimals != 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, tokenDecimals)
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)
}
@ -301,6 +326,14 @@ func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard
}, len(w.Extra.Tokens), name, standard)
}
func tokenFromNEP17Balance(bal *result.NEP17Balance) *wallet.Token {
return wallet.NewToken(bal.Asset, bal.Name, bal.Symbol, int64(bal.Decimals), manifest.NEP17StandardName)
}
func tokenFromNEP11Balance(bal *result.NEP11AssetBalance) *wallet.Token {
return wallet.NewToken(bal.Asset, bal.Name, bal.Symbol, int64(bal.Decimals), manifest.NEP11StandardName)
}
func getMatchingTokenRPC(ctx *cli.Context, c *rpcclient.Client, addr util.Uint160, name string, standard string) (*wallet.Token, error) {
switch standard {
case manifest.NEP17StandardName:
@ -309,20 +342,18 @@ func getMatchingTokenRPC(ctx *cli.Context, c *rpcclient.Client, addr util.Uint16
return nil, err
}
get := func(i int) *wallet.Token {
t, _ := c.NEP17TokenInfo(bs.Balances[i].Asset)
return t
return tokenFromNEP17Balance(&bs.Balances[i])
}
return getMatchingTokenAux(ctx, get, len(bs.Balances), name, standard)
case manifest.NEP11StandardName:
tokenHash, err := flags.ParseAddress(name)
bs, err := c.GetNEP11Balances(addr)
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())
return nil, err
}
get := func(i int) *wallet.Token {
t, _ := c.NEP11TokenInfo(tokenHash)
return t
return tokenFromNEP11Balance(&bs.Balances[i])
}
return getMatchingTokenAux(ctx, get, 1, name, standard)
return getMatchingTokenAux(ctx, get, len(bs.Balances), name, standard)
default:
return nil, fmt.Errorf("unsupported %s token", standard)
}
@ -383,15 +414,7 @@ func importNEPToken(ctx *cli.Context, standard string) error {
return cli.NewExitError(err, 1)
}
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)
}
tok, err := getTokenWithStandard(c, tokenHash, standard)
if err != nil {
return cli.NewExitError(fmt.Errorf("can't receive token info: %w", err), 1)
}
@ -404,6 +427,17 @@ func importNEPToken(ctx *cli.Context, standard string) error {
return nil
}
func getTokenWithStandard(c *rpcclient.Client, hash util.Uint160, std string) (*wallet.Token, error) {
token, err := neptoken.Info(c, hash)
if err != nil {
return nil, err
}
if token.Standard != std {
return nil, fmt.Errorf("%s is not a %s token", hash.StringLE(), std)
}
return token, err
}
func printTokenInfo(ctx *cli.Context, tok *wallet.Token) {
w := ctx.App.Writer
fmt.Fprintf(w, "Name:\t%s\n", tok.Name)

View file

@ -403,9 +403,10 @@ Getting balance is easy:
./bin/neo-go wallet nep17 balance -w /etc/neo-go/wallet.json -r http://localhost:20332
```
By default, you'll get data for all tokens for the default wallet's
address. You can select non-default address with `-a` flag and/or select token
with `--token` flag (token hash or name can be used as parameter).
By default, you'll get data for all tokens that are owned by all accounts
stored in the given wallet. You can specify a particular address with `-a`
flag and/or select token with `--token` flag (token hash, address, name or
symbol can be used as a parameter).
#### Transfers
@ -455,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

View file

@ -49,6 +49,9 @@ func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) {
}
// NEP11TokenInfo returns full NEP-11 token info.
//
// Deprecated: please use Info method from the neptoken subpackage. This method
// will be removed in future versions.
func (c *Client) NEP11TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
return c.nepTokenInfo(tokenHash, manifest.NEP11StandardName)
}

View file

@ -86,12 +86,6 @@ func NewBase(actor Actor, hash util.Uint160) *Base {
return &Base{*NewBaseReader(actor, hash), actor}
}
// BalanceOf returns the number of NFTs owned by the given account. For divisible
// NFTs that's the sum of all parts of tokens.
func (t *BaseReader) BalanceOf(account util.Uint160) (*big.Int, error) {
return unwrap.BigInt(t.invoker.Call(t.hash, "balanceOf", account))
}
// Properties returns a set of token's properties such as name or URL. The map
// is returned as is from this method (stack item) for maximum flexibility,
// contracts can return a lot of specific data there. Most of the time though

View file

@ -61,6 +61,9 @@ func (c *Client) NEP17BalanceOf(tokenHash, acc util.Uint160) (int64, error) {
}
// NEP17TokenInfo returns full NEP-17 token info.
//
// Deprecated: please use Info method from the neptoken subpackage. This method
// will be removed in future versions.
func (c *Client) NEP17TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
return c.nepTokenInfo(tokenHash, manifest.NEP17StandardName)
}

View file

@ -12,7 +12,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
)
@ -35,15 +34,13 @@ type Actor interface {
// used to query various data.
type TokenReader struct {
neptoken.Base
invoker Invoker
hash util.Uint160
}
// Token provides full NEP-17 interface, both safe and state-changing methods.
type Token struct {
TokenReader
hash util.Uint160
actor Actor
}
@ -62,21 +59,16 @@ type TransferParameters struct {
Data interface{}
}
// NewReader creates an instance of TokenReader for contract with the given hash
// using the given Invoker.
// NewReader creates an instance of TokenReader for contract with the given
// hash using the given Invoker.
func NewReader(invoker Invoker, hash util.Uint160) *TokenReader {
return &TokenReader{*neptoken.New(invoker, hash), invoker, hash}
return &TokenReader{*neptoken.New(invoker, hash)}
}
// New creates an instance of Token for contract with the given hash
// using the given Actor.
func New(actor Actor, hash util.Uint160) *Token {
return &Token{*NewReader(actor, hash), actor}
}
// BalanceOf returns the token balance of the given account.
func (t *TokenReader) BalanceOf(account util.Uint160) (*big.Int, error) {
return unwrap.BigInt(t.invoker.Call(t.hash, "balanceOf", account))
return &Token{*NewReader(actor, hash), hash, actor}
}
// Transfer creates and sends a transaction that performs a `transfer` method

View file

@ -61,3 +61,12 @@ func (b *Base) Symbol() (string, error) {
func (b *Base) TotalSupply() (*big.Int, error) {
return unwrap.BigInt(b.invoker.Call(b.hash, "totalSupply"))
}
// BalanceOf returns the token balance of the given account. For NEP-17 that's
// the token balance with decimals (1 TOK with 2 decimals will lead to 100
// returned from this method). For non-divisible NEP-11 that's the number of
// NFTs owned by the account, for divisible NEP-11 that's the sum of the parts
// of all NFTs owned by the account.
func (b *Base) BalanceOf(account util.Uint160) (*big.Int, error) {
return unwrap.BigInt(b.invoker.Call(b.hash, "balanceOf", account))
}

View file

@ -31,6 +31,8 @@ func TestBaseErrors(t *testing.T) {
require.Error(t, err)
_, err = base.TotalSupply()
require.Error(t, err)
_, err = base.BalanceOf(util.Uint160{1, 2, 3})
require.Error(t, err)
ti.err = nil
ti.res = &result.Invoke{
@ -43,6 +45,8 @@ func TestBaseErrors(t *testing.T) {
require.Error(t, err)
_, err = base.TotalSupply()
require.Error(t, err)
_, err = base.BalanceOf(util.Uint160{1, 2, 3})
require.Error(t, err)
ti.res = &result.Invoke{
State: "HALT",
@ -53,6 +57,8 @@ func TestBaseErrors(t *testing.T) {
require.Error(t, err)
_, err = base.TotalSupply()
require.Error(t, err)
_, err = base.BalanceOf(util.Uint160{1, 2, 3})
require.Error(t, err)
}
func TestBaseDecimals(t *testing.T) {
@ -135,3 +141,27 @@ func TestBaseTotalSupply(t *testing.T) {
_, err = base.TotalSupply()
require.Error(t, err)
}
func TestBaseBalanceOf(t *testing.T) {
ti := new(testInv)
base := New(ti, util.Uint160{1, 2, 3})
ti.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(100500),
},
}
bal, err := base.BalanceOf(util.Uint160{1, 2, 3})
require.NoError(t, err)
require.Equal(t, big.NewInt(100500), bal)
ti.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = base.BalanceOf(util.Uint160{1, 2, 3})
require.Error(t, err)
}

View file

@ -0,0 +1,47 @@
package neptoken
import (
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"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"
)
// InfoClient is a set of RPC methods required to get all of the NEP-11/NEP-17
// token data.
type InfoClient interface {
invoker.RPCInvoke
GetContractStateByHash(hash util.Uint160) (*state.Contract, error)
}
// Info allows to get basic token info using RPC client.
func Info(c InfoClient, hash util.Uint160) (*wallet.Token, error) {
cs, err := c.GetContractStateByHash(hash)
if err != nil {
return nil, err
}
var standard string
for _, st := range cs.Manifest.SupportedStandards {
if st == manifest.NEP17StandardName || st == manifest.NEP11StandardName {
standard = st
break
}
}
if standard == "" {
return nil, fmt.Errorf("contract %s is not NEP-11/NEP17", hash.StringLE())
}
b := New(invoker.New(c, nil), hash)
symbol, err := b.Symbol()
if err != nil {
return nil, err
}
decimals, err := b.Decimals()
if err != nil {
return nil, err
}
return wallet.NewToken(hash, cs.Manifest.Name, symbol, int64(decimals), standard), nil
}

View file

@ -0,0 +1,147 @@
package neptoken
import (
"errors"
"testing"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"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/stretchr/testify/require"
)
type rpcClient struct {
cnt int
cserr error
cs *state.Contract
inverrs []error
invs []*result.Invoke
}
func (r *rpcClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
panic("not implemented")
}
func (r *rpcClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
e, i := r.inverrs[r.cnt], r.invs[r.cnt]
r.cnt = (r.cnt + 1) % len(r.invs)
return i, e
}
func (r *rpcClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
panic("not implemented")
}
func (r *rpcClient) TerminateSession(sessionID uuid.UUID) (bool, error) {
panic("not implemented")
}
func (r *rpcClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
panic("not implemented")
}
func (r *rpcClient) GetContractStateByHash(hash util.Uint160) (*state.Contract, error) {
return r.cs, r.cserr
}
func TestInfo(t *testing.T) {
c := &rpcClient{}
hash := util.Uint160{1, 2, 3}
// Error on contract state.
c.cserr = errors.New("")
_, err := Info(c, hash)
require.Error(t, err)
// Error on missing standard.
c.cserr = nil
c.cs = &state.Contract{
ContractBase: state.ContractBase{
Manifest: manifest.Manifest{
Name: "Vasiliy",
SupportedStandards: []string{"RFC 1149"},
},
},
}
_, err = Info(c, hash)
require.Error(t, err)
// Error on Symbol()
c.cs = &state.Contract{
ContractBase: state.ContractBase{
Manifest: manifest.Manifest{
Name: "Übertoken",
SupportedStandards: []string{"NEP-17"},
},
},
}
c.inverrs = []error{errors.New(""), nil}
c.invs = []*result.Invoke{nil, nil}
_, err = Info(c, hash)
require.Error(t, err)
// Error on Decimals()
c.cnt = 0
c.inverrs[0], c.inverrs[1] = c.inverrs[1], c.inverrs[0]
c.invs[0] = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make("UBT"),
},
}
_, err = Info(c, hash)
require.Error(t, err)
// OK
c.cnt = 0
c.inverrs[1] = nil
c.invs[1] = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(8),
},
}
ti, err := Info(c, hash)
require.NoError(t, err)
require.Equal(t, &wallet.Token{
Name: "Übertoken",
Hash: hash,
Decimals: 8,
Symbol: "UBT",
Standard: "NEP-17",
}, ti)
// NEP-11
c.cs = &state.Contract{
ContractBase: state.ContractBase{
Manifest: manifest.Manifest{
Name: "NFTizer",
SupportedStandards: []string{"NEP-11"},
},
},
}
c.cnt = 0
c.inverrs[1] = nil
c.invs[0] = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make("NZ"),
},
}
c.invs[1] = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(0),
},
}
ti, err = Info(c, hash)
require.NoError(t, err)
require.Equal(t, &wallet.Token{
Name: "NFTizer",
Hash: hash,
Decimals: 0,
Symbol: "NZ",
Standard: "NEP-11",
}, ti)
}

View file

@ -36,6 +36,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/oracle"
@ -83,7 +84,7 @@ func TestClient_NEP17(t *testing.T) {
require.Equal(t, "RUB", sym)
})
t.Run("TokenInfo", func(t *testing.T) {
tok, err := c.NEP17TokenInfo(h)
tok, err := neptoken.Info(c, h)
require.NoError(t, err)
require.Equal(t, h, tok.Hash)
require.Equal(t, "Rubl", tok.Name)
@ -1377,7 +1378,7 @@ func TestClient_NEP11_ND(t *testing.T) {
require.Equal(t, "NNS", sym)
})
t.Run("TokenInfo", func(t *testing.T) {
tok, err := c.NEP11TokenInfo(h)
tok, err := neptoken.Info(c, h)
require.NoError(t, err)
require.Equal(t, &wallet.Token{
Name: "NameService",
@ -1463,7 +1464,7 @@ func TestClient_NEP11_D(t *testing.T) {
require.Equal(t, "NFSO", sym)
})
t.Run("TokenInfo", func(t *testing.T) {
tok, err := c.NEP11TokenInfo(nfsoHash)
tok, err := neptoken.Info(c, nfsoHash)
require.NoError(t, err)
require.Equal(t, &wallet.Token{
Name: "NeoFS Object NFT",