lbcwallet/cmd/sweepaccount/main.go
2021-03-24 14:44:32 +01:00

355 lines
11 KiB
Go

// Copyright (c) 2015-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"golang.org/x/crypto/ssh/terminal"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/internal/cfgutil"
"github.com/btcsuite/btcwallet/netparams"
"github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/jessevdk/go-flags"
)
var (
walletDataDirectory = btcutil.AppDataDir("btcwallet", false)
newlineBytes = []byte{'\n'}
)
func fatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Stderr.Write(newlineBytes)
os.Exit(1)
}
func errContext(err error, context string) error {
return fmt.Errorf("%s: %v", context, err)
}
// Flags.
var opts = struct {
TestNet3 bool `long:"testnet" description:"Use the test bitcoin network (version 3)"`
SimNet bool `long:"simnet" description:"Use the simulation bitcoin network"`
RPCConnect string `short:"c" long:"connect" description:"Hostname[:port] of wallet RPC server"`
RPCUsername string `short:"u" long:"rpcuser" description:"Wallet RPC username"`
RPCCertificateFile string `long:"cafile" description:"Wallet RPC TLS certificate"`
FeeRate *cfgutil.AmountFlag `long:"feerate" description:"Transaction fee per kilobyte"`
SourceAccount string `long:"sourceacct" description:"Account to sweep outputs from"`
DestinationAccount string `long:"destacct" description:"Account to send sweeped outputs to"`
RequiredConfirmations int64 `long:"minconf" description:"Required confirmations to include an output"`
}{
TestNet3: false,
SimNet: false,
RPCConnect: "localhost",
RPCUsername: "",
RPCCertificateFile: filepath.Join(walletDataDirectory, "rpc.cert"),
FeeRate: cfgutil.NewAmountFlag(txrules.DefaultRelayFeePerKb),
SourceAccount: "imported",
DestinationAccount: "default",
RequiredConfirmations: 1,
}
// Parse and validate flags.
func init() {
// Unset localhost defaults if certificate file can not be found.
certFileExists, err := cfgutil.FileExists(opts.RPCCertificateFile)
if err != nil {
fatalf("%v", err)
}
if !certFileExists {
opts.RPCConnect = ""
opts.RPCCertificateFile = ""
}
_, err = flags.Parse(&opts)
if err != nil {
os.Exit(1)
}
if opts.TestNet3 && opts.SimNet {
fatalf("Multiple bitcoin networks may not be used simultaneously")
}
var activeNet = &netparams.MainNetParams
if opts.TestNet3 {
activeNet = &netparams.TestNet3Params
} else if opts.SimNet {
activeNet = &netparams.SimNetParams
}
if opts.RPCConnect == "" {
fatalf("RPC hostname[:port] is required")
}
rpcConnect, err := cfgutil.NormalizeAddress(opts.RPCConnect, activeNet.RPCServerPort)
if err != nil {
fatalf("Invalid RPC network address `%v`: %v", opts.RPCConnect, err)
}
opts.RPCConnect = rpcConnect
if opts.RPCUsername == "" {
fatalf("RPC username is required")
}
certFileExists, err = cfgutil.FileExists(opts.RPCCertificateFile)
if err != nil {
fatalf("%v", err)
}
if !certFileExists {
fatalf("RPC certificate file `%s` not found", opts.RPCCertificateFile)
}
if opts.FeeRate.Amount > 1e6 {
fatalf("Fee rate `%v/kB` is exceptionally high", opts.FeeRate.Amount)
}
if opts.FeeRate.Amount < 1e2 {
fatalf("Fee rate `%v/kB` is exceptionally low", opts.FeeRate.Amount)
}
if opts.SourceAccount == opts.DestinationAccount {
fatalf("Source and destination accounts should not be equal")
}
if opts.RequiredConfirmations < 0 {
fatalf("Required confirmations must be non-negative")
}
}
// noInputValue describes an error returned by the input source when no inputs
// were selected because each previous output value was zero. Callers of
// txauthor.NewUnsignedTransaction need not report these errors to the user.
type noInputValue struct {
}
func (noInputValue) Error() string { return "no input value" }
// makeInputSource creates an InputSource that creates inputs for every unspent
// output with non-zero output values. The target amount is ignored since every
// output is consumed. The InputSource does not return any previous output
// scripts as they are not needed for creating the unsinged transaction and are
// looked up again by the wallet during the call to signrawtransaction.
func makeInputSource(outputs []btcjson.ListUnspentResult) txauthor.InputSource {
var (
totalInputValue btcutil.Amount
inputs = make([]*wire.TxIn, 0, len(outputs))
inputValues = make([]btcutil.Amount, 0, len(outputs))
sourceErr error
)
for _, output := range outputs {
output := output
outputAmount, err := btcutil.NewAmount(output.Amount)
if err != nil {
sourceErr = fmt.Errorf(
"invalid amount `%v` in listunspent result",
output.Amount)
break
}
if outputAmount == 0 {
continue
}
if !saneOutputValue(outputAmount) {
sourceErr = fmt.Errorf(
"impossible output amount `%v` in listunspent result",
outputAmount)
break
}
totalInputValue += outputAmount
previousOutPoint, err := parseOutPoint(&output)
if err != nil {
sourceErr = fmt.Errorf(
"invalid data in listunspent result: %v",
err)
break
}
inputs = append(inputs, wire.NewTxIn(&previousOutPoint, nil, nil))
inputValues = append(inputValues, outputAmount)
}
if sourceErr == nil && totalInputValue == 0 {
sourceErr = noInputValue{}
}
return func(btcutil.Amount) (btcutil.Amount, []*wire.TxIn, []btcutil.Amount, [][]byte, error) {
return totalInputValue, inputs, inputValues, nil, sourceErr
}
}
// makeDestinationScriptSource creates a ChangeSource which is used to receive
// all correlated previous input value. A non-change address is created by this
// function.
func makeDestinationScriptSource(rpcClient *rpcclient.Client, accountName string) txauthor.ChangeSource {
return func() ([]byte, error) {
destinationAddress, err := rpcClient.GetNewAddress(accountName)
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(destinationAddress)
}
}
func main() {
err := sweep()
if err != nil {
fatalf("%v", err)
}
}
func sweep() error {
rpcPassword, err := promptSecret("Wallet RPC password")
if err != nil {
return errContext(err, "failed to read RPC password")
}
// Open RPC client.
rpcCertificate, err := ioutil.ReadFile(opts.RPCCertificateFile)
if err != nil {
return errContext(err, "failed to read RPC certificate")
}
rpcClient, err := rpcclient.New(&rpcclient.ConnConfig{
Host: opts.RPCConnect,
User: opts.RPCUsername,
Pass: rpcPassword,
Certificates: rpcCertificate,
HTTPPostMode: true,
}, nil)
if err != nil {
return errContext(err, "failed to create RPC client")
}
defer rpcClient.Shutdown()
// Fetch all unspent outputs, ignore those not from the source
// account, and group by their destination address. Each grouping of
// outputs will be used as inputs for a single transaction sending to a
// new destination account address.
unspentOutputs, err := rpcClient.ListUnspent()
if err != nil {
return errContext(err, "failed to fetch unspent outputs")
}
sourceOutputs := make(map[string][]btcjson.ListUnspentResult)
for _, unspentOutput := range unspentOutputs {
if !unspentOutput.Spendable {
continue
}
if unspentOutput.Confirmations < opts.RequiredConfirmations {
continue
}
if unspentOutput.Account != opts.SourceAccount {
continue
}
sourceAddressOutputs := sourceOutputs[unspentOutput.Address]
sourceOutputs[unspentOutput.Address] = append(sourceAddressOutputs, unspentOutput)
}
var privatePassphrase string
if len(sourceOutputs) != 0 {
privatePassphrase, err = promptSecret("Wallet private passphrase")
if err != nil {
return errContext(err, "failed to read private passphrase")
}
}
var totalSwept btcutil.Amount
var numErrors int
var reportError = func(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Stderr.Write(newlineBytes)
numErrors++
}
for _, previousOutputs := range sourceOutputs {
inputSource := makeInputSource(previousOutputs)
destinationSource := makeDestinationScriptSource(rpcClient, opts.DestinationAccount)
tx, err := txauthor.NewUnsignedTransaction(nil, opts.FeeRate.Amount,
inputSource, destinationSource)
if err != nil {
if err != (noInputValue{}) {
reportError("Failed to create unsigned transaction: %v", err)
}
continue
}
// Unlock the wallet, sign the transaction, and immediately lock.
err = rpcClient.WalletPassphrase(privatePassphrase, 60)
if err != nil {
reportError("Failed to unlock wallet: %v", err)
continue
}
signedTransaction, complete, err := rpcClient.SignRawTransaction(tx.Tx)
_ = rpcClient.WalletLock()
if err != nil {
reportError("Failed to sign transaction: %v", err)
continue
}
if !complete {
reportError("Failed to sign every input")
continue
}
// Publish the signed sweep transaction.
txHash, err := rpcClient.SendRawTransaction(signedTransaction, false)
if err != nil {
reportError("Failed to publish transaction: %v", err)
continue
}
outputAmount := btcutil.Amount(tx.Tx.TxOut[0].Value)
fmt.Printf("Swept %v to destination account with transaction %v\n",
outputAmount, txHash)
totalSwept += outputAmount
}
numPublished := len(sourceOutputs) - numErrors
transactionNoun := pickNoun(numErrors, "transaction", "transactions")
if numPublished != 0 {
fmt.Printf("Swept %v to destination account across %d %s\n",
totalSwept, numPublished, transactionNoun)
}
if numErrors > 0 {
return fmt.Errorf("failed to publish %d %s", numErrors,
transactionNoun)
}
return nil
}
func promptSecret(what string) (string, error) {
fmt.Printf("%s: ", what)
fd := int(os.Stdin.Fd())
input, err := terminal.ReadPassword(fd)
fmt.Println()
if err != nil {
return "", err
}
return string(input), nil
}
func saneOutputValue(amount btcutil.Amount) bool {
return amount >= 0 && amount <= btcutil.MaxSatoshi
}
func parseOutPoint(input *btcjson.ListUnspentResult) (wire.OutPoint, error) {
txHash, err := chainhash.NewHashFromStr(input.TxID)
if err != nil {
return wire.OutPoint{}, err
}
return wire.OutPoint{Hash: *txHash, Index: input.Vout}, nil
}
func pickNoun(n int, singularForm, pluralForm string) string {
if n == 1 {
return singularForm
}
return pluralForm
}