gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/walletcmd.go (about)

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