github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/cmd/spc/walletcmd.go (about)

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