muun-recovery/recovery_tool.go

348 lines
8.2 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/muun/libwallet"
)
func main() {
chainService, close, _ := startChainService()
defer close()
printWelcomeMessage()
recoveryCode := readRecoveryCode()
userRawKey := readKey("first encrypted private key", 147)
userKey := buildExtendedKey(userRawKey, recoveryCode)
userKey.Key.Path = "m/1'/1'"
muunRawKey := readKey("second encrypted private key", 147)
muunKey := buildExtendedKey(muunRawKey, recoveryCode)
derivedMuunKey, err := muunKey.Key.DeriveTo("m/1'/1'")
if err != nil {
printError(err)
}
sweepAddress := readSweepAddress()
fmt.Println("")
fmt.Println("Starting to scan the blockchain. This may take a while.")
g := NewAddressGenerator(userKey.Key, muunKey.Key)
g.Generate()
birthday := muunKey.Birthday
if birthday == 0xFFFF {
birthday = 0
}
utxos := startRescan(chainService, g.Addresses(), birthday)
fmt.Println("")
if len(utxos) > 0 {
fmt.Printf("The recovery tool has found the following utxos: %v", utxos)
} else {
fmt.Printf("No utxos found")
fmt.Println()
return
}
fmt.Println()
// This is fun:
// First we build a sweep tx with 0 fee with the only purpouse of seeing its signed size
zeroFeehexSweepTx := buildSweepTx(utxos, sweepAddress, 0)
zeroFeeSweepTx, err := buildSignedTx(utxos, zeroFeehexSweepTx, userKey.Key, derivedMuunKey)
if err != nil {
printError(err)
}
weightInBytes := int64(zeroFeeSweepTx.SerializeSize())
fee := readFee(zeroFeeSweepTx.TxOut[0].Value, weightInBytes)
// Then we re-build the sweep tx with the actual fee
hexSweepTx := buildSweepTx(utxos, sweepAddress, fee)
tx, err := buildSignedTx(utxos, hexSweepTx, userKey.Key, derivedMuunKey)
if err != nil {
printError(err)
}
fmt.Println("Transaction ready to be sent")
err = chainService.SendTransaction(tx)
if err != nil {
printError(err)
}
fmt.Printf("Transaction sent! You can check the status here: https://blockstream.info/tx/%v", tx.TxHash().String())
fmt.Println("")
fmt.Printf("If you have any feedback, feel free to share it with us. Our email is contact@muun.com")
fmt.Println("")
}
func buildSweepTx(utxos []*RelevantTx, sweepAddress btcutil.Address, fee int64) string {
tx := wire.NewMsgTx(2)
value := int64(0)
for _, utxo := range utxos {
tx.AddTxIn(wire.NewTxIn(&utxo.Outpoint, []byte{}, [][]byte{}))
value += utxo.Satoshis
}
fmt.Println()
fmt.Printf("Total balance in satoshis: %v", value)
fmt.Println()
value -= fee
script, err := txscript.PayToAddrScript(sweepAddress)
if err != nil {
printError(err)
}
tx.AddTxOut(wire.NewTxOut(value, script))
writer := &bytes.Buffer{}
err = tx.Serialize(writer)
if err != nil {
panic(err)
}
if fee != 0 {
readConfirmation(value, fee, sweepAddress.String())
}
return hex.EncodeToString(writer.Bytes())
}
func buildSignedTx(utxos []*RelevantTx, hexSweepTx string, userKey *libwallet.HDPrivateKey,
muunKey *libwallet.HDPrivateKey) (*wire.MsgTx, error) {
pstx, err := libwallet.NewPartiallySignedTransaction(hexSweepTx)
if err != nil {
printError(err)
}
for index, utxo := range utxos {
input := &input{
utxo,
[]byte{},
}
pstx.AddInput(input)
sig, err := pstx.MuunSignatureForInput(index, userKey.PublicKey(), muunKey)
if err != nil {
panic(err)
}
input.muunSignature = sig
}
signedTx, err := pstx.Sign(userKey, muunKey.PublicKey())
if err != nil {
return nil, err
}
wireTx := wire.NewMsgTx(0)
wireTx.BtcDecode(bytes.NewReader(signedTx.Bytes), 0, wire.WitnessEncoding)
return wireTx, nil
}
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 swipe all the balance in your muun account to an")
fmt.Println("address of your choosing.")
fmt.Println("")
fmt.Println("To do this you will need:")
fmt.Println("* The recovery code, that you set up when you created your muun account")
fmt.Println("* The two encrypted private keys that you exported from your muun wallet")
fmt.Println("* A destination bitcoin address where all your funds will be sent")
fmt.Println("")
fmt.Println("If you have any questions, contact us at contact@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("Wrong recovery code, remember to add the '-' separator between the 4 characters chunks")
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, characters int) string {
fmt.Println("")
fmt.Printf("Enter your %v", keyType)
fmt.Println()
fmt.Println("(it looks like this: '9xzpc7y6sNtRvh8Fh...')")
fmt.Print("> ")
var userInput string
fmt.Scan(&userInput)
userInput = strings.TrimSpace(userInput)
if len(userInput) != characters {
fmt.Printf("Your %v must have %v characters", keyType, characters)
fmt.Println("")
fmt.Println("Please, try again")
return readKey(keyType, characters)
}
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)
}
type input struct {
tx *RelevantTx
muunSignature []byte
}
func (i *input) OutPoint() libwallet.Outpoint {
return &outpoint{tx: i.tx}
}
func (i *input) Address() libwallet.MuunAddress {
return i.tx.SigningDetails.Address
}
func (i *input) UserSignature() []byte {
return []byte{}
}
func (i *input) MuunSignature() []byte {
return i.muunSignature
}
func (i *input) SubmarineSwap() libwallet.InputSubmarineSwap {
return nil
}
type outpoint struct {
tx *RelevantTx
}
func (o *outpoint) TxId() []byte {
return o.tx.Outpoint.Hash.CloneBytes()
}
func (o *outpoint) Index() int {
return int(o.tx.Outpoint.Index)
}
func (o *outpoint) Amount() int64 {
return o.tx.Satoshis
}