Skip to content

Commit

Permalink
Add sweepaccount tool. (#173)
Browse files Browse the repository at this point in the history
* Add sweepaccount tool.

This tool creates on-chain transactions, one per used address in a
source account, to sweep all output value to new addresses in a
different destination account.

* Increase max fee.

* Bump required confirmations to 2.
  • Loading branch information
jrick authored and jcvernaleo committed Apr 11, 2016
1 parent eb9d082 commit a4b8c49
Show file tree
Hide file tree
Showing 2 changed files with 389 additions and 0 deletions.
350 changes: 350 additions & 0 deletions cmd/sweepaccount/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2016 The Decred 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"

"github.com/btcsuite/go-flags"
"github.com/btcsuite/golangcrypto/ssh/terminal"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrjson"
"github.com/decred/dcrd/txscript"
"github.com/decred/dcrd/wire"
"github.com/decred/dcrrpcclient"
"github.com/decred/dcrutil"
"github.com/decred/dcrwallet/internal/cfgutil"
"github.com/decred/dcrwallet/netparams"
"github.com/decred/dcrwallet/wallet/txauthor"
"github.com/decred/dcrwallet/wallet/txrules"
)

var (
walletDataDirectory = dcrutil.AppDataDir("dcrwallet", 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 {
TestNet bool `long:"testnet" description:"Use the test decred network"`
SimNet bool `long:"simnet" description:"Use the simulation decred 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"`
}{
TestNet: false,
SimNet: false,
RPCConnect: "localhost",
RPCUsername: "",
RPCCertificateFile: filepath.Join(walletDataDirectory, "rpc.cert"),
FeeRate: &cfgutil.AmountFlag{txrules.DefaultRelayFeePerKb},
SourceAccount: "imported",
DestinationAccount: "default",
RequiredConfirmations: 2,
}

// 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.TestNet && opts.SimNet {
fatalf("Multiple decred networks may not be used simultaneously")
}
var activeNet = &netparams.MainNetParams
if opts.TestNet {
activeNet = &netparams.TestNetParams
} 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 > 1e8 {
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 []dcrjson.ListUnspentResult) txauthor.InputSource {
var (
totalInputValue dcrutil.Amount
inputs = make([]*wire.TxIn, 0, len(outputs))
sourceErr error
)
for _, output := range outputs {
outputAmount, err := dcrutil.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))
}

if sourceErr == nil && totalInputValue == 0 {
sourceErr = noInputValue{}
}

return func(dcrutil.Amount) (dcrutil.Amount, []*wire.TxIn, [][]byte, error) {
return totalInputValue, inputs, 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 *dcrrpcclient.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 := dcrrpcclient.New(&dcrrpcclient.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][]dcrjson.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 dcrutil.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 := dcrutil.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 dcrutil.Amount) bool {
return amount >= 0 && amount <= dcrutil.MaxAmount
}

func parseOutPoint(input *dcrjson.ListUnspentResult) (wire.OutPoint, error) {
txHash, err := chainhash.NewHashFromStr(input.TxID)
if err != nil {
return wire.OutPoint{}, err
}
return wire.OutPoint{*txHash, input.Vout, input.Tree}, nil
}

func pickNoun(n int, singularForm, pluralForm string) string {
if n == 1 {
return singularForm
}
return pluralForm
}
39 changes: 39 additions & 0 deletions internal/cfgutil/amount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package cfgutil

import (
"strconv"
"strings"

"github.com/decred/dcrutil"
)

// AmountFlag embeds a dcrutil.Amount and implements the flags.Marshaler and
// Unmarshaler interfaces so it can be used as a config struct field.
type AmountFlag struct {
dcrutil.Amount
}

// MarshalFlag satisifes the flags.Marshaler interface.
func (a *AmountFlag) MarshalFlag() (string, error) {
return a.Amount.String(), nil
}

// UnmarshalFlag satisifes the flags.Unmarshaler interface.
func (a *AmountFlag) UnmarshalFlag(value string) error {
value = strings.TrimSuffix(value, " DCR")
valueF64, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
amount, err := dcrutil.NewAmount(valueF64)
if err != nil {
return err
}
a.Amount = amount
return nil
}

0 comments on commit a4b8c49

Please sign in to comment.