github.com/dmmcquay/sia@v1.3.1-0.20180712220038-9f8d535311b9/cmd/siac/walletcmd.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"math/big"
     8  	"os"
     9  	"syscall"
    10  	"time"
    11  
    12  	"github.com/spf13/cobra"
    13  	"golang.org/x/crypto/ssh/terminal"
    14  
    15  	"github.com/NebulousLabs/Sia/types"
    16  )
    17  
    18  var (
    19  	walletAddressCmd = &cobra.Command{
    20  		Use:   "address",
    21  		Short: "Get a new wallet address",
    22  		Long:  "Generate a new wallet address from the wallet's primary seed.",
    23  		Run:   wrap(walletaddresscmd),
    24  	}
    25  
    26  	walletAddressesCmd = &cobra.Command{
    27  		Use:   "addresses",
    28  		Short: "List all addresses",
    29  		Long:  "List all addresses that have been generated by the wallet.",
    30  		Run:   wrap(walletaddressescmd),
    31  	}
    32  
    33  	walletBalanceCmd = &cobra.Command{
    34  		Use:   "balance",
    35  		Short: "View wallet balance",
    36  		Long:  "View wallet balance, including confirmed and unconfirmed siacoins and siafunds.",
    37  		Run:   wrap(walletbalancecmd),
    38  	}
    39  
    40  	walletChangepasswordCmd = &cobra.Command{
    41  		Use:   "change-password",
    42  		Short: "Change the wallet password",
    43  		Long:  "Change the encryption password of the wallet, re-encrypting all keys + seeds kept by the wallet.",
    44  		Run:   wrap(walletchangepasswordcmd),
    45  	}
    46  
    47  	walletCmd = &cobra.Command{
    48  		Use:   "wallet",
    49  		Short: "Perform wallet actions",
    50  		Long: `Generate a new address, send coins to another wallet, or view info about the wallet.
    51  
    52  Units:
    53  The smallest unit of siacoins is the hasting. One siacoin is 10^24 hastings. Other supported units are:
    54    pS (pico,  10^-12 SC)
    55    nS (nano,  10^-9 SC)
    56    uS (micro, 10^-6 SC)
    57    mS (milli, 10^-3 SC)
    58    SC
    59    KS (kilo, 10^3 SC)
    60    MS (mega, 10^6 SC)
    61    GS (giga, 10^9 SC)
    62    TS (tera, 10^12 SC)`,
    63  		Run: wrap(walletbalancecmd),
    64  	}
    65  
    66  	walletInitCmd = &cobra.Command{
    67  		Use:   "init",
    68  		Short: "Initialize and encrypt a new wallet",
    69  		Long: `Generate a new wallet from a randomly generated seed, and encrypt it.
    70  By default the wallet encryption / unlock password is the same as the generated seed.`,
    71  		Run: wrap(walletinitcmd),
    72  	}
    73  
    74  	walletInitSeedCmd = &cobra.Command{
    75  		Use:   "init-seed",
    76  		Short: "Initialize and encrypt a new wallet using a pre-existing seed",
    77  		Long:  `Initialize and encrypt a new wallet using a pre-existing seed.`,
    78  		Run:   wrap(walletinitseedcmd),
    79  	}
    80  
    81  	walletLoad033xCmd = &cobra.Command{
    82  		Use:   "033x [filepath]",
    83  		Short: "Load a v0.3.3.x wallet",
    84  		Long:  "Load a v0.3.3.x wallet into the current wallet",
    85  		Run:   wrap(walletload033xcmd),
    86  	}
    87  
    88  	walletLoadCmd = &cobra.Command{
    89  		Use:   "load",
    90  		Short: "Load a wallet seed, v0.3.3.x wallet, or siag keyset",
    91  		// Run field is not set, as the load command itself is not a valid command.
    92  		// A subcommand must be provided.
    93  	}
    94  
    95  	walletLoadSeedCmd = &cobra.Command{
    96  		Use:   `seed`,
    97  		Short: "Add a seed to the wallet",
    98  		Long:  "Loads an auxiliary seed into the wallet.",
    99  		Run:   wrap(walletloadseedcmd),
   100  	}
   101  
   102  	walletLoadSiagCmd = &cobra.Command{
   103  		Use:     `siag [filepath,...]`,
   104  		Short:   "Load siag key(s) into the wallet",
   105  		Long:    "Load siag key(s) into the wallet - typically used for siafunds.",
   106  		Example: "siac wallet load siag key1.siakey,key2.siakey",
   107  		Run:     wrap(walletloadsiagcmd),
   108  	}
   109  
   110  	walletLockCmd = &cobra.Command{
   111  		Use:   "lock",
   112  		Short: "Lock the wallet",
   113  		Long:  "Lock the wallet, preventing further use",
   114  		Run:   wrap(walletlockcmd),
   115  	}
   116  
   117  	walletSeedsCmd = &cobra.Command{
   118  		Use:   "seeds",
   119  		Short: "View information about your seeds",
   120  		Long:  "View your primary and auxiliary wallet seeds.",
   121  		Run:   wrap(walletseedscmd),
   122  	}
   123  
   124  	walletSendCmd = &cobra.Command{
   125  		Use:   "send",
   126  		Short: "Send either siacoins or siafunds to an address",
   127  		Long:  "Send either siacoins or siafunds to an address",
   128  		// Run field is not set, as the send command itself is not a valid command.
   129  		// A subcommand must be provided.
   130  	}
   131  
   132  	walletSendSiacoinsCmd = &cobra.Command{
   133  		Use:   "siacoins [amount] [dest]",
   134  		Short: "Send siacoins to an address",
   135  		Long: `Send siacoins to an address. 'dest' must be a 76-byte hexadecimal address.
   136  'amount' can be specified in units, e.g. 1.23KS. Run 'wallet --help' for a list of units.
   137  If no unit is supplied, hastings will be assumed.
   138  
   139  A dynamic transaction fee is applied depending on the size of the transaction and how busy the network is.`,
   140  		Run: wrap(walletsendsiacoinscmd),
   141  	}
   142  
   143  	walletSendSiafundsCmd = &cobra.Command{
   144  		Use:   "siafunds [amount] [dest]",
   145  		Short: "Send siafunds",
   146  		Long: `Send siafunds to an address, and transfer the claim siacoins to your wallet.
   147  Run 'wallet send --help' to see a list of available units.`,
   148  		Run: wrap(walletsendsiafundscmd),
   149  	}
   150  
   151  	walletSweepCmd = &cobra.Command{
   152  		Use:   "sweep",
   153  		Short: "Sweep siacoins and siafunds from a seed.",
   154  		Long: `Sweep siacoins and siafunds from a seed. The outputs belonging to the seed
   155  will be sent to your wallet.`,
   156  		Run: wrap(walletsweepcmd),
   157  	}
   158  
   159  	walletTransactionsCmd = &cobra.Command{
   160  		Use:   "transactions",
   161  		Short: "View transactions",
   162  		Long:  "View transactions related to addresses spendable by the wallet, providing a net flow of siacoins and siafunds for each transaction",
   163  		Run:   wrap(wallettransactionscmd),
   164  	}
   165  
   166  	walletUnlockCmd = &cobra.Command{
   167  		Use:   `unlock`,
   168  		Short: "Unlock the wallet",
   169  		Long: `Decrypt and load the wallet into memory.
   170  Automatic unlocking is also supported via environment variable: if the
   171  SIA_WALLET_PASSWORD environment variable is set, the unlock command will
   172  use it instead of displaying the typical interactive prompt.`,
   173  		Run: wrap(walletunlockcmd),
   174  	}
   175  )
   176  
   177  const askPasswordText = "We need to encrypt the new data using the current wallet password, please provide: "
   178  
   179  const currentPasswordText = "Current Password: "
   180  const newPasswordText = "New Password: "
   181  const confirmPasswordText = "Confirm: "
   182  
   183  // For an unconfirmed Transaction, the TransactionTimestamp field is set to the
   184  // maximum value of a uint64.
   185  const unconfirmedTransactionTimestamp = ^uint64(0)
   186  
   187  // passwordPrompt securely reads a password from stdin.
   188  func passwordPrompt(prompt string) (string, error) {
   189  	fmt.Print(prompt)
   190  	pw, err := terminal.ReadPassword(int(syscall.Stdin))
   191  	fmt.Println()
   192  	return string(pw), err
   193  }
   194  
   195  // confirmPassword requests confirmation of a previously-entered password.
   196  func confirmPassword(prev string) error {
   197  	pw, err := passwordPrompt(confirmPasswordText)
   198  	if err != nil {
   199  		return err
   200  	} else if pw != prev {
   201  		return errors.New("passwords do not match")
   202  	}
   203  	return nil
   204  }
   205  
   206  // walletaddresscmd fetches a new address from the wallet that will be able to
   207  // receive coins.
   208  func walletaddresscmd() {
   209  	addr, err := httpClient.WalletAddressGet()
   210  	if err != nil {
   211  		die("Could not generate new address:", err)
   212  	}
   213  	fmt.Printf("Created new address: %s\n", addr.Address)
   214  }
   215  
   216  // walletaddressescmd fetches the list of addresses that the wallet knows.
   217  func walletaddressescmd() {
   218  	addrs, err := httpClient.WalletAddressesGet()
   219  	if err != nil {
   220  		die("Failed to fetch addresses:", err)
   221  	}
   222  	for _, addr := range addrs.Addresses {
   223  		fmt.Println(addr)
   224  	}
   225  }
   226  
   227  // walletchangepasswordcmd changes the password of the wallet.
   228  func walletchangepasswordcmd() {
   229  	currentPassword, err := passwordPrompt(currentPasswordText)
   230  	if err != nil {
   231  		die("Reading password failed:", err)
   232  	}
   233  	newPassword, err := passwordPrompt(newPasswordText)
   234  	if err != nil {
   235  		die("Reading password failed:", err)
   236  	} else if err = confirmPassword(newPassword); err != nil {
   237  		die(err)
   238  	}
   239  	err = httpClient.WalletChangePasswordPost(currentPassword, newPassword)
   240  	if err != nil {
   241  		die("Changing the password failed:", err)
   242  	}
   243  	fmt.Println("Password changed successfully.")
   244  }
   245  
   246  // walletinitcmd encrypts the wallet with the given password
   247  func walletinitcmd() {
   248  	var password string
   249  	var err error
   250  	if initPassword {
   251  		password, err = passwordPrompt("Wallet password: ")
   252  		if err != nil {
   253  			die("Reading password failed:", err)
   254  		} else if err = confirmPassword(password); err != nil {
   255  			die(err)
   256  		}
   257  	}
   258  	er, err := httpClient.WalletInitPost(password, initForce)
   259  	if err != nil {
   260  		die("Error when encrypting wallet:", err)
   261  	}
   262  	fmt.Printf("Recovery seed:\n%s\n\n", er.PrimarySeed)
   263  	if initPassword {
   264  		fmt.Printf("Wallet encrypted with given password\n")
   265  	} else {
   266  		fmt.Printf("Wallet encrypted with password:\n%s\n", er.PrimarySeed)
   267  	}
   268  }
   269  
   270  // walletinitseedcmd initializes the wallet from a preexisting seed.
   271  func walletinitseedcmd() {
   272  	seed, err := passwordPrompt("Seed: ")
   273  	if err != nil {
   274  		die("Reading seed failed:", err)
   275  	}
   276  	var password string
   277  	if initPassword {
   278  		password, err = passwordPrompt("Wallet password: ")
   279  		if err != nil {
   280  			die("Reading password failed:", err)
   281  		} else if err = confirmPassword(password); err != nil {
   282  			die(err)
   283  		}
   284  	}
   285  	err = httpClient.WalletInitSeedPost(seed, password, initForce)
   286  	if err != nil {
   287  		die("Could not initialize wallet from seed:", err)
   288  	}
   289  	if initPassword {
   290  		fmt.Println("Wallet initialized and encrypted with given password.")
   291  	} else {
   292  		fmt.Println("Wallet initialized and encrypted with seed.")
   293  	}
   294  }
   295  
   296  // walletload033xcmd loads a v0.3.3.x wallet into the current wallet.
   297  func walletload033xcmd(source string) {
   298  	password, err := passwordPrompt(askPasswordText)
   299  	if err != nil {
   300  		die("Reading password failed:", err)
   301  	}
   302  	err = httpClient.Wallet033xPost(abs(source), password)
   303  	if err != nil {
   304  		die("Loading wallet failed:", err)
   305  	}
   306  	fmt.Println("Wallet loading successful.")
   307  }
   308  
   309  // walletloadseedcmd adds a seed to the wallet's list of seeds
   310  func walletloadseedcmd() {
   311  	seed, err := passwordPrompt("New seed: ")
   312  	if err != nil {
   313  		die("Reading seed failed:", err)
   314  	}
   315  	password, err := passwordPrompt(askPasswordText)
   316  	if err != nil {
   317  		die("Reading password failed:", err)
   318  	}
   319  	err = httpClient.WalletSeedPost(seed, password)
   320  	if err != nil {
   321  		die("Could not add seed:", err)
   322  	}
   323  	fmt.Println("Added Key")
   324  }
   325  
   326  // walletloadsiagcmd loads a siag key set into the wallet.
   327  func walletloadsiagcmd(keyfiles string) {
   328  	password, err := passwordPrompt(askPasswordText)
   329  	if err != nil {
   330  		die("Reading password failed:", err)
   331  	}
   332  	err = httpClient.WalletSiagKeyPost(keyfiles, password)
   333  	if err != nil {
   334  		die("Loading siag key failed:", err)
   335  	}
   336  	fmt.Println("Wallet loading successful.")
   337  }
   338  
   339  // walletlockcmd locks the wallet
   340  func walletlockcmd() {
   341  	err := httpClient.WalletLockPost()
   342  	if err != nil {
   343  		die("Could not lock wallet:", err)
   344  	}
   345  }
   346  
   347  // walletseedcmd returns the current seed {
   348  func walletseedscmd() {
   349  	seedInfo, err := httpClient.WalletSeedsGet()
   350  	if err != nil {
   351  		die("Error retrieving the current seed:", err)
   352  	}
   353  	fmt.Println("Primary Seed:")
   354  	fmt.Println(seedInfo.PrimarySeed)
   355  	if len(seedInfo.AllSeeds) == 1 {
   356  		// AllSeeds includes the primary seed
   357  		return
   358  	}
   359  	fmt.Println()
   360  	fmt.Println("Auxiliary Seeds:")
   361  	for _, seed := range seedInfo.AllSeeds {
   362  		if seed == seedInfo.PrimarySeed {
   363  			continue
   364  		}
   365  		fmt.Println() // extra newline for readability
   366  		fmt.Println(seed)
   367  	}
   368  }
   369  
   370  // walletsendsiacoinscmd sends siacoins to a destination address.
   371  func walletsendsiacoinscmd(amount, dest string) {
   372  	hastings, err := parseCurrency(amount)
   373  	if err != nil {
   374  		die("Could not parse amount:", err)
   375  	}
   376  	var value types.Currency
   377  	if _, err := fmt.Sscan(hastings, &value); err != nil {
   378  		die("Failed to parse amount", err)
   379  	}
   380  	var hash types.UnlockHash
   381  	if _, err := fmt.Sscan(dest, &hash); err != nil {
   382  		die("Failed to parse destination address", err)
   383  	}
   384  	_, err = httpClient.WalletSiacoinsPost(value, hash)
   385  	if err != nil {
   386  		die("Could not send siacoins:", err)
   387  	}
   388  	fmt.Printf("Sent %s hastings to %s\n", hastings, dest)
   389  }
   390  
   391  // walletsendsiafundscmd sends siafunds to a destination address.
   392  func walletsendsiafundscmd(amount, dest string) {
   393  	var value types.Currency
   394  	if _, err := fmt.Sscan(amount, &value); err != nil {
   395  		die("Failed to parse amount", err)
   396  	}
   397  	var hash types.UnlockHash
   398  	if _, err := fmt.Sscan(dest, &hash); err != nil {
   399  		die("Failed to parse destination address", err)
   400  	}
   401  	_, err := httpClient.WalletSiafundsPost(value, hash)
   402  	if err != nil {
   403  		die("Could not send siafunds:", err)
   404  	}
   405  	fmt.Printf("Sent %s siafunds to %s\n", amount, dest)
   406  }
   407  
   408  // walletbalancecmd retrieves and displays information about the wallet.
   409  func walletbalancecmd() {
   410  	status, err := httpClient.WalletGet()
   411  	if err != nil {
   412  		die("Could not get wallet status:", err)
   413  	}
   414  	fees, err := httpClient.TransactionPoolFeeGet()
   415  	if err != nil {
   416  		die("Could not get fee estimation:", err)
   417  	}
   418  	encStatus := "Unencrypted"
   419  	if status.Encrypted {
   420  		encStatus = "Encrypted"
   421  	}
   422  	if !status.Unlocked {
   423  		fmt.Printf(`Wallet status:
   424  %v, Locked
   425  Unlock the wallet to view balance
   426  `, encStatus)
   427  		return
   428  	}
   429  
   430  	unconfirmedBalance := status.ConfirmedSiacoinBalance.Add(status.UnconfirmedIncomingSiacoins).Sub(status.UnconfirmedOutgoingSiacoins)
   431  	var delta string
   432  	if unconfirmedBalance.Cmp(status.ConfirmedSiacoinBalance) >= 0 {
   433  		delta = "+" + currencyUnits(unconfirmedBalance.Sub(status.ConfirmedSiacoinBalance))
   434  	} else {
   435  		delta = "-" + currencyUnits(status.ConfirmedSiacoinBalance.Sub(unconfirmedBalance))
   436  	}
   437  
   438  	fmt.Printf(`Wallet status:
   439  %s, Unlocked
   440  Height:              %v
   441  Confirmed Balance:   %v
   442  Unconfirmed Delta:  %v
   443  Exact:               %v H
   444  Siafunds:            %v SF
   445  Siafund Claims:      %v H
   446  
   447  Estimated Fee:       %v / KB
   448  `, encStatus, status.Height, currencyUnits(status.ConfirmedSiacoinBalance), delta,
   449  		status.ConfirmedSiacoinBalance, status.SiafundBalance, status.SiacoinClaimBalance,
   450  		fees.Maximum.Mul64(1e3).HumanString())
   451  }
   452  
   453  // walletsweepcmd sweeps coins and funds from a seed.
   454  func walletsweepcmd() {
   455  	seed, err := passwordPrompt("Seed: ")
   456  	if err != nil {
   457  		die("Reading seed failed:", err)
   458  	}
   459  
   460  	swept, err := httpClient.WalletSweepPost(seed)
   461  	if err != nil {
   462  		die("Could not sweep seed:", err)
   463  	}
   464  	fmt.Printf("Swept %v and %v SF from seed.\n", currencyUnits(swept.Coins), swept.Funds)
   465  }
   466  
   467  // wallettransactionscmd lists all of the transactions related to the wallet,
   468  // providing a net flow of siacoins and siafunds for each.
   469  func wallettransactionscmd() {
   470  	wtg, err := httpClient.WalletTransactionsGet(0, math.MaxInt64)
   471  	if err != nil {
   472  		die("Could not fetch transaction history:", err)
   473  	}
   474  	fmt.Println("             [timestamp]    [height]                                                   [transaction id]    [net siacoins]   [net siafunds]")
   475  	txns := append(wtg.ConfirmedTransactions, wtg.UnconfirmedTransactions...)
   476  	for _, txn := range txns {
   477  		// Determine the number of outgoing siacoins and siafunds.
   478  		var outgoingSiacoins types.Currency
   479  		var outgoingSiafunds types.Currency
   480  		for _, input := range txn.Inputs {
   481  			if input.FundType == types.SpecifierSiacoinInput && input.WalletAddress {
   482  				outgoingSiacoins = outgoingSiacoins.Add(input.Value)
   483  			}
   484  			if input.FundType == types.SpecifierSiafundInput && input.WalletAddress {
   485  				outgoingSiafunds = outgoingSiafunds.Add(input.Value)
   486  			}
   487  		}
   488  
   489  		// Determine the number of incoming siacoins and siafunds.
   490  		var incomingSiacoins types.Currency
   491  		var incomingSiafunds types.Currency
   492  		for _, output := range txn.Outputs {
   493  			if output.FundType == types.SpecifierMinerPayout {
   494  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   495  			}
   496  			if output.FundType == types.SpecifierSiacoinOutput && output.WalletAddress {
   497  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   498  			}
   499  			if output.FundType == types.SpecifierSiafundOutput && output.WalletAddress {
   500  				incomingSiafunds = incomingSiafunds.Add(output.Value)
   501  			}
   502  		}
   503  
   504  		// Convert the siacoins to a float.
   505  		incomingSiacoinsFloat, _ := new(big.Rat).SetFrac(incomingSiacoins.Big(), types.SiacoinPrecision.Big()).Float64()
   506  		outgoingSiacoinsFloat, _ := new(big.Rat).SetFrac(outgoingSiacoins.Big(), types.SiacoinPrecision.Big()).Float64()
   507  
   508  		// Print the results.
   509  		if uint64(txn.ConfirmationTimestamp) != unconfirmedTransactionTimestamp {
   510  			fmt.Printf(time.Unix(int64(txn.ConfirmationTimestamp), 0).Format("2006-01-02 15:04:05-0700"))
   511  		} else {
   512  			fmt.Printf("             unconfirmed")
   513  		}
   514  		if txn.ConfirmationHeight < 1e9 {
   515  			fmt.Printf("%12v", txn.ConfirmationHeight)
   516  		} else {
   517  			fmt.Printf(" unconfirmed")
   518  		}
   519  		fmt.Printf("%67v%15.2f SC", txn.TransactionID, incomingSiacoinsFloat-outgoingSiacoinsFloat)
   520  		// For siafunds, need to avoid having a negative types.Currency.
   521  		if incomingSiafunds.Cmp(outgoingSiafunds) >= 0 {
   522  			fmt.Printf("%14v SF\n", incomingSiafunds.Sub(outgoingSiafunds))
   523  		} else {
   524  			fmt.Printf("-%14v SF\n", outgoingSiafunds.Sub(incomingSiafunds))
   525  		}
   526  	}
   527  }
   528  
   529  // walletunlockcmd unlocks a saved wallet
   530  func walletunlockcmd() {
   531  	// try reading from environment variable first, then fallback to
   532  	// interactive method. Also allow overriding auto-unlock via -p
   533  	password := os.Getenv("SIA_WALLET_PASSWORD")
   534  	if password != "" && !initPassword {
   535  		fmt.Println("Using SIA_WALLET_PASSWORD environment variable")
   536  		err := httpClient.WalletUnlockPost(password)
   537  		if err != nil {
   538  			fmt.Println("Automatic unlock failed!")
   539  		} else {
   540  			fmt.Println("Wallet unlocked")
   541  			return
   542  		}
   543  	}
   544  	password, err := passwordPrompt("Wallet password: ")
   545  	if err != nil {
   546  		die("Reading password failed:", err)
   547  	}
   548  	err = httpClient.WalletUnlockPost(password)
   549  	if err != nil {
   550  		die("Could not unlock wallet:", err)
   551  	}
   552  }