lotus/cli/wallet.go
Andrew Jackson (Ajax) b95e95f4d6
feat: SPTool (#11788)
* sptool: Initial structure

* sptool: Port lotus-miner actor withdraw

* sptool: Make cli docsgen happy

* actors are done

* info

* proving

* sptool the rest

* fixed gitignore

* lints

* oops

* 2

* terminate

* fixes

* sptool: improve sectors list

---------

Co-authored-by: Łukasz Magiera <magik6k@gmail.com>
2024-04-01 10:30:35 -05:00

781 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cli
import (
"bufio"
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"golang.org/x/term"
"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/actors/builtin"
"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
}
inSync, err := IsSyncDone(ctx, api)
if err != nil {
return err
}
if balance.Equals(types.NewInt(0)) && !inSync {
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() == "-" {
if term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Print("Enter private key(not display in the terminal): ")
sigCh := make(chan os.Signal, 1)
// Notify the channel when SIGINT is received
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Println("\nInterrupt signal received. Exiting...")
os.Exit(1)
}()
inpdata, err = term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
fmt.Println()
} else {
reader := bufio.NewReader(os.Stdin)
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 {
// Check if the address is a multisig address
act, actErr := api.StateGetActor(ctx, addr, types.EmptyTSK)
if actErr == nil && builtin.IsMultisigActor(act.Code) {
return xerrors.Errorf("specified signer address is a multisig actor, it doesnt have keys to sign transactions. To send a message with a multisig, signers of the multisig need to propose and approve transactions.")
}
return xerrors.Errorf("failed to sign message: %w", 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
},
}