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 }