lotus/cli/wallet.go

748 lines
17 KiB
Go

package cli
import (
"bufio"
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/crypto"
"github.com/filecoin-project/go-state-types/network"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/lib/tablewriter"
)
var walletCmd = &cli.Command{
Name: "wallet",
Usage: "Manage wallet",
Subcommands: []*cli.Command{
walletNew,
walletList,
walletBalance,
walletExport,
walletImport,
walletGetDefault,
walletSetDefault,
walletSign,
walletVerify,
walletDelete,
walletMarket,
},
}
var walletNew = &cli.Command{
Name: "new",
Usage: "Generate a new key of the given type",
ArgsUsage: "[bls|secp256k1 (default secp256k1)]",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
t := cctx.Args().First()
if t == "" {
t = "secp256k1"
}
nk, err := api.WalletNew(ctx, types.KeyType(t))
if err != nil {
return err
}
afmt.Println(nk.String())
return nil
},
}
var walletList = &cli.Command{
Name: "list",
Usage: "List wallet address",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "addr-only",
Usage: "Only print addresses",
Aliases: []string{"a"},
},
&cli.BoolFlag{
Name: "id",
Usage: "Output ID addresses",
Aliases: []string{"i"},
},
&cli.BoolFlag{
Name: "market",
Usage: "Output market balances",
Aliases: []string{"m"},
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
addrs, err := api.WalletList(ctx)
if err != nil {
return err
}
// Assume an error means no default key is set
def, _ := api.WalletDefaultAddress(ctx)
tw := tablewriter.New(
tablewriter.Col("Address"),
tablewriter.Col("ID"),
tablewriter.Col("Balance"),
tablewriter.Col("Market(Avail)"),
tablewriter.Col("Market(Locked)"),
tablewriter.Col("Nonce"),
tablewriter.Col("Default"),
tablewriter.NewLineCol("Error"))
for _, addr := range addrs {
if cctx.Bool("addr-only") {
afmt.Println(addr.String())
} else {
a, err := api.StateGetActor(ctx, addr, types.EmptyTSK)
if err != nil {
if !strings.Contains(err.Error(), "actor not found") {
tw.Write(map[string]interface{}{
"Address": addr,
"Error": err,
})
continue
}
a = &types.Actor{
Balance: big.Zero(),
}
}
row := map[string]interface{}{
"Address": addr,
"Balance": types.FIL(a.Balance),
"Nonce": a.Nonce,
}
if addr == def {
row["Default"] = "X"
}
if cctx.Bool("id") {
id, err := api.StateLookupID(ctx, addr, types.EmptyTSK)
if err != nil {
row["ID"] = "n/a"
} else {
row["ID"] = id
}
}
if cctx.Bool("market") {
mbal, err := api.StateMarketBalance(ctx, addr, types.EmptyTSK)
if err == nil {
row["Market(Avail)"] = types.FIL(types.BigSub(mbal.Escrow, mbal.Locked))
row["Market(Locked)"] = types.FIL(mbal.Locked)
}
}
tw.Write(row)
}
}
if !cctx.Bool("addr-only") {
return tw.Flush(os.Stdout)
}
return nil
},
}
var walletBalance = &cli.Command{
Name: "balance",
Usage: "Get account balance",
ArgsUsage: "[address]",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
var addr address.Address
if cctx.Args().First() != "" {
addr, err = address.NewFromString(cctx.Args().First())
} else {
addr, err = api.WalletDefaultAddress(ctx)
}
if err != nil {
return err
}
balance, err := api.WalletBalance(ctx, addr)
if err != nil {
return err
}
if balance.Equals(types.NewInt(0)) {
afmt.Printf("%s (warning: may display 0 if chain sync in progress)\n", types.FIL(balance))
} else {
afmt.Printf("%s\n", types.FIL(balance))
}
return nil
},
}
var walletGetDefault = &cli.Command{
Name: "default",
Usage: "Get default wallet address",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
addr, err := api.WalletDefaultAddress(ctx)
if err != nil {
return err
}
afmt.Printf("%s\n", addr.String())
return nil
},
}
var walletSetDefault = &cli.Command{
Name: "set-default",
Usage: "Set default wallet address",
ArgsUsage: "[address]",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
if cctx.NArg() != 1 {
return IncorrectNumArgs(cctx)
}
addr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
fmt.Println("Default address set to:", addr)
return api.WalletSetDefault(ctx, addr)
},
}
var walletExport = &cli.Command{
Name: "export",
Usage: "export keys",
ArgsUsage: "[address]",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
if cctx.NArg() != 1 {
return IncorrectNumArgs(cctx)
}
addr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
ki, err := api.WalletExport(ctx, addr)
if err != nil {
return err
}
b, err := json.Marshal(ki)
if err != nil {
return err
}
afmt.Println(hex.EncodeToString(b))
return nil
},
}
var walletImport = &cli.Command{
Name: "import",
Usage: "import keys",
ArgsUsage: "[<path> (optional, will read from stdin if omitted)]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "format",
Usage: "specify input format for key",
Value: "hex-lotus",
},
&cli.BoolFlag{
Name: "as-default",
Usage: "import the given key as your new default key",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
var inpdata []byte
if !cctx.Args().Present() || cctx.Args().First() == "-" {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter private key: ")
indata, err := reader.ReadBytes('\n')
if err != nil {
return err
}
inpdata = indata
} else {
fdata, err := os.ReadFile(cctx.Args().First())
if err != nil {
return err
}
inpdata = fdata
}
var ki types.KeyInfo
switch cctx.String("format") {
case "hex-lotus":
data, err := hex.DecodeString(strings.TrimSpace(string(inpdata)))
if err != nil {
return err
}
if err := json.Unmarshal(data, &ki); err != nil {
return err
}
case "json-lotus":
if err := json.Unmarshal(inpdata, &ki); err != nil {
return err
}
case "gfc-json":
var f struct {
KeyInfo []struct {
PrivateKey []byte
SigType int
}
}
if err := json.Unmarshal(inpdata, &f); err != nil {
return xerrors.Errorf("failed to parse go-filecoin key: %s", err)
}
gk := f.KeyInfo[0]
ki.PrivateKey = gk.PrivateKey
switch gk.SigType {
case 1:
ki.Type = types.KTSecp256k1
case 2:
ki.Type = types.KTBLS
default:
return fmt.Errorf("unrecognized key type: %d", gk.SigType)
}
default:
return fmt.Errorf("unrecognized format: %s", cctx.String("format"))
}
addr, err := api.WalletImport(ctx, &ki)
if err != nil {
return err
}
if cctx.Bool("as-default") {
if err := api.WalletSetDefault(ctx, addr); err != nil {
return fmt.Errorf("failed to set default key: %w", err)
}
}
fmt.Printf("imported key %s successfully!\n", addr)
return nil
},
}
var walletSign = &cli.Command{
Name: "sign",
Usage: "sign a message",
ArgsUsage: "<signing address> <hexMessage>",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
if cctx.NArg() != 2 {
return IncorrectNumArgs(cctx)
}
addr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
msg, err := hex.DecodeString(cctx.Args().Get(1))
if err != nil {
return err
}
sig, err := api.WalletSign(ctx, addr, msg)
if err != nil {
return err
}
sigBytes := append([]byte{byte(sig.Type)}, sig.Data...)
afmt.Println(hex.EncodeToString(sigBytes))
return nil
},
}
var walletVerify = &cli.Command{
Name: "verify",
Usage: "verify the signature of a message",
ArgsUsage: "<signing address> <hexMessage> <signature>",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
if cctx.NArg() != 3 {
return IncorrectNumArgs(cctx)
}
addr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
msg, err := hex.DecodeString(cctx.Args().Get(1))
if err != nil {
return err
}
sigBytes, err := hex.DecodeString(cctx.Args().Get(2))
if err != nil {
return err
}
var sig crypto.Signature
if err := sig.UnmarshalBinary(sigBytes); err != nil {
return err
}
ok, err := api.WalletVerify(ctx, addr, msg, &sig)
if err != nil {
return err
}
if ok {
afmt.Println("valid")
return nil
}
afmt.Println("invalid")
return NewCliError("CLI Verify called with invalid signature")
},
}
var walletDelete = &cli.Command{
Name: "delete",
Usage: "Soft delete an address from the wallet - hard deletion needed for permanent removal",
ArgsUsage: "<address> ",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
if cctx.NArg() != 1 {
return IncorrectNumArgs(cctx)
}
addr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
fmt.Println("Soft deleting address:", addr)
fmt.Println("Hard deletion of the address in `~/.lotus/keystore` is needed for permanent removal")
return api.WalletDelete(ctx, addr)
},
}
var walletMarket = &cli.Command{
Name: "market",
Usage: "Interact with market balances",
Subcommands: []*cli.Command{
walletMarketWithdraw,
walletMarketAdd,
},
}
var walletMarketWithdraw = &cli.Command{
Name: "withdraw",
Usage: "Withdraw funds from the Storage Market Actor",
ArgsUsage: "[amount (FIL) optional, otherwise will withdraw max available]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "wallet",
Usage: "Specify address to withdraw funds to, otherwise it will use the default wallet address",
Aliases: []string{"w"},
},
&cli.StringFlag{
Name: "address",
Usage: "Market address to withdraw from (account or miner actor address, defaults to --wallet address)",
Aliases: []string{"a"},
},
&cli.IntFlag{
Name: "confidence",
Usage: "number of block confirmations to wait for",
Value: int(build.MessageConfidence),
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return xerrors.Errorf("getting node API: %w", err)
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
var wallet address.Address
if cctx.String("wallet") != "" {
wallet, err = address.NewFromString(cctx.String("wallet"))
if err != nil {
return xerrors.Errorf("parsing from address: %w", err)
}
} else {
wallet, err = api.WalletDefaultAddress(ctx)
if err != nil {
return xerrors.Errorf("getting default wallet address: %w", err)
}
}
addr := wallet
if cctx.String("address") != "" {
addr, err = address.NewFromString(cctx.String("address"))
if err != nil {
return xerrors.Errorf("parsing market address: %w", err)
}
}
// Work out if there are enough unreserved, unlocked funds to withdraw
bal, err := api.StateMarketBalance(ctx, addr, types.EmptyTSK)
if err != nil {
return xerrors.Errorf("getting market balance for address %s: %w", addr.String(), err)
}
reserved, err := api.MarketGetReserved(ctx, addr)
if err != nil {
return xerrors.Errorf("getting market reserved amount for address %s: %w", addr.String(), err)
}
avail := big.Subtract(big.Subtract(bal.Escrow, bal.Locked), reserved)
notEnoughErr := func(msg string) error {
return xerrors.Errorf("%s; "+
"available (%s) = escrow (%s) - locked (%s) - reserved (%s)",
msg, types.FIL(avail), types.FIL(bal.Escrow), types.FIL(bal.Locked), types.FIL(reserved))
}
if avail.IsZero() || avail.LessThan(big.Zero()) {
avail = big.Zero()
return notEnoughErr("no funds available to withdraw")
}
// Default to withdrawing all available funds
amt := avail
// If there was an amount argument, only withdraw that amount
if cctx.Args().Present() {
f, err := types.ParseFIL(cctx.Args().First())
if err != nil {
return xerrors.Errorf("parsing 'amount' argument: %w", err)
}
amt = abi.TokenAmount(f)
}
// Check the amount is positive
if amt.IsZero() || amt.LessThan(big.Zero()) {
return xerrors.Errorf("amount must be > 0")
}
// Check there are enough available funds
if amt.GreaterThan(avail) {
msg := fmt.Sprintf("can't withdraw more funds than available; requested: %s", types.FIL(amt))
return notEnoughErr(msg)
}
fmt.Printf("Submitting WithdrawBalance message for amount %s for address %s\n", types.FIL(amt), wallet.String())
smsg, err := api.MarketWithdraw(ctx, wallet, addr, amt)
if err != nil {
return xerrors.Errorf("fund manager withdraw error: %w", err)
}
afmt.Printf("WithdrawBalance message cid: %s\n", smsg)
// wait for it to get mined into a block
wait, err := api.StateWaitMsg(ctx, smsg, uint64(cctx.Int("confidence")))
if err != nil {
return err
}
// check it executed successfully
if wait.Receipt.ExitCode.IsError() {
afmt.Println(cctx.App.Writer, "withdrawal failed!")
return err
}
nv, err := api.StateNetworkVersion(ctx, wait.TipSet)
if err != nil {
return err
}
if nv >= network.Version14 {
var withdrawn abi.TokenAmount
if err := withdrawn.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)); err != nil {
return err
}
afmt.Printf("Successfully withdrew %s \n", types.FIL(withdrawn))
if withdrawn.LessThan(amt) {
fmt.Printf("Note that this is less than the requested amount of %s \n", types.FIL(amt))
}
}
return nil
},
}
var walletMarketAdd = &cli.Command{
Name: "add",
Usage: "Add funds to the Storage Market Actor",
ArgsUsage: "<amount>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "from",
Usage: "Specify address to move funds from, otherwise it will use the default wallet address",
Aliases: []string{"f"},
},
&cli.StringFlag{
Name: "address",
Usage: "Market address to move funds to (account or miner actor address, defaults to --from address)",
Aliases: []string{"a"},
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return xerrors.Errorf("getting node API: %w", err)
}
defer closer()
ctx := ReqContext(cctx)
afmt := NewAppFmt(cctx.App)
// Get amount param
if cctx.NArg() < 1 {
return IncorrectNumArgs(cctx)
}
f, err := types.ParseFIL(cctx.Args().First())
if err != nil {
return xerrors.Errorf("parsing 'amount' argument: %w", err)
}
amt := abi.TokenAmount(f)
// Get from param
var from address.Address
if cctx.String("from") != "" {
from, err = address.NewFromString(cctx.String("from"))
if err != nil {
return xerrors.Errorf("parsing from address: %w", err)
}
} else {
from, err = api.WalletDefaultAddress(ctx)
if err != nil {
return xerrors.Errorf("getting default wallet address: %w", err)
}
}
// Get address param
addr := from
if cctx.String("address") != "" {
addr, err = address.NewFromString(cctx.String("address"))
if err != nil {
return xerrors.Errorf("parsing market address: %w", err)
}
}
// Add balance to market actor
fmt.Printf("Submitting Add Balance message for amount %s for address %s\n", types.FIL(amt), addr)
smsg, err := api.MarketAddBalance(ctx, from, addr, amt)
if err != nil {
return xerrors.Errorf("add balance error: %w", err)
}
afmt.Printf("AddBalance message cid: %s\n", smsg)
return nil
},
}