muun-recovery/main.go

252 lines
6.6 KiB
Go

package main
import (
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/btcsuite/btcutil"
"github.com/muun/libwallet"
)
func main() {
printWelcomeMessage()
recoveryCode := readRecoveryCode()
userRawKey := readKey("first encrypted private key")
muunRawKey := readKey("second encrypted private key")
userKey, muunKey := buildExtendedKeys(userRawKey, muunRawKey, recoveryCode)
userKey.Key.Path = "m/1'/1'"
sweepAddress := readSweepAddress()
fmt.Println("")
fmt.Println("\nStarting scan of all your addresses. This may take a while")
sweeper := Sweeper{
UserKey: userKey.Key,
MuunKey: muunKey.Key,
Birthday: muunKey.Birthday,
SweepAddress: sweepAddress,
}
utxos, err := sweeper.GetUTXOs()
if err != nil {
exitWithError(err)
}
fmt.Println("")
if len(utxos) > 0 {
fmt.Printf("The recovery tool has found the following confirmed UTXOs:\n%v", utxos)
} else {
fmt.Printf("No confirmed UTXOs found")
fmt.Println()
return
}
fmt.Println()
txOutputAmount, txWeightInBytes, err := sweeper.GetSweepTxAmountAndWeightInBytes(utxos)
if err != nil {
printError(err)
}
fee := readFee(txOutputAmount, txWeightInBytes)
// Then we re-build the sweep tx with the actual fee
sweepTx, err := sweeper.BuildSweepTx(utxos, fee)
if err != nil {
printError(err)
}
fmt.Println("Transaction ready to be sent")
err = sweeper.BroadcastTx(sweepTx)
if err != nil {
printError(err)
}
fmt.Printf("Transaction sent! You can check the status here: https://blockstream.info/tx/%v", sweepTx.TxHash().String())
fmt.Println("")
fmt.Printf("We appreciate all kinds of feedback. If you have any, send it to contact@muun.com")
fmt.Println("")
}
func printError(err error) {
log.Printf("The recovery tool failed with the following error: %v", err.Error())
log.Printf("")
log.Printf("You can try again or contact us at support@muun.com")
panic(err)
}
func printWelcomeMessage() {
fmt.Println("Welcome to Muun's Recovery Tool")
fmt.Println("")
fmt.Println("You can use this tool to transfer all funds from your Muun account to an")
fmt.Println("address of your choosing.")
fmt.Println("")
fmt.Println("To do this you will need:")
fmt.Println("1. Your Recovery Code, which you wrote down during your security setup")
fmt.Println("2. Your two encrypted private keys, which you exported from your wallet")
fmt.Println("3. A destination bitcoin address where all your funds will be sent")
fmt.Println("")
fmt.Println("If you have any questions, we'll be happy to answer them. Contact us at support@muun.com")
fmt.Println("")
}
func readRecoveryCode() string {
fmt.Println("")
fmt.Printf("Enter your Recovery Code")
fmt.Println()
fmt.Println("(it looks like this: 'ABCD-1234-POW2-R561-P120-JK26-12RW-45TT')")
fmt.Print("> ")
var userInput string
fmt.Scan(&userInput)
userInput = strings.TrimSpace(userInput)
finalRC := strings.ToUpper(userInput)
if strings.Count(finalRC, "-") != 7 {
fmt.Printf("Invalid recovery code. Did you add the '-' separator between each 4-characters segment?")
fmt.Println()
fmt.Println("Please, try again")
return readRecoveryCode()
}
if len(finalRC) != 39 {
fmt.Println("Your recovery code must have 39 characters")
fmt.Println("Please, try again")
return readRecoveryCode()
}
return finalRC
}
func readKey(keyType string) string {
fmt.Println("")
fmt.Printf("Enter your %v", keyType)
fmt.Println()
fmt.Println("(it looks like this: '9xzpc7y6sNtRvh8Fh...')")
fmt.Print("> ")
// NOTE:
// Users will most likely copy and paste their keys from the Emergency Kit PDF. In this case,
// input will come suddenly in multiple lines, so a simple scan & retry (let's say 3 lines
// were pasted) will attempt to parse a key and fail 2 times in a row, with leftover characters
// until the user presses enter to fail for a 3rd time.
// Given the line lengths actually found in our Emergency Kits, we have a simple solution for now:
// scan a minimum length of characters. Pasing from current versions of the Emergency Kit will
// only go past a minimum length when the key being entered is complete, in all cases.
userInput := scanMultiline(libwallet.EncodedKeyLengthLegacy)
if len(userInput) < libwallet.EncodedKeyLengthLegacy {
// This is obviously invalid. Other problems will be detected later on, during the actual
// decoding and decryption stage.
fmt.Println("The key you entered doesn't look valid\nPlease, try again")
return readKey(keyType)
}
return userInput
}
func readSweepAddress() btcutil.Address {
fmt.Println("")
fmt.Println("Enter your destination bitcoin address")
fmt.Print("> ")
var userInput string
fmt.Scan(&userInput)
userInput = strings.TrimSpace(userInput)
addr, err := btcutil.DecodeAddress(userInput, &chainParams)
if err != nil {
fmt.Println("This is not a valid bitcoin address")
fmt.Println("")
fmt.Println("Please, try again")
return readSweepAddress()
}
return addr
}
func readFee(totalBalance, weight int64) int64 {
fmt.Println("")
fmt.Printf("Enter the fee in satoshis per byte. Tx weight: %v bytes. You can check the status of the mempool here: https://bitcoinfees.earn.com/#fees", weight)
fmt.Println()
fmt.Println("(Example: 5)")
fmt.Print("> ")
var userInput string
fmt.Scan(&userInput)
feeInSatsPerByte, err := strconv.ParseInt(userInput, 10, 64)
if err != nil || feeInSatsPerByte <= 0 {
fmt.Printf("The fee must be a number")
fmt.Println("")
fmt.Println("Please, try again")
return readFee(totalBalance, weight)
}
totalFee := feeInSatsPerByte * weight
if totalBalance-totalFee < 546 {
fmt.Printf("The fee is too high. The amount left must be higher than dust")
fmt.Println("")
fmt.Println("Please, try again")
return readFee(totalBalance, weight)
}
return totalFee
}
func readConfirmation(value, fee int64, address string) {
fmt.Println("")
fmt.Printf("About to send %v satoshis with fee: %v satoshis to %v", value, fee, address)
fmt.Println()
fmt.Println("Confirm? (y/n)")
fmt.Print("> ")
var userInput string
fmt.Scan(&userInput)
if userInput == "y" || userInput == "Y" {
return
}
if userInput == "n" || userInput == "N" {
log.Println()
log.Printf("Recovery tool stopped")
log.Println()
log.Printf("You can try again or contact us at support@muun.com")
os.Exit(1)
}
fmt.Println()
fmt.Println("You can only enter 'y' to accept or 'n' to cancel")
readConfirmation(value, fee, address)
}
func scanMultiline(minChars int) string {
var result strings.Builder
for result.Len() < minChars {
var line string
fmt.Scan(&line)
result.WriteString(strings.TrimSpace(line))
}
return result.String()
}
func exitWithError(reason error) {
fmt.Println("\nError while scanning. Can't continue. Please, try again later.")
os.Exit(1)
}