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 }