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 }