0092330fe1
1. Keep initDone check only for the places where cache is directly accessed. We don't need to check it in other places, otherwise we have a mess of duplicating checks. 2. Fix bug in code related to block deserialisation. There's no magic, so checking that initialisation is done is not enough for proper block deserialisation. We need to manually fill StateRootEnabled field. 3. Since transaction doesn't need network magic to compute its hash, we don't need to perform Client initialisation before transaction-related requests. 4. Check that cache is initialised before accessing network magic. 5. Refactor the way Policy contract hash is fetched for Client requests. We don't really need Client initialisation for that, it's OK to fetch Policy hash on-the-fly.
444 lines
13 KiB
Go
444 lines
13 KiB
Go
package wallet
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/nspcc-dev/neo-go/cli/flags"
|
|
"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/transaction"
|
|
"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/rpc/client"
|
|
"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"
|
|
)
|
|
|
|
func newNEP11Commands() []cli.Command {
|
|
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",
|
|
}
|
|
tokenID := cli.StringFlag{
|
|
Name: "id",
|
|
Usage: "Hex-encoded token ID",
|
|
}
|
|
|
|
balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
|
|
copy(balanceFlags, baseBalanceFlags)
|
|
balanceFlags = append(balanceFlags, tokenID)
|
|
balanceFlags = append(balanceFlags, options.RPC...)
|
|
transferFlags := make([]cli.Flag, len(baseTransferFlags))
|
|
copy(transferFlags, baseTransferFlags)
|
|
transferFlags = append(transferFlags, tokenID)
|
|
transferFlags = append(transferFlags, options.RPC...)
|
|
return []cli.Command{
|
|
{
|
|
Name: "balance",
|
|
Usage: "get address balance",
|
|
UsageText: "balance --wallet <path> --rpc-endpoint <node> [--timeout <time>] [--address <address>] --token <hash-or-name> [--id <token-id>]",
|
|
Action: getNEP11Balance,
|
|
Flags: balanceFlags,
|
|
},
|
|
{
|
|
Name: "import",
|
|
Usage: "import NEP-11 token to a wallet",
|
|
UsageText: "import --wallet <path> --rpc-endpoint <node> --timeout <time> --token <hash>",
|
|
Action: importNEP11Token,
|
|
Flags: importFlags,
|
|
},
|
|
{
|
|
Name: "info",
|
|
Usage: "print imported NEP-11 token info",
|
|
UsageText: "print --wallet <path> [--token <hash-or-name>]",
|
|
Action: printNEP11Info,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
tokenFlag,
|
|
},
|
|
},
|
|
{
|
|
Name: "remove",
|
|
Usage: "remove NEP-11 token from the wallet",
|
|
UsageText: "remove --wallet <path> --token <hash-or-name>",
|
|
Action: removeNEP11Token,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
tokenFlag,
|
|
forceFlag,
|
|
},
|
|
},
|
|
{
|
|
Name: "transfer",
|
|
Usage: "transfer NEP-11 tokens",
|
|
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
|
|
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
|
|
'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.
|
|
`,
|
|
},
|
|
{
|
|
Name: "properties",
|
|
Usage: "print properties of NEP-11 token",
|
|
UsageText: "properties --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
|
|
Action: printNEP11Properties,
|
|
Flags: append([]cli.Flag{
|
|
tokenAddressFlag,
|
|
tokenID,
|
|
}, options.RPC...),
|
|
},
|
|
{
|
|
Name: "ownerOf",
|
|
Usage: "print owner of non-divisible NEP-11 token with the specified ID",
|
|
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",
|
|
UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
|
|
Action: printNEP11DOwner,
|
|
Flags: append([]cli.Flag{
|
|
tokenAddressFlag,
|
|
tokenID,
|
|
}, options.RPC...),
|
|
},
|
|
{
|
|
Name: "tokensOf",
|
|
Usage: "print list of tokens IDs for the specified NFT owner",
|
|
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)",
|
|
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
|
|
Action: printNEP11Tokens,
|
|
Flags: append([]cli.Flag{
|
|
tokenAddressFlag,
|
|
}, options.RPC...),
|
|
},
|
|
}
|
|
}
|
|
|
|
func importNEP11Token(ctx *cli.Context) error {
|
|
return importNEPToken(ctx, manifest.NEP11StandardName)
|
|
}
|
|
|
|
func printNEP11Info(ctx *cli.Context) error {
|
|
return printNEPInfo(ctx, manifest.NEP11StandardName)
|
|
}
|
|
|
|
func removeNEP11Token(ctx *cli.Context) error {
|
|
return removeNEPToken(ctx, manifest.NEP11StandardName)
|
|
}
|
|
|
|
func getNEP11Balance(ctx *cli.Context) error {
|
|
var accounts []*wallet.Account
|
|
|
|
wall, err := readWallet(ctx.String("wallet"))
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1)
|
|
}
|
|
defer wall.Close()
|
|
|
|
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)
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 int64
|
|
if len(tokenIDBytes) == 0 {
|
|
amount, err = c.NEP11BalanceOf(token.Hash, addrHash)
|
|
} else {
|
|
amount, err = c.NEP11DBalanceOf(token.Hash, addrHash, tokenIDBytes)
|
|
}
|
|
if err != nil {
|
|
continue
|
|
}
|
|
amountStr := fixedn.ToString(big.NewInt(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)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func transferNEP11(ctx *cli.Context) error {
|
|
return transferNEP(ctx, manifest.NEP11StandardName)
|
|
}
|
|
|
|
func signAndSendNEP11Transfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, token, to util.Uint160, tokenID []byte, amount *big.Int, data interface{}, cosigners []client.SignerAccount) error {
|
|
gas := flags.Fixed8FromContext(ctx, "gas")
|
|
sysgas := flags.Fixed8FromContext(ctx, "sysgas")
|
|
|
|
var (
|
|
tx *transaction.Transaction
|
|
err error
|
|
)
|
|
if amount != nil {
|
|
var from util.Uint160
|
|
|
|
from, err = address.StringToUint160(acc.Address)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("bad account address: %w", err), 1)
|
|
}
|
|
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, from, to, amount, tokenID, data)
|
|
} else {
|
|
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID, data)
|
|
}
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
tx.SystemFee += int64(sysgas)
|
|
|
|
if outFile := ctx.String("out"); outFile != "" {
|
|
m, err := c.GetNetwork()
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1)
|
|
}
|
|
if err := paramcontext.InitAndSave(m, tx, acc, outFile); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
} else {
|
|
if !ctx.Bool("force") {
|
|
err := input.ConfirmTx(ctx.App.Writer, tx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
_, err := c.SignAndPushTx(tx, acc, cosigners)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
|
|
return nil
|
|
}
|
|
|
|
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 {
|
|
var err error
|
|
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)
|
|
}
|
|
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
if divisible {
|
|
result, err := c.NEP11DOwnerOf(tokenHash.Uint160(), tokenIDBytes)
|
|
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 {
|
|
result, err := c.NEP11NDOwnerOf(tokenHash.Uint160(), 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))
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
result, err := c.NEP11TokensOf(tokenHash.Uint160(), acc.Uint160())
|
|
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
|
|
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)
|
|
}
|
|
|
|
result, err := c.NEP11Tokens(tokenHash.Uint160())
|
|
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
|
|
}
|
|
|
|
func printNEP11Properties(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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
result, err := c.NEP11Properties(tokenHash.Uint160(), tokenIDBytes)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `properties` method: %s", err.Error()), 1)
|
|
}
|
|
|
|
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
|
|
}
|