decred.org/dcrwallet/v3@v3.1.0/cmd/sweepaccount/main.go (about)

     1  // Copyright (c) 2015-2016 The btcsuite developers
     2  // Copyright (c) 2016-2020 The Decred developers
     3  // Use of this source code is governed by an ISC
     4  // license that can be found in the LICENSE file.
     5  
     6  package main
     7  
     8  import (
     9  	"context"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"errors"
    13  	"fmt"
    14  	"net"
    15  	"os"
    16  	"path/filepath"
    17  
    18  	"decred.org/dcrwallet/v3/rpc/jsonrpc/types"
    19  	"decred.org/dcrwallet/v3/wallet/txauthor"
    20  	"decred.org/dcrwallet/v3/wallet/txrules"
    21  	"github.com/decred/dcrd/chaincfg/chainhash"
    22  	"github.com/decred/dcrd/chaincfg/v3"
    23  	"github.com/decred/dcrd/dcrutil/v4"
    24  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    25  	"github.com/decred/dcrd/wire"
    26  	"github.com/jessevdk/go-flags"
    27  	"github.com/jrick/wsrpc/v2"
    28  	"golang.org/x/term"
    29  )
    30  
    31  var (
    32  	activeNet           = chaincfg.MainNetParams()
    33  	walletDataDirectory = dcrutil.AppDataDir("dcrwallet", false)
    34  	newlineBytes        = []byte{'\n'}
    35  )
    36  
    37  func fatalf(format string, args ...interface{}) {
    38  	fmt.Fprintf(os.Stderr, format, args...)
    39  	os.Stderr.Write(newlineBytes)
    40  	os.Exit(1)
    41  }
    42  
    43  func errContext(err error, context string) error {
    44  	return fmt.Errorf("%s: %v", context, err)
    45  }
    46  
    47  // Flags.
    48  var opts = struct {
    49  	TestNet               bool    `long:"testnet" description:"Use the test decred network"`
    50  	SimNet                bool    `long:"simnet" description:"Use the simulation decred network"`
    51  	RPCConnect            string  `short:"c" long:"connect" description:"Hostname[:port] of wallet RPC server"`
    52  	RPCUsername           string  `short:"u" long:"rpcuser" description:"Wallet RPC username"`
    53  	RPCPassword           string  `short:"P" long:"rpcpass" description:"Wallet RPC password"`
    54  	RPCCertificateFile    string  `long:"cafile" description:"Wallet RPC TLS certificate"`
    55  	FeeRate               float64 `long:"feerate" description:"Transaction fee per kilobyte"`
    56  	SourceAccount         string  `long:"sourceacct" description:"Account to sweep outputs from"`
    57  	SourceAddress         string  `long:"sourceaddr" description:"Address to sweep outputs from"`
    58  	DestinationAccount    string  `long:"destacct" description:"Account to send sweeped outputs to"`
    59  	DestinationAddress    string  `long:"destaddr" description:"Address to send sweeped outputs to"`
    60  	RequiredConfirmations int64   `long:"minconf" description:"Required confirmations to include an output"`
    61  	DryRun                bool    `long:"dryrun" description:"Do not actually send any transactions but output what would have happened"`
    62  }{
    63  	TestNet:               false,
    64  	SimNet:                false,
    65  	RPCConnect:            "localhost",
    66  	RPCUsername:           "",
    67  	RPCPassword:           "",
    68  	RPCCertificateFile:    filepath.Join(walletDataDirectory, "rpc.cert"),
    69  	FeeRate:               txrules.DefaultRelayFeePerKb.ToCoin(),
    70  	SourceAccount:         "",
    71  	SourceAddress:         "",
    72  	DestinationAccount:    "",
    73  	DestinationAddress:    "",
    74  	RequiredConfirmations: 2,
    75  	DryRun:                false,
    76  }
    77  
    78  // normalizeAddress returns the normalized form of the address, adding a default
    79  // port if necessary.  An error is returned if the address, even without a port,
    80  // is not valid.
    81  func normalizeAddress(addr string, defaultPort string) (hostport string, err error) {
    82  	// If the first SplitHostPort errors because of a missing port and not
    83  	// for an invalid host, add the port.  If the second SplitHostPort
    84  	// fails, then a port is not missing and the original error should be
    85  	// returned.
    86  	host, port, origErr := net.SplitHostPort(addr)
    87  	if origErr == nil {
    88  		return net.JoinHostPort(host, port), nil
    89  	}
    90  	addr = net.JoinHostPort(addr, defaultPort)
    91  	_, _, err = net.SplitHostPort(addr)
    92  	if err != nil {
    93  		return "", origErr
    94  	}
    95  	return addr, nil
    96  }
    97  
    98  func walletPort(net *chaincfg.Params) string {
    99  	switch net.Net {
   100  	case wire.MainNet:
   101  		return "9110"
   102  	case wire.TestNet3:
   103  		return "19110"
   104  	case wire.SimNet:
   105  		return "19557"
   106  	default:
   107  		return ""
   108  	}
   109  }
   110  
   111  // Parse and validate flags.
   112  func init() {
   113  	// Unset localhost defaults if certificate file can not be found.
   114  	_, err := os.Stat(opts.RPCCertificateFile)
   115  	if err != nil {
   116  		opts.RPCConnect = ""
   117  		opts.RPCCertificateFile = ""
   118  	}
   119  
   120  	_, err = flags.Parse(&opts)
   121  	if err != nil {
   122  		os.Exit(1)
   123  	}
   124  
   125  	if opts.TestNet && opts.SimNet {
   126  		fatalf("Multiple decred networks may not be used simultaneously")
   127  	}
   128  	if opts.TestNet {
   129  		activeNet = chaincfg.TestNet3Params()
   130  	} else if opts.SimNet {
   131  		activeNet = chaincfg.SimNetParams()
   132  	}
   133  
   134  	if opts.RPCConnect == "" {
   135  		fatalf("RPC hostname[:port] is required")
   136  	}
   137  	rpcConnect, err := normalizeAddress(opts.RPCConnect, walletPort(activeNet))
   138  	if err != nil {
   139  		fatalf("Invalid RPC network address `%v`: %v", opts.RPCConnect, err)
   140  	}
   141  	opts.RPCConnect = rpcConnect
   142  
   143  	if opts.RPCUsername == "" {
   144  		fatalf("RPC username is required")
   145  	}
   146  
   147  	_, err = os.Stat(opts.RPCCertificateFile)
   148  	if err != nil {
   149  		fatalf("RPC certificate file `%s` not found", opts.RPCCertificateFile)
   150  	}
   151  
   152  	if opts.FeeRate > 1 {
   153  		fatalf("Fee rate `%v/kB` is exceptionally high", opts.FeeRate)
   154  	}
   155  	if opts.FeeRate < 1e-6 {
   156  		fatalf("Fee rate `%v/kB` is exceptionally low", opts.FeeRate)
   157  	}
   158  	if opts.SourceAccount == "" && opts.SourceAddress == "" {
   159  		fatalf("A source is required")
   160  	}
   161  	if opts.SourceAccount != "" && opts.SourceAccount == opts.DestinationAccount {
   162  		fatalf("Source and destination accounts should not be equal")
   163  	}
   164  	if opts.DestinationAccount == "" && opts.DestinationAddress == "" {
   165  		fatalf("A destination is required")
   166  	}
   167  	if opts.DestinationAccount != "" && opts.DestinationAddress != "" {
   168  		fatalf("Destination must be either an account or an address")
   169  	}
   170  	if opts.RequiredConfirmations < 0 {
   171  		fatalf("Required confirmations must be non-negative")
   172  	}
   173  }
   174  
   175  // noInputValue describes an error returned by the input source when no inputs
   176  // were selected because each previous output value was zero.  Callers of
   177  // txauthor.NewUnsignedTransaction need not report these errors to the user.
   178  type noInputValue struct {
   179  }
   180  
   181  func (noInputValue) Error() string { return "no input value" }
   182  
   183  // makeInputSource creates an InputSource that creates inputs for every unspent
   184  // output with non-zero output values.  The target amount is ignored since every
   185  // output is consumed.  The InputSource does not return any previous output
   186  // scripts as they are not needed for creating the unsinged transaction and are
   187  // looked up again by the wallet during the call to signrawtransaction.
   188  func makeInputSource(outputs []types.ListUnspentResult) txauthor.InputSource {
   189  	var (
   190  		totalInputValue   dcrutil.Amount
   191  		inputs            = make([]*wire.TxIn, 0, len(outputs))
   192  		redeemScriptSizes = make([]int, 0, len(outputs))
   193  		sourceErr         error
   194  	)
   195  	for _, output := range outputs {
   196  		outputAmount, err := dcrutil.NewAmount(output.Amount)
   197  		if err != nil {
   198  			sourceErr = fmt.Errorf(
   199  				"invalid amount `%v` in listunspent result",
   200  				output.Amount)
   201  			break
   202  		}
   203  		if outputAmount == 0 {
   204  			continue
   205  		}
   206  		if !saneOutputValue(outputAmount) {
   207  			sourceErr = fmt.Errorf(
   208  				"impossible output amount `%v` in listunspent result",
   209  				outputAmount)
   210  			break
   211  		}
   212  		totalInputValue += outputAmount
   213  
   214  		previousOutPoint, err := parseOutPoint(&output)
   215  		if err != nil {
   216  			sourceErr = fmt.Errorf(
   217  				"invalid data in listunspent result: %v", err)
   218  			break
   219  		}
   220  
   221  		txIn := wire.NewTxIn(&previousOutPoint, int64(outputAmount), nil)
   222  		inputs = append(inputs, txIn)
   223  	}
   224  
   225  	if sourceErr == nil && totalInputValue == 0 {
   226  		sourceErr = noInputValue{}
   227  	}
   228  
   229  	return func(dcrutil.Amount) (*txauthor.InputDetail, error) {
   230  		inputDetail := txauthor.InputDetail{
   231  			Amount:            totalInputValue,
   232  			Inputs:            inputs,
   233  			Scripts:           nil,
   234  			RedeemScriptSizes: redeemScriptSizes,
   235  		}
   236  		return &inputDetail, sourceErr
   237  	}
   238  }
   239  
   240  // destinationScriptSourceToAccount is a ChangeSource which is used to receive
   241  // all correlated previous input value.
   242  type destinationScriptSourceToAccount struct {
   243  	accountName string
   244  	rpcClient   *wsrpc.Client
   245  }
   246  
   247  // Source creates a non-change address.
   248  func (src *destinationScriptSourceToAccount) Script() ([]byte, uint16, error) {
   249  	var destinationAddressStr string
   250  	err := src.rpcClient.Call(context.Background(), "getnewaddress", &destinationAddressStr,
   251  		src.accountName)
   252  	if err != nil {
   253  		return nil, 0, err
   254  	}
   255  
   256  	destinationAddress, err := stdaddr.DecodeAddress(destinationAddressStr, activeNet)
   257  	if err != nil {
   258  		return nil, 0, err
   259  	}
   260  
   261  	scriptVer, script := destinationAddress.PaymentScript()
   262  
   263  	return script, scriptVer, nil
   264  }
   265  
   266  func (src *destinationScriptSourceToAccount) ScriptSize() int {
   267  	return 25 // P2PKHPkScriptSize
   268  }
   269  
   270  // destinationScriptSourceToAddress s a ChangeSource which is used to
   271  // receive all correlated previous input value.
   272  type destinationScriptSourceToAddress struct {
   273  	address string
   274  }
   275  
   276  // Source creates a non-change address.
   277  func (src *destinationScriptSourceToAddress) Script() ([]byte, uint16, error) {
   278  	destinationAddress, err := stdaddr.DecodeAddress(src.address, activeNet)
   279  	if err != nil {
   280  		return nil, 0, err
   281  	}
   282  	scriptVer, script := destinationAddress.PaymentScript()
   283  	return script, scriptVer, err
   284  }
   285  
   286  func (src *destinationScriptSourceToAddress) ScriptSize() int {
   287  	return 25 // P2PKHPkScriptSize
   288  }
   289  
   290  func main() {
   291  	ctx := context.Background()
   292  	err := sweep(ctx)
   293  	if err != nil {
   294  		fatalf("%v", err)
   295  	}
   296  }
   297  
   298  func sweep(ctx context.Context) error {
   299  	rpcPassword := opts.RPCPassword
   300  
   301  	if rpcPassword == "" {
   302  		secret, err := promptSecret("Wallet RPC password")
   303  		if err != nil {
   304  			return errContext(err, "failed to read RPC password")
   305  		}
   306  
   307  		rpcPassword = secret
   308  	}
   309  
   310  	// Open RPC client.
   311  	rpcCertificate, err := os.ReadFile(opts.RPCCertificateFile)
   312  	if err != nil {
   313  		return errContext(err, "failed to read RPC certificate")
   314  	}
   315  	caPool := x509.NewCertPool()
   316  	if ok := caPool.AppendCertsFromPEM(rpcCertificate); !ok {
   317  		err := errors.New("unparsable certificate authority")
   318  		return errContext(err, err.Error())
   319  	}
   320  	tc := &tls.Config{RootCAs: caPool}
   321  	tlsOpt := wsrpc.WithTLSConfig(tc)
   322  
   323  	authOpt := wsrpc.WithBasicAuth(opts.RPCUsername, rpcPassword)
   324  
   325  	rpcClient, err := wsrpc.Dial(ctx, opts.RPCConnect, tlsOpt, authOpt)
   326  	if err != nil {
   327  		return errContext(err, "failed to create RPC client")
   328  	}
   329  	defer rpcClient.Close()
   330  
   331  	// Fetch all unspent outputs, ignore those not from the source
   332  	// account, and group by their destination address.  Each grouping of
   333  	// outputs will be used as inputs for a single transaction sending to a
   334  	// new destination account address.
   335  	var unspentOutputs []types.ListUnspentResult
   336  	err = rpcClient.Call(ctx, "listunspent", &unspentOutputs)
   337  	if err != nil {
   338  		return errContext(err, "failed to fetch unspent outputs")
   339  	}
   340  	sourceOutputs := make(map[string][]types.ListUnspentResult)
   341  	for _, unspentOutput := range unspentOutputs {
   342  		if !unspentOutput.Spendable {
   343  			continue
   344  		}
   345  		if unspentOutput.Confirmations < opts.RequiredConfirmations {
   346  			continue
   347  		}
   348  		if opts.SourceAccount != "" && opts.SourceAccount != unspentOutput.Account {
   349  			continue
   350  		}
   351  		if opts.SourceAddress != "" && opts.SourceAddress != unspentOutput.Address {
   352  			continue
   353  		}
   354  		sourceAddressOutputs := sourceOutputs[unspentOutput.Address]
   355  		sourceOutputs[unspentOutput.Address] = append(sourceAddressOutputs, unspentOutput)
   356  	}
   357  
   358  	for address, outputs := range sourceOutputs {
   359  		outputNoun := pickNoun(len(outputs), "output", "outputs")
   360  		fmt.Printf("Found %d matching unspent %s for address %s\n",
   361  			len(outputs), outputNoun, address)
   362  	}
   363  
   364  	var privatePassphrase string
   365  	if len(sourceOutputs) != 0 {
   366  		privatePassphrase, err = promptSecret("Wallet private passphrase")
   367  		if err != nil {
   368  			return errContext(err, "failed to read private passphrase")
   369  		}
   370  	}
   371  
   372  	var totalSwept dcrutil.Amount
   373  	var numErrors int
   374  	var reportError = func(format string, args ...interface{}) {
   375  		fmt.Fprintf(os.Stderr, format, args...)
   376  		os.Stderr.Write(newlineBytes)
   377  		numErrors++
   378  	}
   379  	feeRate, err := dcrutil.NewAmount(opts.FeeRate)
   380  	if err != nil {
   381  		return errContext(err, "invalid fee rate")
   382  	}
   383  	for _, previousOutputs := range sourceOutputs {
   384  		inputSource := makeInputSource(previousOutputs)
   385  
   386  		var destinationSourceToAccount *destinationScriptSourceToAccount
   387  		var destinationSourceToAddress *destinationScriptSourceToAddress
   388  		var atx *txauthor.AuthoredTx
   389  		var err error
   390  
   391  		if opts.DestinationAccount != "" {
   392  			destinationSourceToAccount = &destinationScriptSourceToAccount{
   393  				accountName: opts.DestinationAccount,
   394  				rpcClient:   rpcClient,
   395  			}
   396  			atx, err = txauthor.NewUnsignedTransaction(nil, feeRate,
   397  				inputSource, destinationSourceToAccount, activeNet.MaxTxSize)
   398  		}
   399  
   400  		if opts.DestinationAddress != "" {
   401  			destinationSourceToAddress = &destinationScriptSourceToAddress{
   402  				address: opts.DestinationAddress,
   403  			}
   404  			atx, err = txauthor.NewUnsignedTransaction(nil, feeRate,
   405  				inputSource, destinationSourceToAddress, activeNet.MaxTxSize)
   406  		}
   407  
   408  		if err != nil {
   409  			if !errors.Is(err, (noInputValue{})) {
   410  				reportError("Failed to create unsigned transaction: %v", err)
   411  			}
   412  			continue
   413  		}
   414  
   415  		// Unlock the wallet, sign the transaction, and immediately lock.
   416  		err = rpcClient.Call(ctx, "walletpassphrase", nil, privatePassphrase, 60)
   417  		if err != nil {
   418  			reportError("Failed to unlock wallet: %v", err)
   419  			continue
   420  		}
   421  
   422  		var srtResult types.SignRawTransactionResult
   423  		err = rpcClient.Call(ctx, "signrawtransaction", &srtResult, atx.Tx)
   424  		_ = rpcClient.Call(ctx, "walletlock", nil)
   425  		if err != nil {
   426  			reportError("Failed to sign transaction: %v", err)
   427  			continue
   428  		}
   429  		if !srtResult.Complete {
   430  			reportError("Failed to sign every input")
   431  			continue
   432  		}
   433  
   434  		// Publish the signed sweep transaction.
   435  		txHash := "DRYRUN"
   436  		if opts.DryRun {
   437  			fmt.Printf("DRY RUN: not actually sending transaction\n")
   438  		} else {
   439  			var hash string
   440  			err := rpcClient.Call(ctx, "sendrawtransaction", &hash, srtResult.Hex, false)
   441  			if err != nil {
   442  				reportError("Failed to publish transaction: %v", err)
   443  				continue
   444  			}
   445  
   446  			txHash = hash
   447  		}
   448  
   449  		outputAmount := dcrutil.Amount(atx.Tx.TxOut[0].Value)
   450  		fmt.Printf("Swept %v to destination with transaction %v\n",
   451  			outputAmount, txHash)
   452  		totalSwept += outputAmount
   453  	}
   454  
   455  	numPublished := len(sourceOutputs) - numErrors
   456  	transactionNoun := pickNoun(numErrors, "transaction", "transactions")
   457  	if numPublished != 0 {
   458  		fmt.Printf("Swept %v to destination across %d %s\n",
   459  			totalSwept, numPublished, transactionNoun)
   460  	}
   461  	if numErrors > 0 {
   462  		return fmt.Errorf("failed to publish %d %s", numErrors, transactionNoun)
   463  	}
   464  
   465  	return nil
   466  }
   467  
   468  func promptSecret(what string) (string, error) {
   469  	fmt.Printf("%s: ", what)
   470  	fd := int(os.Stdin.Fd())
   471  	input, err := term.ReadPassword(fd)
   472  	fmt.Println()
   473  	if err != nil {
   474  		return "", err
   475  	}
   476  	return string(input), nil
   477  }
   478  
   479  func saneOutputValue(amount dcrutil.Amount) bool {
   480  	return amount >= 0 && amount <= dcrutil.MaxAmount
   481  }
   482  
   483  func parseOutPoint(input *types.ListUnspentResult) (wire.OutPoint, error) {
   484  	txHash, err := chainhash.NewHashFromStr(input.TxID)
   485  	if err != nil {
   486  		return wire.OutPoint{}, err
   487  	}
   488  	return wire.OutPoint{Hash: *txHash, Index: input.Vout, Tree: input.Tree}, nil
   489  }
   490  
   491  func pickNoun(n int, singularForm, pluralForm string) string {
   492  	if n == 1 {
   493  		return singularForm
   494  	}
   495  	return pluralForm
   496  }