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 }