muun-recovery/sweeper.go

157 lines
3.7 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/muun/recovery/electrum"
"github.com/muun/recovery/scanner"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/muun/libwallet"
)
var (
chainParams = chaincfg.MainNetParams
)
type Sweeper struct {
UserKey *libwallet.HDPrivateKey
MuunKey *libwallet.HDPrivateKey
Birthday int
SweepAddress btcutil.Address
}
// RelevantTx contains a PKScipt, an Address an a boolean to check if its spent or not
type RelevantTx struct {
PkScript []byte
Address string
Spent bool
Satoshis int64
SigningDetails signingDetails
Outpoint wire.OutPoint
}
func (tx *RelevantTx) String() string {
return fmt.Sprintf("outpoint %v:%v for %v sats on path %v",
tx.Outpoint.Hash, tx.Outpoint.Index, tx.Satoshis, tx.SigningDetails.Address.DerivationPath())
}
func (s *Sweeper) GetUTXOs() ([]*RelevantTx, error) {
addresses := s.generateAddresses()
results, err := scanner.NewScanner().Scan(addresses)
if err != nil {
return nil, fmt.Errorf("error while scanning addresses: %w", err)
}
txs, err := buildRelevantTxs(results)
if err != nil {
return nil, fmt.Errorf("error while crafting transaction: %w", err)
}
return txs, nil
}
func (s *Sweeper) generateAddresses() chan libwallet.MuunAddress {
ch := make(chan libwallet.MuunAddress)
go func() {
g := NewAddressGenerator(s.UserKey, s.MuunKey)
g.Generate()
for _, details := range g.Addresses() {
ch <- details.Address
}
close(ch)
}()
return ch
}
func (s *Sweeper) GetSweepTxAmountAndWeightInBytes(utxos []*RelevantTx) (outputAmount int64, weightInBytes int64, err error) {
// we build a sweep tx with 0 fee with the only purpose of checking its signed size
zeroFeeSweepTx, err := s.BuildSweepTx(utxos, 0)
if err != nil {
return 0, 0, err
}
outputAmount = zeroFeeSweepTx.TxOut[0].Value
weightInBytes = int64(zeroFeeSweepTx.SerializeSize())
return outputAmount, weightInBytes, nil
}
func (s *Sweeper) BuildSweepTx(utxos []*RelevantTx, fee int64) (*wire.MsgTx, error) {
derivedMuunKey, err := s.MuunKey.DeriveTo("m/1'/1'")
if err != nil {
return nil, err
}
sweepTx := buildSweepTx(utxos, s.SweepAddress, fee)
return buildSignedTx(utxos, sweepTx, s.UserKey, derivedMuunKey)
}
func (s *Sweeper) BroadcastTx(tx *wire.MsgTx) error {
// Connect to an Electurm server using a fresh client and provider pair:
sp := scanner.NewServerProvider() // TODO create servers module, for provider and pool
client := electrum.NewClient()
for !client.IsConnected() {
client.Connect(sp.NextServer())
}
// Encode the transaction for broadcast:
txBytes := new(bytes.Buffer)
err := tx.BtcEncode(txBytes, wire.ProtocolVersion, wire.WitnessEncoding)
if err != nil {
return fmt.Errorf("error while encoding tx: %w", err)
}
txHex := hex.EncodeToString(txBytes.Bytes())
// Do the thing!
_, err = client.Broadcast(txHex)
if err != nil {
return fmt.Errorf("error while broadcasting: %w", err)
}
return nil
}
// buildRelevantTxs prepares the output from Scanner for crafting.
func buildRelevantTxs(utxos []scanner.Utxo) ([]*RelevantTx, error) {
var relevantTxs []*RelevantTx
for _, utxo := range utxos {
address := utxo.Address.Address()
chainHash, err := chainhash.NewHashFromStr(utxo.TxID)
if err != nil {
return nil, err
}
relevantTx := &RelevantTx{
PkScript: utxo.Script,
Address: address,
Spent: false,
Satoshis: int64(utxo.Amount),
SigningDetails: signingDetails{utxo.Address},
Outpoint: wire.OutPoint{
Hash: *chainHash,
Index: uint32(utxo.OutputIndex),
},
}
relevantTxs = append(relevantTxs, relevantTx)
}
return relevantTxs, nil
}