neoneo-go/cli/wallet/nep11.go

387 lines
12 KiB
Go
Raw Normal View History

2021-04-22 15:21:18 +00:00
package wallet
import (
"encoding/hex"
2021-04-23 15:42:59 +00:00
"errors"
"fmt"
"strconv"
2021-04-23 15:42:59 +00:00
"github.com/nspcc-dev/neo-go/cli/cmdargs"
2021-04-23 15:42:59 +00:00
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/config"
2021-04-23 15:42:59 +00:00
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"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"
2021-04-22 15:21:18 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
2021-04-26 14:09:37 +00:00
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
2021-04-23 15:42:59 +00:00
"github.com/nspcc-dev/neo-go/pkg/wallet"
2021-04-22 15:21:18 +00:00
"github.com/urfave/cli"
)
func newNEP11Commands() []cli.Command {
maxIters := strconv.Itoa(config.DefaultMaxIteratorResultItems)
2021-04-27 14:55:01 +00:00
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",
}
2021-04-23 15:42:59 +00:00
tokenID := cli.StringFlag{
Name: "id",
Usage: "Hex-encoded token ID",
2021-04-23 15:42:59 +00:00
}
balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
copy(balanceFlags, baseBalanceFlags)
balanceFlags = append(balanceFlags, tokenID)
balanceFlags = append(balanceFlags, options.RPC...)
2021-04-23 16:00:45 +00:00
transferFlags := make([]cli.Flag, len(baseTransferFlags))
copy(transferFlags, baseTransferFlags)
transferFlags = append(transferFlags, tokenID)
transferFlags = append(transferFlags, options.RPC...)
2021-04-22 15:21:18 +00:00
return []cli.Command{
2021-04-23 15:42:59 +00:00
{
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>]",
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,
2021-04-23 15:42:59 +00:00
},
2021-04-22 15:21:18 +00:00
{
Name: "import",
Usage: "import NEP-11 token to a wallet",
UsageText: "import -w wallet [--wallet-config path] --rpc-endpoint <node> --timeout <time> --token <hash>",
2021-04-22 15:21:18 +00:00
Action: importNEP11Token,
Flags: importFlags,
},
2021-04-23 10:37:59 +00:00
{
Name: "info",
Usage: "print imported NEP-11 token info",
UsageText: "print -w wallet [--wallet-config path] [--token <hash-or-name>]",
2021-04-23 10:37:59 +00:00
Action: printNEP11Info,
Flags: []cli.Flag{
walletPathFlag,
walletConfigFlag,
2021-04-23 10:37:59 +00:00
tokenFlag,
},
},
2021-04-23 15:08:47 +00:00
{
Name: "remove",
Usage: "remove NEP-11 token from the wallet",
UsageText: "remove -w wallet [--wallet-config path] --token <hash-or-name>",
2021-04-23 15:08:47 +00:00
Action: removeNEP11Token,
Flags: []cli.Flag{
walletPathFlag,
walletConfigFlag,
2021-04-23 15:08:47 +00:00
tokenFlag,
forceFlag,
},
},
2021-04-23 16:00:45 +00:00
{
Name: "transfer",
Usage: "transfer NEP-11 tokens",
UsageText: "transfer -w wallet [--wallet-config path] --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
2021-04-23 16:00:45 +00:00
Action: transferNEP11,
Flags: transferFlags,
Description: `Transfers specified NEP-11 token with optional cosigners list attached to
the transfer. Amount should be specified for divisible NEP-11
tokens and omitted for non-divisible NEP-11 tokens. See
2021-04-23 16:00:45 +00:00
'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.
`,
},
2021-04-26 14:09:37 +00:00
{
Name: "properties",
Usage: "print properties of NEP-11 token",
2021-04-26 14:09:37 +00:00
UsageText: "properties --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11Properties,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
}, options.RPC...),
},
2021-04-27 14:55:01 +00:00
{
Name: "ownerOf",
Usage: "print owner of non-divisible NEP-11 token with the specified ID",
2021-04-27 14:55:01 +00:00
UsageText: "ownerOf --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11NDOwner,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
}, options.RPC...),
},
{
Name: "ownerOfD",
Usage: "print set of owners of divisible NEP-11 token with the specified ID (" + maxIters + " will be printed at max)",
UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11DOwner,
2021-04-27 14:55:01 +00:00
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
}, options.RPC...),
},
{
Name: "tokensOf",
Usage: "print list of tokens IDs for the specified NFT owner (" + maxIters + " will be printed at max)",
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; " + maxIters + " will be printed at max)",
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
Action: printNEP11Tokens,
Flags: append([]cli.Flag{
tokenAddressFlag,
}, options.RPC...),
},
2021-04-22 15:21:18 +00:00
}
}
func importNEP11Token(ctx *cli.Context) error {
return importNEPToken(ctx, manifest.NEP11StandardName)
}
2021-04-23 10:37:59 +00:00
func printNEP11Info(ctx *cli.Context) error {
return printNEPInfo(ctx, manifest.NEP11StandardName)
}
2021-04-23 15:08:47 +00:00
func removeNEP11Token(ctx *cli.Context) error {
return removeNEPToken(ctx, manifest.NEP11StandardName)
}
2021-04-23 15:42:59 +00:00
func getNEP11Balance(ctx *cli.Context) error {
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)
2021-04-23 15:42:59 +00:00
if err != nil {
return err
2021-04-23 15:42:59 +00:00
}
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
}
2021-04-23 15:42:59 +00:00
}
if name == "" || tokenFound {
return nil
2021-04-23 15:42:59 +00:00
}
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,
}, "")
2021-04-23 15:42:59 +00:00
} 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)
2021-04-23 15:42:59 +00:00
}
return nil
})
}
2021-04-23 15:42:59 +00:00
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
2021-04-23 15:42:59 +00:00
}
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)
2021-04-23 15:42:59 +00:00
}
}
2021-04-23 16:00:45 +00:00
func transferNEP11(ctx *cli.Context) error {
return transferNEP(ctx, manifest.NEP11StandardName)
}
func printNEP11NDOwner(ctx *cli.Context) error {
return printNEP11Owner(ctx, false)
}
func printNEP11DOwner(ctx *cli.Context) error {
return printNEP11Owner(ctx, true)
}
func printNEP11Owner(ctx *cli.Context, divisible bool) error {
2021-04-27 14:55:01 +00:00
var err error
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
2021-04-27 14:55:01 +00:00
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)
}
tokenIDBytes, err := hex.DecodeString(tokenID)
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid tokenID bytes: %w", err), 1)
}
2021-04-27 14:55:01 +00:00
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
if divisible {
n11 := nep11.NewDivisibleReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.OwnerOfExpanded(tokenIDBytes, config.DefaultMaxIteratorResultItems)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1)
}
for _, h := range result {
fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(h))
}
} else {
n11 := nep11.NewNonDivisibleReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.OwnerOf(tokenIDBytes)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 non-divisible `ownerOf` method: %s", err.Error()), 1)
}
fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(result))
2021-04-27 14:55:01 +00:00
}
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)
}
n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.TokensOfExpanded(acc.Uint160(), config.DefaultMaxIteratorResultItems)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1)
}
for i := range result {
fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(result[i]))
}
return nil
}
func printNEP11Tokens(ctx *cli.Context) error {
var err error
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
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)
}
n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1)
}
for i := range result {
fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(result[i]))
}
return nil
}
2021-04-26 14:09:37 +00:00
func printNEP11Properties(ctx *cli.Context) error {
var err error
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
2021-04-26 14:09:37 +00:00
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)
}
tokenIDBytes, err := hex.DecodeString(tokenID)
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid tokenID bytes: %w", err), 1)
}
2021-04-26 14:09:37 +00:00
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.Properties(tokenIDBytes)
2021-04-26 14:09:37 +00:00
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `properties` method: %s", err.Error()), 1)
2021-04-26 14:09:37 +00:00
}
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
}