github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/cmd/siac/walletcmd.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math/big"
     7  	"os"
     8  	"syscall"
     9  	"time"
    10  
    11  	"github.com/spf13/cobra"
    12  	"golang.org/x/crypto/ssh/terminal"
    13  
    14  	"github.com/Synthesix/Sia/node/api"
    15  	"github.com/Synthesix/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 miner fee of 10 SC is levied on all transactions.`,
   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 := new(api.WalletAddressGET)
   210  	err := getAPI("/wallet/address", addr)
   211  	if err != nil {
   212  		die("Could not generate new address:", err)
   213  	}
   214  	fmt.Printf("Created new address: %s\n", addr.Address)
   215  }
   216  
   217  // walletaddressescmd fetches the list of addresses that the wallet knows.
   218  func walletaddressescmd() {
   219  	addrs := new(api.WalletAddressesGET)
   220  	err := getAPI("/wallet/addresses", addrs)
   221  	if err != nil {
   222  		die("Failed to fetch addresses:", err)
   223  	}
   224  	for _, addr := range addrs.Addresses {
   225  		fmt.Println(addr)
   226  	}
   227  }
   228  
   229  // walletchangepasswordcmd changes the password of the wallet.
   230  func walletchangepasswordcmd() {
   231  	currentPassword, err := passwordPrompt(currentPasswordText)
   232  	if err != nil {
   233  		die("Reading password failed:", err)
   234  	}
   235  	newPassword, err := passwordPrompt(newPasswordText)
   236  	if err != nil {
   237  		die("Reading password failed:", err)
   238  	} else if err = confirmPassword(newPassword); err != nil {
   239  		die(err)
   240  	}
   241  	qs := fmt.Sprintf("newpassword=%s&encryptionpassword=%s", newPassword, currentPassword)
   242  	err = post("/wallet/changepassword", qs)
   243  	if err != nil {
   244  		die("Changing the password failed:", err)
   245  	}
   246  	fmt.Println("Password changed successfully.")
   247  }
   248  
   249  // walletinitcmd encrypts the wallet with the given password
   250  func walletinitcmd() {
   251  	var er api.WalletInitPOST
   252  	qs := fmt.Sprintf("dictionary=%s", "english")
   253  	if initPassword {
   254  		password, err := passwordPrompt("Wallet password: ")
   255  		if err != nil {
   256  			die("Reading password failed:", err)
   257  		} else if err = confirmPassword(password); err != nil {
   258  			die(err)
   259  		}
   260  		qs += fmt.Sprintf("&encryptionpassword=%s", password)
   261  	}
   262  	if initForce {
   263  		qs += "&force=true"
   264  	}
   265  	err := postResp("/wallet/init", qs, &er)
   266  	if err != nil {
   267  		die("Error when encrypting wallet:", err)
   268  	}
   269  	fmt.Printf("Recovery seed:\n%s\n\n", er.PrimarySeed)
   270  	if initPassword {
   271  		fmt.Printf("Wallet encrypted with given password\n")
   272  	} else {
   273  		fmt.Printf("Wallet encrypted with password:\n%s\n", er.PrimarySeed)
   274  	}
   275  }
   276  
   277  // walletinitseedcmd initializes the wallet from a preexisting seed.
   278  func walletinitseedcmd() {
   279  	seed, err := passwordPrompt("Seed: ")
   280  	if err != nil {
   281  		die("Reading seed failed:", err)
   282  	}
   283  	qs := fmt.Sprintf("&seed=%s&dictionary=%s", seed, "english")
   284  	if initPassword {
   285  		password, err := passwordPrompt("Wallet password: ")
   286  		if err != nil {
   287  			die("Reading password failed:", err)
   288  		} else if err = confirmPassword(password); err != nil {
   289  			die(err)
   290  		}
   291  		qs += fmt.Sprintf("&encryptionpassword=%s", password)
   292  	}
   293  	if initForce {
   294  		qs += "&force=true"
   295  	}
   296  	err = post("/wallet/init/seed", qs)
   297  	if err != nil {
   298  		die("Could not initialize wallet from seed:", err)
   299  	}
   300  	if initPassword {
   301  		fmt.Println("Wallet initialized and encrypted with given password.")
   302  	} else {
   303  		fmt.Println("Wallet initialized and encrypted with seed.")
   304  	}
   305  }
   306  
   307  // walletload033xcmd loads a v0.3.3.x wallet into the current wallet.
   308  func walletload033xcmd(source string) {
   309  	password, err := passwordPrompt(askPasswordText)
   310  	if err != nil {
   311  		die("Reading password failed:", err)
   312  	}
   313  	qs := fmt.Sprintf("source=%s&encryptionpassword=%s", abs(source), password)
   314  	err = post("/wallet/033x", qs)
   315  	if err != nil {
   316  		die("Loading wallet failed:", err)
   317  	}
   318  	fmt.Println("Wallet loading successful.")
   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  	qs := fmt.Sprintf("encryptionpassword=%s&seed=%s&dictionary=%s", password, seed, "english")
   332  	err = post("/wallet/seed", qs)
   333  	if err != nil {
   334  		die("Could not add seed:", err)
   335  	}
   336  	fmt.Println("Added Key")
   337  }
   338  
   339  // walletloadsiagcmd loads a siag key set into the wallet.
   340  func walletloadsiagcmd(keyfiles string) {
   341  	password, err := passwordPrompt(askPasswordText)
   342  	if err != nil {
   343  		die("Reading password failed:", err)
   344  	}
   345  	qs := fmt.Sprintf("keyfiles=%s&encryptionpassword=%s", keyfiles, password)
   346  	err = post("/wallet/siagkey", qs)
   347  	if err != nil {
   348  		die("Loading siag key failed:", err)
   349  	}
   350  	fmt.Println("Wallet loading successful.")
   351  }
   352  
   353  // walletlockcmd locks the wallet
   354  func walletlockcmd() {
   355  	err := post("/wallet/lock", "")
   356  	if err != nil {
   357  		die("Could not lock wallet:", err)
   358  	}
   359  }
   360  
   361  // walletseedcmd returns the current seed {
   362  func walletseedscmd() {
   363  	var seedInfo api.WalletSeedsGET
   364  	err := getAPI("/wallet/seeds", &seedInfo)
   365  	if err != nil {
   366  		die("Error retrieving the current seed:", err)
   367  	}
   368  	fmt.Println("Primary Seed:")
   369  	fmt.Println(seedInfo.PrimarySeed)
   370  	if len(seedInfo.AllSeeds) == 1 {
   371  		// AllSeeds includes the primary seed
   372  		return
   373  	}
   374  	fmt.Println()
   375  	fmt.Println("Auxiliary Seeds:")
   376  	for _, seed := range seedInfo.AllSeeds {
   377  		if seed == seedInfo.PrimarySeed {
   378  			continue
   379  		}
   380  		fmt.Println() // extra newline for readability
   381  		fmt.Println(seed)
   382  	}
   383  }
   384  
   385  // walletsendsiacoinscmd sends siacoins to a destination address.
   386  func walletsendsiacoinscmd(amount, dest string) {
   387  	hastings, err := parseCurrency(amount)
   388  	if err != nil {
   389  		die("Could not parse amount:", err)
   390  	}
   391  	err = post("/wallet/siacoins", fmt.Sprintf("amount=%s&destination=%s", hastings, dest))
   392  	if err != nil {
   393  		die("Could not send siacoins:", err)
   394  	}
   395  	fmt.Printf("Sent %s hastings to %s\n", hastings, dest)
   396  }
   397  
   398  // walletsendsiafundscmd sends siafunds to a destination address.
   399  func walletsendsiafundscmd(amount, dest string) {
   400  	err := post("/wallet/siafunds", fmt.Sprintf("amount=%s&destination=%s", amount, dest))
   401  	if err != nil {
   402  		die("Could not send siafunds:", err)
   403  	}
   404  	fmt.Printf("Sent %s siafunds to %s\n", amount, dest)
   405  }
   406  
   407  // walletbalancecmd retrieves and displays information about the wallet.
   408  func walletbalancecmd() {
   409  	status := new(api.WalletGET)
   410  	err := getAPI("/wallet", status)
   411  	if err != nil {
   412  		die("Could not get wallet status:", err)
   413  	}
   414  	var fees api.TpoolFeeGET
   415  	err = getAPI("/tpool/fee", &fees)
   416  	if err != nil {
   417  		die("Could not get fee estimation:", err)
   418  	}
   419  	encStatus := "Unencrypted"
   420  	if status.Encrypted {
   421  		encStatus = "Encrypted"
   422  	}
   423  	if !status.Unlocked {
   424  		fmt.Printf(`Wallet status:
   425  %v, Locked
   426  Unlock the wallet to view balance
   427  `, encStatus)
   428  		return
   429  	}
   430  
   431  	unconfirmedBalance := status.ConfirmedSiacoinBalance.Add(status.UnconfirmedIncomingSiacoins).Sub(status.UnconfirmedOutgoingSiacoins)
   432  	var delta string
   433  	if unconfirmedBalance.Cmp(status.ConfirmedSiacoinBalance) >= 0 {
   434  		delta = "+" + currencyUnits(unconfirmedBalance.Sub(status.ConfirmedSiacoinBalance))
   435  	} else {
   436  		delta = "-" + currencyUnits(status.ConfirmedSiacoinBalance.Sub(unconfirmedBalance))
   437  	}
   438  
   439  	fmt.Printf(`Wallet status:
   440  %s, Unlocked
   441  Height:              %v
   442  Confirmed Balance:   %v
   443  Unconfirmed Delta:  %v
   444  Exact:               %v H
   445  Siafunds:            %v SF
   446  Siafund Claims:      %v H
   447  
   448  Estimated Fee:       %v / KB
   449  `, encStatus, status.Height, currencyUnits(status.ConfirmedSiacoinBalance), delta,
   450  		status.ConfirmedSiacoinBalance, status.SiafundBalance, status.SiacoinClaimBalance,
   451  		fees.Maximum.Mul64(1e3).HumanString())
   452  }
   453  
   454  // walletsweepcmd sweeps coins and funds from a seed.
   455  func walletsweepcmd() {
   456  	seed, err := passwordPrompt("Seed: ")
   457  	if err != nil {
   458  		die("Reading seed failed:", err)
   459  	}
   460  
   461  	var swept api.WalletSweepPOST
   462  	err = postResp("/wallet/sweep/seed", fmt.Sprintf("seed=%s&dictionary=%s", seed, "english"), &swept)
   463  	if err != nil {
   464  		die("Could not sweep seed:", err)
   465  	}
   466  	fmt.Printf("Swept %v and %v SF from seed.\n", currencyUnits(swept.Coins), swept.Funds)
   467  }
   468  
   469  // wallettransactionscmd lists all of the transactions related to the wallet,
   470  // providing a net flow of siacoins and siafunds for each.
   471  func wallettransactionscmd() {
   472  	wtg := new(api.WalletTransactionsGET)
   473  	err := getAPI("/wallet/transactions?startheight=0&endheight=10000000", wtg)
   474  	if err != nil {
   475  		die("Could not fetch transaction history:", err)
   476  	}
   477  	fmt.Println("             [timestamp]    [height]                                                   [transaction id]    [net siacoins]   [net siafunds]")
   478  	txns := append(wtg.ConfirmedTransactions, wtg.UnconfirmedTransactions...)
   479  	for _, txn := range txns {
   480  		// Determine the number of outgoing siacoins and siafunds.
   481  		var outgoingSiacoins types.Currency
   482  		var outgoingSiafunds types.Currency
   483  		for _, input := range txn.Inputs {
   484  			if input.FundType == types.SpecifierSiacoinInput && input.WalletAddress {
   485  				outgoingSiacoins = outgoingSiacoins.Add(input.Value)
   486  			}
   487  			if input.FundType == types.SpecifierSiafundInput && input.WalletAddress {
   488  				outgoingSiafunds = outgoingSiafunds.Add(input.Value)
   489  			}
   490  		}
   491  
   492  		// Determine the number of incoming siacoins and siafunds.
   493  		var incomingSiacoins types.Currency
   494  		var incomingSiafunds types.Currency
   495  		for _, output := range txn.Outputs {
   496  			if output.FundType == types.SpecifierMinerPayout {
   497  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   498  			}
   499  			if output.FundType == types.SpecifierSiacoinOutput && output.WalletAddress {
   500  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   501  			}
   502  			if output.FundType == types.SpecifierSiafundOutput && output.WalletAddress {
   503  				incomingSiafunds = incomingSiafunds.Add(output.Value)
   504  			}
   505  		}
   506  
   507  		// Convert the siacoins to a float.
   508  		incomingSiacoinsFloat, _ := new(big.Rat).SetFrac(incomingSiacoins.Big(), types.SiacoinPrecision.Big()).Float64()
   509  		outgoingSiacoinsFloat, _ := new(big.Rat).SetFrac(outgoingSiacoins.Big(), types.SiacoinPrecision.Big()).Float64()
   510  
   511  		// Print the results.
   512  		if uint64(txn.ConfirmationTimestamp) != unconfirmedTransactionTimestamp {
   513  			fmt.Printf(time.Unix(int64(txn.ConfirmationTimestamp), 0).Format("2006-01-02 15:04:05-0700"))
   514  		} else {
   515  			fmt.Printf("             unconfirmed")
   516  		}
   517  		if txn.ConfirmationHeight < 1e9 {
   518  			fmt.Printf("%12v", txn.ConfirmationHeight)
   519  		} else {
   520  			fmt.Printf(" unconfirmed")
   521  		}
   522  		fmt.Printf("%67v%15.2f SC", txn.TransactionID, incomingSiacoinsFloat-outgoingSiacoinsFloat)
   523  		// For siafunds, need to avoid having a negative types.Currency.
   524  		if incomingSiafunds.Cmp(outgoingSiafunds) >= 0 {
   525  			fmt.Printf("%14v SF\n", incomingSiafunds.Sub(outgoingSiafunds))
   526  		} else {
   527  			fmt.Printf("-%14v SF\n", outgoingSiafunds.Sub(incomingSiafunds))
   528  		}
   529  	}
   530  }
   531  
   532  // walletunlockcmd unlocks a saved wallet
   533  func walletunlockcmd() {
   534  	// try reading from environment variable first, then fallback to
   535  	// interactive method. Also allow overriding auto-unlock via -p
   536  	password := os.Getenv("SIA_WALLET_PASSWORD")
   537  	if password != "" && !initPassword {
   538  		fmt.Println("Using SIA_WALLET_PASSWORD environment variable")
   539  		qs := fmt.Sprintf("encryptionpassword=%s&dictonary=%s", password, "english")
   540  		err := post("/wallet/unlock", qs)
   541  		if err != nil {
   542  			fmt.Println("Automatic unlock failed!")
   543  		} else {
   544  			fmt.Println("Wallet unlocked")
   545  			return
   546  		}
   547  	}
   548  	password, err := passwordPrompt("Wallet password: ")
   549  	if err != nil {
   550  		die("Reading password failed:", err)
   551  	}
   552  	qs := fmt.Sprintf("encryptionpassword=%s&dictonary=%s", password, "english")
   553  	err = post("/wallet/unlock", qs)
   554  	if err != nil {
   555  		die("Could not unlock wallet:", err)
   556  	}
   557  }