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 }