From ea5ce15676b89c2e0b51692323244c3c925d60e0 Mon Sep 17 00:00:00 2001 From: kifen Date: Thu, 5 Sep 2019 14:41:10 +0100 Subject: [PATCH 01/15] new method UnspentOutputs --- accounts.go | 40 ++++++++++++++++++++++++++++++++++++++++ types.go | 11 +++++++++++ 2 files changed, 51 insertions(+) diff --git a/accounts.go b/accounts.go index a36ed980..8f2f597d 100644 --- a/accounts.go +++ b/accounts.go @@ -2,9 +2,12 @@ package dcrlibwallet import ( "encoding/json" + "fmt" "time" + "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrwallet/errors" + "github.com/decred/dcrwallet/wallet" ) func (lw *LibWallet) GetAccounts(requiredConfirmations int32) (string, error) { @@ -75,6 +78,43 @@ func (lw *LibWallet) SpendableForAccount(account int32, requiredConfirmations in return int64(bals.Spendable), nil } +func (lw *LibWallet) UnspentOutputs(account uint32, requiredConfirmations int32) ([]*UnspentOutput, error) { + policy := wallet.OutputSelectionPolicy{ + Account: account, + RequiredConfirmations: requiredConfirmations, + } + inputDetail, err := lw.wallet.SelectInputs(dcrutil.Amount(0), policy) + + if err != nil { + return nil, err + } + + unspentOutputs := make([]*UnspentOutput, len(inputDetail.Inputs)) + + for i, input := range inputDetail.Inputs { + outputInfo, err := lw.wallet.OutputInfo(&input.PreviousOutPoint) + if err != nil { + return nil, err + } + + // unique key to identify utxo + outputKey := fmt.Sprintf("%s:%d", input.PreviousOutPoint.Hash, input.PreviousOutPoint.Index) + + unspentOutputs[i] = &UnspentOutput{ + TransactionHash: input.PreviousOutPoint.Hash[:], + OutputIndex: input.PreviousOutPoint.Index, + OutputKey: outputKey, + Tree: int32(input.PreviousOutPoint.Tree), + Amount: int64(outputInfo.Amount), + PkScript: inputDetail.Scripts[i], + ReceiveTime: outputInfo.Received.Unix(), + FromCoinbase: outputInfo.FromCoinbase, + } + } + + return unspentOutputs, nil +} + func (lw *LibWallet) NextAccount(accountName string, privPass []byte) error { _, err := lw.NextAccountRaw(accountName, privPass) if err != nil { diff --git a/types.go b/types.go index 0236f4ba..6f5ad3ee 100644 --- a/types.go +++ b/types.go @@ -227,3 +227,14 @@ type VSPTicketPurchaseInfo struct { } /** end ticket-related types */ + +type UnspentOutput struct { + TransactionHash []byte + OutputIndex uint32 + OutputKey string + ReceiveTime int64 + Amount int64 + FromCoinbase bool + Tree int32 + PkScript []byte +} From ce970a5c910118d05a099a25cf54567c44e88533 Mon Sep 17 00:00:00 2001 From: kifen Date: Thu, 5 Sep 2019 15:07:22 +0100 Subject: [PATCH 02/15] new method SendFromCustomIputs --- txauthor.go | 134 +++++++++++++++++++++++++++++++++++++++ txhelper/changeamount.go | 43 +++++++++++++ txhelper/outputs.go | 86 +++++++++++++++++++++++++ txhelper/txsizes.go | 77 ++++++++++++++++++++++ txhelper/types.go | 6 ++ txhelper/unsignedtx.go | 123 +++++++++++++++++++++++++++++++++++ 6 files changed, 469 insertions(+) create mode 100644 txhelper/changeamount.go create mode 100644 txhelper/txsizes.go create mode 100644 txhelper/unsignedtx.go diff --git a/txauthor.go b/txauthor.go index b26c254c..f631aea2 100644 --- a/txauthor.go +++ b/txauthor.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" @@ -221,3 +222,136 @@ func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { return tx.lw.wallet.NewUnsignedTransaction(outputs, txrules.DefaultRelayFeePerKb, tx.sendFromAccount, tx.requiredConfirmations, outputSelectionAlgorithm, changeSource) } + +func (lw *LibWallet) SendFromCustomInputs(sourceAccount uint32, requiredConfirmations int32, utxoKeys []string, + txDestinations []txhelper.TransactionDestination, changeDestinations []txhelper.TransactionDestination, privPass []byte) (string, error) { + + // fetch all utxos in account to extract details for the utxos selected by user + // use targetAmount = 0 to fetch ALL utxos in account + unspentOutputs, err := lw.UnspentOutputs(sourceAccount, requiredConfirmations, 0) + if err != nil { + return "", err + } + + // loop through unspentOutputs to find user selected utxos + inputs := make([]*wire.TxIn, 0, len(utxoKeys)) + var totalInputAmount int64 + for _, utxo := range unspentOutputs { + useUtxo := false + for _, key := range utxoKeys { + if utxo.OutputKey == key { + useUtxo = true + } + } + if !useUtxo { + continue + } + + // this is a reverse conversion and should not throw an error + // this []byte was originally converted from chainhash.Hash using chainhash.Hash[:] + txHash, _ := chainhash.NewHash(utxo.TransactionHash) + + outpoint := wire.NewOutPoint(txHash, utxo.OutputIndex, int8(utxo.Tree)) + input := wire.NewTxIn(outpoint, int64(utxo.Amount), nil) + inputs = append(inputs, input) + totalInputAmount += input.ValueIn + + if len(inputs) == len(utxoKeys) { + break + } + } + + unsignedTx, err := txhelper.NewUnsignedTx(inputs, txDestinations, changeDestinations, func() (address string, err error) { + return lw.NextAddress(int32(sourceAccount)) + }) + if err != nil { + return "", err + } + + // serialize unsigned tx + var txBuf bytes.Buffer + txBuf.Grow(unsignedTx.SerializeSize()) + err = unsignedTx.Serialize(&txBuf) + if err != nil { + return "", fmt.Errorf("error serializing transaction: %s", err.Error()) + } + + txHash, err := lw.SignAndPublishTransaction(txBuf.Bytes(), privPass) + if err != nil { + return "", err + } + + transactionHash, err := chainhash.NewHash(txHash) + if err != nil { + return "", fmt.Errorf("error parsing successful transaction hash: %s", err.Error()) + } + + return transactionHash.String(), nil +} + +func (lw *LibWallet) SignAndPublishTransaction(serializedTx, privPass []byte) ([]byte, error) { + n, err := lw.wallet.NetworkBackend() + if err != nil { + log.Error(err) + return nil, err + } + defer func() { + for i := range privPass { + privPass[i] = 0 + } + }() + + var tx wire.MsgTx + err = tx.Deserialize(bytes.NewReader(serializedTx)) + if err != nil { + log.Error(err) + //Bytes do not represent a valid raw transaction + return nil, err + } + + lock := make(chan time.Time, 1) + defer func() { + lock <- time.Time{} + }() + + err = lw.wallet.Unlock(privPass, lock) + if err != nil { + log.Error(err) + return nil, errors.New(ErrInvalidPassphrase) + } + + var additionalPkScripts map[wire.OutPoint][]byte + + invalidSigs, err := lw.wallet.SignTransaction(&tx, txscript.SigHashAll, additionalPkScripts, nil, nil) + if err != nil { + log.Error(err) + return nil, err + } + + invalidInputIndexes := make([]uint32, len(invalidSigs)) + for i, e := range invalidSigs { + invalidInputIndexes[i] = e.InputIndex + } + + var serializedTransaction bytes.Buffer + serializedTransaction.Grow(tx.SerializeSize()) + err = tx.Serialize(&serializedTransaction) + if err != nil { + log.Error(err) + return nil, err + } + + var msgTx wire.MsgTx + err = msgTx.Deserialize(bytes.NewReader(serializedTransaction.Bytes())) + if err != nil { + //Invalid tx + log.Error(err) + return nil, err + } + + txHash, err := lw.wallet.PublishTransaction(&msgTx, serializedTransaction.Bytes(), n) + if err != nil { + return nil, translateError(err) + } + return txHash[:], nil +} diff --git a/txhelper/changeamount.go b/txhelper/changeamount.go new file mode 100644 index 00000000..1ede3de4 --- /dev/null +++ b/txhelper/changeamount.go @@ -0,0 +1,43 @@ +package txhelper + +import ( + "errors" + "fmt" + + "github.com/decred/dcrd/dcrutil" + "github.com/decred/dcrd/txscript" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/wallet/txrules" +) + +func EstimateChangeWithOutputs(numberOfInputs int, totalInputAmount int64, outputs []*wire.TxOut, totalSendAmount int64, changeAddresses []string) (int64, error) { + if totalSendAmount >= totalInputAmount { + return 0, fmt.Errorf("total send amount (%s) is higher than or equal to the total input amount (%s)", + dcrutil.Amount(totalSendAmount).String(), dcrutil.Amount(totalInputAmount).String()) + } + + totalChangeScriptSize, err := calculateChangeScriptSize(changeAddresses) + if err != nil { + return 0, err + } + + scriptSizes := make([]int, numberOfInputs) + for i := 0; i < numberOfInputs; i++ { + scriptSizes[i] = RedeemP2PKHSigScriptSize + } + + relayFeePerKb := txrules.DefaultRelayFeePerKb + maxSignedSize := EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) + maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) + changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) + + // if change amount is valid, check if the script size exceeds maximum script size + if changeAmount > 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), totalChangeScriptSize, relayFeePerKb) { + maxChangeScriptSize := len(changeAddresses) * txscript.MaxScriptElementSize + if totalChangeScriptSize > maxChangeScriptSize { + return 0, errors.New("script size exceed maximum bytes pushable to the stack") + } + } + + return changeAmount, nil +} diff --git a/txhelper/outputs.go b/txhelper/outputs.go index 4f330caf..fbd8eaa6 100644 --- a/txhelper/outputs.go +++ b/txhelper/outputs.go @@ -1,6 +1,9 @@ package txhelper import ( + "fmt" + + "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" "github.com/raedahgroup/dcrlibwallet/addresshelper" @@ -19,3 +22,86 @@ func MakeTxOutput(address string, amountInAtom int64) (output *wire.TxOut, err e } return } + +// TxOutputsExtractMaxChangeDestination checks the provided txDestinations +// if there is 1 and not more than 1 recipient set to receive max amount. +// Returns an error if more than 1 max amount recipients identified. +// Returns the outputs for the tx excluding the max amount recipient, +// and also returns the change object for the max amount recipient address if there is a max recipient address. +func TxOutputsExtractMaxChangeDestination(nInputs int, totalInputAmount int64, txDestinations []TransactionDestination) ( + outputs []*wire.TxOut, totalSendAmount int64, maxChangeDestinations []TransactionDestination, err error) { + + outputs, totalSendAmount, maxAmountRecipientAddress, err := TxOutputsExtractMaxDestinationAddress(txDestinations) + if err != nil { + return + } + + if maxAmountRecipientAddress != "" { + // use as change address + changeAddresses := []string{maxAmountRecipientAddress} + changeAmount, err := EstimateChangeWithOutputs(nInputs, totalInputAmount, outputs, totalSendAmount, changeAddresses) + if err != nil { + return nil, 0, nil, err + } + + if changeAmount < 0 { + excessSpending := 0 - changeAmount // equivalent to math.Abs() + err = fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", + dcrutil.Amount(excessSpending).String()) + return nil, 0, nil, err + } + + maxChangeDestinations = []TransactionDestination{ + { + Address: maxAmountRecipientAddress, + Amount: dcrutil.Amount(changeAmount).ToCoin(), + }, + } + } + + return +} + +// TxOutputsExtractMaxDestinationAddress checks the provided txDestinations +// if there is 1 and not more than 1 recipient set to receive max amount. +// Returns an error if more than 1 max amount recipients identified. +// Returns the outputs for the tx excluding the max amount recipient, +// and also returns the max amount recipient address if there is one. +func TxOutputsExtractMaxDestinationAddress(txDestinations []TransactionDestination) ( + outputs []*wire.TxOut, totalSendAmount int64, maxAmountRecipientAddress string, err error) { + + // check if there's a max amount recipient, and not more than 1 such recipient + nOutputs := len(txDestinations) + for _, destination := range txDestinations { + if destination.SendMax && maxAmountRecipientAddress != "" { + err = fmt.Errorf("cannot send max amount to multiple recipients") + return + } else if destination.SendMax { + maxAmountRecipientAddress = destination.Address + nOutputs-- + } + } + + // create transaction outputs for all destination addresses and amounts, excluding destination for send max + outputs = make([]*wire.TxOut, 0, nOutputs) + var output *wire.TxOut + for _, destination := range txDestinations { + if !destination.SendMax { + amount, err := dcrutil.NewAmount(destination.Amount) + if err != nil { + err = fmt.Errorf("invalid amount: %v", destination.Amount) + return nil, 0, "", err + } + + output, err = MakeTxOutput(destination.Address, int64(amount)) + if err != nil { + return + } + + outputs = append(outputs, output) + totalSendAmount += output.Value + } + } + + return +} diff --git a/txhelper/txsizes.go b/txhelper/txsizes.go new file mode 100644 index 00000000..010a5c6e --- /dev/null +++ b/txhelper/txsizes.go @@ -0,0 +1,77 @@ +package txhelper + +import "github.com/decred/dcrd/wire" + +// from "github.com/decred/dcrwallet/wallet/internal/txsizes" +// RedeemP2PKHSigScriptSize is the worst case (largest) serialize size +// of a transaction input script that redeems a compressed P2PKH output. +// It is calculated as: +// +// - OP_DATA_73 +// - 72 bytes DER signature + 1 byte sighash +// - OP_DATA_33 +// - 33 bytes serialized compressed pubkey +const RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 + +// from "github.com/decred/dcrwallet/wallet/internal/txsizes" +// EstimateSerializeSize returns a worst case serialize size estimate for a +// signed transaction that spends a number of outputs and contains each +// transaction output from txOuts. The estimated size is incremented for an +// additional change output if changeScriptSize is greater than 0. Passing 0 +// does not add a change output. +func EstimateSerializeSize(scriptSizes []int, txOuts []*wire.TxOut, changeScriptSize int) int { + // Generate and sum up the estimated sizes of the inputs. + txInsSize := 0 + for _, size := range scriptSizes { + txInsSize += EstimateInputSize(size) + } + + inputCount := len(scriptSizes) + outputCount := len(txOuts) + changeSize := 0 + if changeScriptSize > 0 { + changeSize = EstimateOutputSize(changeScriptSize) + outputCount++ + } + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + + wire.VarIntSerializeSize(uint64(outputCount)) + + txInsSize + + SumOutputSerializeSizes(txOuts) + + changeSize +} + +// from "github.com/decred/dcrwallet/wallet/internal/txsizes" +// EstimateInputSize returns the worst case serialize size estimate for a tx input +// - 32 bytes previous tx +// - 4 bytes output index +// - 1 byte tree +// - 8 bytes amount +// - 4 bytes block height +// - 4 bytes block index +// - the compact int representation of the script size +// - the supplied script size +// - 4 bytes sequence +func EstimateInputSize(scriptSize int) int { + return 32 + 4 + 1 + 8 + 4 + 4 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize + 4 +} + +// from "github.com/decred/dcrwallet/wallet/internal/txsizes" +// EstimateOutputSize returns the worst case serialize size estimate for a tx output +// - 8 bytes amount +// - 2 bytes version +// - the compact int representation of the script size +// - the supplied script size +func EstimateOutputSize(scriptSize int) int { + return 8 + 2 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize +} + +// from github.com/decred/dcrwallet/internal/helpers +// SumOutputSerializeSizes sums up the serialized size of the supplied outputs. +func SumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} diff --git a/txhelper/types.go b/txhelper/types.go index 66b4944b..ef193766 100644 --- a/txhelper/types.go +++ b/txhelper/types.go @@ -12,3 +12,9 @@ const ( TxTypeVote = "Vote" TxTypeRevocation = "Revocation" ) + +type TransactionDestination struct { + Address string + Amount float64 + SendMax bool +} diff --git a/txhelper/unsignedtx.go b/txhelper/unsignedtx.go new file mode 100644 index 00000000..53337dec --- /dev/null +++ b/txhelper/unsignedtx.go @@ -0,0 +1,123 @@ +package txhelper + +import ( + "fmt" + + "github.com/decred/dcrd/dcrutil" + "github.com/decred/dcrd/txscript" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/wallet/txrules" +) + +type GenerateAddressFunc func() (address string, err error) + +// NewUnsignedTx uses the inputs to prepare a tx with outputs for the provided send destinations and change destinations. +// If any of the send destinations is set to receive max amount, that destination address is used as single change destination. +// If no change destinations are provided and no recipient is set to receive max amount, +// a single change destination is created for an address gotten by calling `generateAccountAddress()`. +func NewUnsignedTx(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, + generateAccountAddress GenerateAddressFunc) (*wire.MsgTx, error) { + + var totalInputAmount int64 + scriptSizes := make([]int, 0, len(inputs)) + for _, txIn := range inputs { + totalInputAmount += txIn.ValueIn + scriptSizes = append(scriptSizes, RedeemP2PKHSigScriptSize) + } + + outputs, totalSendAmount, maxChangeDestinations, err := TxOutputsExtractMaxChangeDestination(len(inputs), totalInputAmount, sendDestinations) + if err != nil { + return nil, err + } + + if totalSendAmount > totalInputAmount { + return nil, fmt.Errorf("total send amount (%s) is higher than the total input amount (%s)", + dcrutil.Amount(totalSendAmount).String(), dcrutil.Amount(totalInputAmount).String()) + } + + // if a max change destination is returned, use it as the only change destination + if len(maxChangeDestinations) == 1 { + changeDestinations = maxChangeDestinations + } + + // create a default change destination if none is specified and there is a change amount from this tx + if len(changeDestinations) == 0 { + changeAddress, err := generateAccountAddress() + if err != nil { + return nil, fmt.Errorf("error generating change address for tx: %s", err.Error()) + } + + changeAmount, err := EstimateChangeWithOutputs(len(inputs), totalInputAmount, outputs, totalSendAmount, []string{changeAddress}) + if err != nil { + return nil, fmt.Errorf("error in getting change amount: %s", err.Error()) + } + if changeAmount > 0 { + changeDestinations = append(changeDestinations, TransactionDestination{ + Address: changeAddress, + Amount: dcrutil.Amount(changeAmount).ToCoin(), + }) + } + } + + changeAddresses := make([]string, len(changeDestinations)) + for i, changeDestination := range changeDestinations { + changeAddresses[i] = changeDestination.Address + } + totalChangeScriptSize, err := calculateChangeScriptSize(changeAddresses) + if err != nil { + return nil, fmt.Errorf("error processing change outputs: %s", err.Error()) + } + + maxSignedSize := EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) + maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) + changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) + + if changeAmount < 0 { + excessSpending := 0 - changeAmount // equivalent to math.Abs() + return nil, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", + dcrutil.Amount(excessSpending).String()) + } + + if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), totalChangeScriptSize, txrules.DefaultRelayFeePerKb) { + maxAcceptableChangeScriptSize := len(changeDestinations) * txscript.MaxScriptElementSize + if totalChangeScriptSize > maxAcceptableChangeScriptSize { + return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") + } + + changeOutputs, totalChangeAmount, err := makeTxOutputs(changeDestinations) + if err != nil { + return nil, fmt.Errorf("error creating change outputs for tx: %s", err.Error()) + } + + if totalChangeAmount > changeAmount { + return nil, fmt.Errorf("total amount allocated to change addresses (%s) is higher than actual change amount for transaction (%s)", + dcrutil.Amount(totalChangeAmount).String(), dcrutil.Amount(changeAmount).String()) + } + + // todo dcrwallet randomizes change position, should look into that as well + outputs = append(outputs, changeOutputs...) + } + + unsignedTransaction := &wire.MsgTx{ + SerType: wire.TxSerializeFull, + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + Expiry: 0, + } + + return unsignedTransaction, nil +} + +func calculateChangeScriptSize(changeAddresses []string) (int, error) { + var totalChangeScriptSize int + for _, changeAddress := range changeAddresses { + changeSource, err := MakeTxChangeSource(changeAddress) + if err != nil { + return 0, err + } + totalChangeScriptSize += changeSource.ScriptSize() + } + return totalChangeScriptSize, nil +} From 025f7295c9139eb5e5645101dab9be8d35b93f2b Mon Sep 17 00:00:00 2001 From: kifen Date: Fri, 6 Sep 2019 10:04:55 +0100 Subject: [PATCH 03/15] change method name from UnspentOutputs to AllUnspentOutputs --- accounts.go | 5 ++++- txauthor.go | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/accounts.go b/accounts.go index 8f2f597d..2c5e581d 100644 --- a/accounts.go +++ b/accounts.go @@ -78,11 +78,14 @@ func (lw *LibWallet) SpendableForAccount(account int32, requiredConfirmations in return int64(bals.Spendable), nil } -func (lw *LibWallet) UnspentOutputs(account uint32, requiredConfirmations int32) ([]*UnspentOutput, error) { +func (lw *LibWallet) AllUnspentOutputs(account uint32, requiredConfirmations int32) ([]*UnspentOutput, error) { policy := wallet.OutputSelectionPolicy{ Account: account, RequiredConfirmations: requiredConfirmations, } + + // fetch all utxos in account to extract details for the utxos selected by user + // use targetAmount = 0 to fetch ALL utxos in account inputDetail, err := lw.wallet.SelectInputs(dcrutil.Amount(0), policy) if err != nil { diff --git a/txauthor.go b/txauthor.go index f631aea2..4d41dcb6 100644 --- a/txauthor.go +++ b/txauthor.go @@ -226,9 +226,7 @@ func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { func (lw *LibWallet) SendFromCustomInputs(sourceAccount uint32, requiredConfirmations int32, utxoKeys []string, txDestinations []txhelper.TransactionDestination, changeDestinations []txhelper.TransactionDestination, privPass []byte) (string, error) { - // fetch all utxos in account to extract details for the utxos selected by user - // use targetAmount = 0 to fetch ALL utxos in account - unspentOutputs, err := lw.UnspentOutputs(sourceAccount, requiredConfirmations, 0) + unspentOutputs, err := lw.AllUnspentOutputs(sourceAccount, requiredConfirmations) if err != nil { return "", err } @@ -256,8 +254,8 @@ func (lw *LibWallet) SendFromCustomInputs(sourceAccount uint32, requiredConfirma inputs = append(inputs, input) totalInputAmount += input.ValueIn - if len(inputs) == len(utxoKeys) { - break + if len(inputs) != len(utxoKeys) { + return "", fmt.Errorf("could not find matching inputs for all provided utxo keys") } } From eea686126918f3f9911b0ed428f2062abb315119 Mon Sep 17 00:00:00 2001 From: Wisdom Arerosuoghene Date: Tue, 17 Sep 2019 03:52:37 +0100 Subject: [PATCH 04/15] copy txsizes/size.go exact from dcrwallet, place in internal pkg --- txhelper/changeamount.go | 5 +- txhelper/internal/txsizes/size.go | 188 ++++++++++++++++++++++++++++++ txhelper/txsizes.go | 77 ------------ txhelper/unsignedtx.go | 5 +- 4 files changed, 194 insertions(+), 81 deletions(-) create mode 100644 txhelper/internal/txsizes/size.go delete mode 100644 txhelper/txsizes.go diff --git a/txhelper/changeamount.go b/txhelper/changeamount.go index 1ede3de4..2b37fa88 100644 --- a/txhelper/changeamount.go +++ b/txhelper/changeamount.go @@ -8,6 +8,7 @@ import ( "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" "github.com/decred/dcrwallet/wallet/txrules" + "github.com/raedahgroup/dcrlibwallet/txhelper/internal/txsizes" ) func EstimateChangeWithOutputs(numberOfInputs int, totalInputAmount int64, outputs []*wire.TxOut, totalSendAmount int64, changeAddresses []string) (int64, error) { @@ -23,11 +24,11 @@ func EstimateChangeWithOutputs(numberOfInputs int, totalInputAmount int64, outpu scriptSizes := make([]int, numberOfInputs) for i := 0; i < numberOfInputs; i++ { - scriptSizes[i] = RedeemP2PKHSigScriptSize + scriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize } relayFeePerKb := txrules.DefaultRelayFeePerKb - maxSignedSize := EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) + maxSignedSize := txsizes.EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) diff --git a/txhelper/internal/txsizes/size.go b/txhelper/internal/txsizes/size.go new file mode 100644 index 00000000..1ac4a187 --- /dev/null +++ b/txhelper/internal/txsizes/size.go @@ -0,0 +1,188 @@ +// Copyright (c) 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. + +// from "github.com/decred/dcrwallet/wallet/internal/txsizes" + +package txsizes + +import "github.com/decred/dcrd/wire" + +// Worst case script and input/output size estimates. +const ( + // RedeemP2PKSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2PK output. + // It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + RedeemP2PKSigScriptSize = 1 + 73 + + // RedeemP2PKHSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2PKH output. + // It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 + + // RedeemP2SHSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a P2SH output. + // It is calculated as: + // + // - OP_DATA_73 + // - 73-byte signature + // - OP_DATA_35 + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_CHECKSIG + RedeemP2SHSigScriptSize = 1 + 73 + 1 + 1 + 33 + 1 + + // RedeemP2PKHInputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2PKH output. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte tree + // - 8 bytes amount + // - 4 bytes block height + // - 4 bytes block index + // - 1 byte compact int encoding value 107 + // - 107 bytes signature script + // - 4 bytes sequence + RedeemP2PKHInputSize = 32 + 4 + 1 + 8 + 4 + 4 + 1 + RedeemP2PKHSigScriptSize + 4 + + // P2PKHPkScriptSize is the size of a transaction output script that + // pays to a compressed pubkey hash. It is calculated as: + // + // - OP_DUP + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes pubkey hash + // - OP_EQUALVERIFY + // - OP_CHECKSIG + P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 + + // P2SHPkScriptSize is the size of a transaction output script that + // pays to a script hash. It is calculated as: + // + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes script hash + // - OP_EQUAL + P2SHPkScriptSize = 1 + 1 + 20 + 1 + + // TicketCommitmentScriptSize is the size of a ticket purchase commitment + // script. It is calculated as: + // + // - OP_RETURN + // - OP_DATA_30 + // - 20 bytes P2SH/P2PKH + // - 8 byte amount + // - 2 byte fee range limits + TicketCommitmentScriptSize = 1 + 1 + 20 + 8 + 2 + + // P2PKHOutputSize is the serialize size of a transaction output with a + // P2PKH output script. It is calculated as: + // + // - 8 bytes output value + // - 2 bytes version + // - 1 byte compact int encoding value 25 + // - 25 bytes P2PKH output script + P2PKHOutputSize = 8 + 2 + 1 + 25 +) + +func sumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} + +// EstimateSerializeSize returns a worst case serialize size estimate for a +// signed transaction that spends a number of outputs and contains each +// transaction output from txOuts. The estimated size is incremented for an +// additional change output if changeScriptSize is greater than 0. Passing 0 +// does not add a change output. +func EstimateSerializeSize(scriptSizes []int, txOuts []*wire.TxOut, changeScriptSize int) int { + // Generate and sum up the estimated sizes of the inputs. + txInsSize := 0 + for _, size := range scriptSizes { + txInsSize += EstimateInputSize(size) + } + + inputCount := len(scriptSizes) + outputCount := len(txOuts) + changeSize := 0 + if changeScriptSize > 0 { + changeSize = EstimateOutputSize(changeScriptSize) + outputCount++ + } + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + + wire.VarIntSerializeSize(uint64(outputCount)) + + txInsSize + + sumOutputSerializeSizes(txOuts) + + changeSize +} + +// EstimateSerializeSizeFromScriptSizes returns a worst case serialize size +// estimate for a signed transaction that spends len(inputSizes) previous +// outputs and pays to len(outputSizes) outputs with scripts of the provided +// worst-case sizes. The estimated size is incremented for an additional +// change output if changeScriptSize is greater than 0. Passing 0 does not +// add a change output. +func EstimateSerializeSizeFromScriptSizes(inputSizes []int, outputSizes []int, changeScriptSize int) int { + // Generate and sum up the estimated sizes of the inputs. + txInsSize := 0 + for _, inputSize := range inputSizes { + txInsSize += EstimateInputSize(inputSize) + } + + // Generate and sum up the estimated sizes of the outputs. + txOutsSize := 0 + for _, outputSize := range outputSizes { + txOutsSize += EstimateOutputSize(outputSize) + } + + inputCount := len(inputSizes) + outputCount := len(outputSizes) + changeSize := 0 + if changeScriptSize > 0 { + changeSize = EstimateOutputSize(changeScriptSize) + outputCount++ + } + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + + wire.VarIntSerializeSize(uint64(outputCount)) + + txInsSize + txOutsSize + changeSize +} + +// EstimateInputSize returns the worst case serialize size estimate for a tx input +// - 32 bytes previous tx +// - 4 bytes output index +// - 1 byte tree +// - 8 bytes amount +// - 4 bytes block height +// - 4 bytes block index +// - the compact int representation of the script size +// - the supplied script size +// - 4 bytes sequence +func EstimateInputSize(scriptSize int) int { + return 32 + 4 + 1 + 8 + 4 + 4 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize + 4 +} + +// EstimateOutputSize returns the worst case serialize size estimate for a tx output +// - 8 bytes amount +// - 2 bytes version +// - the compact int representation of the script size +// - the supplied script size +func EstimateOutputSize(scriptSize int) int { + return 8 + 2 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize +} diff --git a/txhelper/txsizes.go b/txhelper/txsizes.go deleted file mode 100644 index 010a5c6e..00000000 --- a/txhelper/txsizes.go +++ /dev/null @@ -1,77 +0,0 @@ -package txhelper - -import "github.com/decred/dcrd/wire" - -// from "github.com/decred/dcrwallet/wallet/internal/txsizes" -// RedeemP2PKHSigScriptSize is the worst case (largest) serialize size -// of a transaction input script that redeems a compressed P2PKH output. -// It is calculated as: -// -// - OP_DATA_73 -// - 72 bytes DER signature + 1 byte sighash -// - OP_DATA_33 -// - 33 bytes serialized compressed pubkey -const RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 - -// from "github.com/decred/dcrwallet/wallet/internal/txsizes" -// EstimateSerializeSize returns a worst case serialize size estimate for a -// signed transaction that spends a number of outputs and contains each -// transaction output from txOuts. The estimated size is incremented for an -// additional change output if changeScriptSize is greater than 0. Passing 0 -// does not add a change output. -func EstimateSerializeSize(scriptSizes []int, txOuts []*wire.TxOut, changeScriptSize int) int { - // Generate and sum up the estimated sizes of the inputs. - txInsSize := 0 - for _, size := range scriptSizes { - txInsSize += EstimateInputSize(size) - } - - inputCount := len(scriptSizes) - outputCount := len(txOuts) - changeSize := 0 - if changeScriptSize > 0 { - changeSize = EstimateOutputSize(changeScriptSize) - outputCount++ - } - - // 12 additional bytes are for version, locktime and expiry. - return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + - wire.VarIntSerializeSize(uint64(outputCount)) + - txInsSize + - SumOutputSerializeSizes(txOuts) + - changeSize -} - -// from "github.com/decred/dcrwallet/wallet/internal/txsizes" -// EstimateInputSize returns the worst case serialize size estimate for a tx input -// - 32 bytes previous tx -// - 4 bytes output index -// - 1 byte tree -// - 8 bytes amount -// - 4 bytes block height -// - 4 bytes block index -// - the compact int representation of the script size -// - the supplied script size -// - 4 bytes sequence -func EstimateInputSize(scriptSize int) int { - return 32 + 4 + 1 + 8 + 4 + 4 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize + 4 -} - -// from "github.com/decred/dcrwallet/wallet/internal/txsizes" -// EstimateOutputSize returns the worst case serialize size estimate for a tx output -// - 8 bytes amount -// - 2 bytes version -// - the compact int representation of the script size -// - the supplied script size -func EstimateOutputSize(scriptSize int) int { - return 8 + 2 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize -} - -// from github.com/decred/dcrwallet/internal/helpers -// SumOutputSerializeSizes sums up the serialized size of the supplied outputs. -func SumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { - for _, txOut := range outputs { - serializeSize += txOut.SerializeSize() - } - return serializeSize -} diff --git a/txhelper/unsignedtx.go b/txhelper/unsignedtx.go index 53337dec..b2ae6076 100644 --- a/txhelper/unsignedtx.go +++ b/txhelper/unsignedtx.go @@ -7,6 +7,7 @@ import ( "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" "github.com/decred/dcrwallet/wallet/txrules" + "github.com/raedahgroup/dcrlibwallet/txhelper/internal/txsizes" ) type GenerateAddressFunc func() (address string, err error) @@ -22,7 +23,7 @@ func NewUnsignedTx(inputs []*wire.TxIn, sendDestinations, changeDestinations []T scriptSizes := make([]int, 0, len(inputs)) for _, txIn := range inputs { totalInputAmount += txIn.ValueIn - scriptSizes = append(scriptSizes, RedeemP2PKHSigScriptSize) + scriptSizes = append(scriptSizes, txsizes.RedeemP2PKHSigScriptSize) } outputs, totalSendAmount, maxChangeDestinations, err := TxOutputsExtractMaxChangeDestination(len(inputs), totalInputAmount, sendDestinations) @@ -68,7 +69,7 @@ func NewUnsignedTx(inputs []*wire.TxIn, sendDestinations, changeDestinations []T return nil, fmt.Errorf("error processing change outputs: %s", err.Error()) } - maxSignedSize := EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) + maxSignedSize := txsizes.EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) From 91eb34d660676308009a86e2e63f44dfdfa3db28 Mon Sep 17 00:00:00 2001 From: Wisdom Arerosuoghene Date: Sat, 28 Sep 2019 00:01:18 +0100 Subject: [PATCH 05/15] refactor: add custom send fn to txauthor type --- txauthor.go | 312 +++++++++++++++++---------------------- txhelper/changeamount.go | 44 ------ txhelper/outputs.go | 97 ++++-------- txhelper/types.go | 6 +- txhelper/unsignedtx.go | 135 +++++++++-------- types.go | 6 - utils.go | 1 - 7 files changed, 236 insertions(+), 365 deletions(-) delete mode 100644 txhelper/changeamount.go diff --git a/txauthor.go b/txauthor.go index 4d41dcb6..fb6c1c4d 100644 --- a/txauthor.go +++ b/txauthor.go @@ -17,16 +17,18 @@ import ( ) type TxAuthor struct { + lw *LibWallet sendFromAccount uint32 - destinations []TransactionDestination requiredConfirmations int32 - lw *LibWallet + inputs []*wire.TxIn + destinations []txhelper.TransactionDestination + changeDestinations []txhelper.TransactionDestination } func (lw *LibWallet) NewUnsignedTx(sourceAccountNumber, requiredConfirmations int32) *TxAuthor { return &TxAuthor{ sendFromAccount: uint32(sourceAccountNumber), - destinations: make([]TransactionDestination, 0), + destinations: make([]txhelper.TransactionDestination, 0), requiredConfirmations: requiredConfirmations, lw: lw, } @@ -37,7 +39,7 @@ func (tx *TxAuthor) SetSourceAccount(accountNumber int32) { } func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax bool) { - tx.destinations = append(tx.destinations, TransactionDestination{ + tx.destinations = append(tx.destinations, txhelper.TransactionDestination{ Address: address, AtomAmount: atomAmount, SendMax: sendMax, @@ -45,7 +47,7 @@ func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax } func (tx *TxAuthor) UpdateSendDestination(index int, address string, atomAmount int64, sendMax bool) { - tx.destinations[index] = TransactionDestination{ + tx.destinations[index] = txhelper.TransactionDestination{ Address: address, AtomAmount: atomAmount, SendMax: sendMax, @@ -58,25 +60,58 @@ func (tx *TxAuthor) RemoveSendDestination(index int) { } } +func (tx *TxAuthor) AddChangeDestination(address string, atomAmount int64) { + tx.changeDestinations = append(tx.changeDestinations, txhelper.TransactionDestination{ + Address: address, + AtomAmount: atomAmount, + }) +} + +func (tx *TxAuthor) UpdateChangeDestination(index int, address string, atomAmount int64) { + tx.changeDestinations[index] = txhelper.TransactionDestination{ + Address: address, + AtomAmount: atomAmount, + } +} + +func (tx *TxAuthor) RemoveChangeDestination(index int) { + if len(tx.changeDestinations) > index { + tx.changeDestinations = append(tx.changeDestinations[:index], tx.changeDestinations[index+1:]...) + } +} + func (tx *TxAuthor) EstimateFeeAndSize() (*TxFeeAndSize, error) { - unsignedTx, err := tx.constructTransaction() + _, txSerializeSize, err := tx.constructTransaction() if err != nil { return nil, translateError(err) } - feeToSendTx := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, unsignedTx.EstimatedSignedSerializeSize) + feeToSendTx := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, txSerializeSize) feeAmount := &Amount{ AtomValue: int64(feeToSendTx), DcrValue: feeToSendTx.ToCoin(), } return &TxFeeAndSize{ - EstimatedSignedSize: unsignedTx.EstimatedSignedSerializeSize, + EstimatedSignedSize: txSerializeSize, Fee: feeAmount, }, nil } func (tx *TxAuthor) EstimateMaxSendAmount() (*Amount, error) { + sendMaxDestinations := 0 + for _, destination := range tx.destinations { + if destination.SendMax { + sendMaxDestinations++ + } + } + + if sendMaxDestinations == 0 { + return nil, fmt.Errorf("specify a send max destination to get max send amount estimate") + } else if sendMaxDestinations > 1 { + return nil, fmt.Errorf("multiple send max destinations not permitted") + } + txFeeAndSize, err := tx.EstimateFeeAndSize() if err != nil { return nil, err @@ -95,7 +130,48 @@ func (tx *TxAuthor) EstimateMaxSendAmount() (*Amount, error) { }, nil } -func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { +func (tx *TxAuthor) UseInputs(utxoKeys []string) error { + // first clear any previously set inputs + // so that an outdated set of inputs isn't used if an error occurs from this function + tx.inputs = nil + + accountUtxos, err := tx.lw.AllUnspentOutputs(tx.sendFromAccount, tx.requiredConfirmations) + if err != nil { + return fmt.Errorf("error reading unspent outputs in account: %v", err) + } + + retrieveAccountUtxo := func(utxoKey string) *UnspentOutput { + for _, accountUtxo := range accountUtxos { + if accountUtxo.OutputKey == utxoKey { + return accountUtxo + } + } + return nil + } + + inputs := make([]*wire.TxIn, 0, len(utxoKeys)) + + // retrieve utxo details for each key in the provided slice of utxoKeys + for _, utxoKey := range utxoKeys { + utxo := retrieveAccountUtxo(utxoKey) + if utxo == nil { + return fmt.Errorf("no valid utxo found for '%s' in the source account", utxoKey) + } + + // this is a reverse conversion and should not throw an error + // this []byte was originally converted from chainhash.Hash using chainhash.Hash[:] + txHash, _ := chainhash.NewHash(utxo.TransactionHash) + + outpoint := wire.NewOutPoint(txHash, utxo.OutputIndex, int8(utxo.Tree)) + input := wire.NewTxIn(outpoint, int64(utxo.Amount), nil) + inputs = append(inputs, input) + } + + tx.inputs = inputs + return nil +} + +func (tx *TxAuthor) Broadcast(privatePassphrase []byte) (string, error) { defer func() { for i := range privatePassphrase { privatePassphrase[i] = 0 @@ -105,24 +181,20 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { n, err := tx.lw.wallet.NetworkBackend() if err != nil { log.Error(err) - return nil, err + return "", err } - unsignedTx, err := tx.constructTransaction() + unsignedTx, _, err := tx.constructTransaction() if err != nil { - return nil, translateError(err) - } - - if unsignedTx.ChangeIndex >= 0 { - unsignedTx.RandomizeChangePosition() + return "", translateError(err) } var txBuf bytes.Buffer - txBuf.Grow(unsignedTx.Tx.SerializeSize()) - err = unsignedTx.Tx.Serialize(&txBuf) + txBuf.Grow(unsignedTx.SerializeSize()) + err = unsignedTx.Serialize(&txBuf) if err != nil { log.Error(err) - return nil, err + return "", err } var msgTx wire.MsgTx @@ -130,7 +202,7 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { if err != nil { log.Error(err) //Bytes do not represent a valid raw transaction - return nil, err + return "", err } lock := make(chan time.Time, 1) @@ -141,7 +213,7 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { err = tx.lw.wallet.Unlock(privatePassphrase, lock) if err != nil { log.Error(err) - return nil, errors.New(ErrInvalidPassphrase) + return "", errors.New(ErrInvalidPassphrase) } var additionalPkScripts map[wire.OutPoint][]byte @@ -149,7 +221,7 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { invalidSigs, err := tx.lw.wallet.SignTransaction(&msgTx, txscript.SigHashAll, additionalPkScripts, nil, nil) if err != nil { log.Error(err) - return nil, err + return "", err } invalidInputIndexes := make([]uint32, len(invalidSigs)) @@ -162,194 +234,74 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { err = msgTx.Serialize(&serializedTransaction) if err != nil { log.Error(err) - return nil, err + return "", err } err = msgTx.Deserialize(bytes.NewReader(serializedTransaction.Bytes())) if err != nil { //Invalid tx log.Error(err) - return nil, err + return "", err } txHash, err := tx.lw.wallet.PublishTransaction(&msgTx, serializedTransaction.Bytes(), n) if err != nil { - return nil, translateError(err) - } - return txHash[:], nil -} - -func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { - var err error - var outputs = make([]*wire.TxOut, 0) - var outputSelectionAlgorithm wallet.OutputSelectionAlgorithm = wallet.OutputSelectionAlgorithmDefault - var changeSource txauthor.ChangeSource - - for _, destination := range tx.destinations { - // validate the amount to send to this destination address - if !destination.SendMax && (destination.AtomAmount <= 0 || destination.AtomAmount > MaxAmountAtom) { - return nil, errors.E(errors.Invalid, "invalid amount") - } - - // check if multiple destinations are set to receive max amount - if destination.SendMax && changeSource != nil { - return nil, fmt.Errorf("cannot send max amount to multiple recipients") - } - - if destination.SendMax { - // This is a send max destination, set output selection algo to all. - outputSelectionAlgorithm = wallet.OutputSelectionAlgorithmAll - - // Use this destination address to make a changeSource rather than a tx output. - changeSource, err = txhelper.MakeTxChangeSource(destination.Address) - if err != nil { - log.Errorf("constructTransaction: error preparing change source: %v", err) - return nil, fmt.Errorf("max amount change source error: %v", err) - } - - continue // do not prepare a tx output for this destination - } - - output, err := txhelper.MakeTxOutput(destination.Address, destination.AtomAmount) - if err != nil { - log.Errorf("constructTransaction: error preparing tx output: %v", err) - return nil, fmt.Errorf("make tx output error: %v", err) - } - - outputs = append(outputs, output) + return "", translateError(err) } - return tx.lw.wallet.NewUnsignedTransaction(outputs, txrules.DefaultRelayFeePerKb, tx.sendFromAccount, - tx.requiredConfirmations, outputSelectionAlgorithm, changeSource) + return txHash.String(), nil } -func (lw *LibWallet) SendFromCustomInputs(sourceAccount uint32, requiredConfirmations int32, utxoKeys []string, - txDestinations []txhelper.TransactionDestination, changeDestinations []txhelper.TransactionDestination, privPass []byte) (string, error) { - - unspentOutputs, err := lw.AllUnspentOutputs(sourceAccount, requiredConfirmations) - if err != nil { - return "", err +func (tx *TxAuthor) constructTransaction() (*wire.MsgTx, int, error) { + if len(tx.inputs) != 0 { + // custom transaction + return tx.constructCustomTransaction() } - // loop through unspentOutputs to find user selected utxos - inputs := make([]*wire.TxIn, 0, len(utxoKeys)) - var totalInputAmount int64 - for _, utxo := range unspentOutputs { - useUtxo := false - for _, key := range utxoKeys { - if utxo.OutputKey == key { - useUtxo = true - } - } - if !useUtxo { - continue - } - - // this is a reverse conversion and should not throw an error - // this []byte was originally converted from chainhash.Hash using chainhash.Hash[:] - txHash, _ := chainhash.NewHash(utxo.TransactionHash) - - outpoint := wire.NewOutPoint(txHash, utxo.OutputIndex, int8(utxo.Tree)) - input := wire.NewTxIn(outpoint, int64(utxo.Amount), nil) - inputs = append(inputs, input) - totalInputAmount += input.ValueIn - - if len(inputs) != len(utxoKeys) { - return "", fmt.Errorf("could not find matching inputs for all provided utxo keys") - } + if len(tx.changeDestinations) != 0 { + return nil, 0, fmt.Errorf("specify custom inputs to use custom change outputs") } - unsignedTx, err := txhelper.NewUnsignedTx(inputs, txDestinations, changeDestinations, func() (address string, err error) { - return lw.NextAddress(int32(sourceAccount)) - }) + outputs, _, maxAmountRecipientAddress, err := txhelper.ParseOutputsAndChangeDestination(tx.destinations) if err != nil { - return "", err + log.Errorf("constructTransaction: error parsing tx destinations: %v", err) + return nil, 0, fmt.Errorf("error parsing tx destinations: %v", err) } - // serialize unsigned tx - var txBuf bytes.Buffer - txBuf.Grow(unsignedTx.SerializeSize()) - err = unsignedTx.Serialize(&txBuf) - if err != nil { - return "", fmt.Errorf("error serializing transaction: %s", err.Error()) - } + var outputSelectionAlgorithm wallet.OutputSelectionAlgorithm = wallet.OutputSelectionAlgorithmDefault + var changeSource txauthor.ChangeSource - txHash, err := lw.SignAndPublishTransaction(txBuf.Bytes(), privPass) - if err != nil { - return "", err + if maxAmountRecipientAddress != "" { + // set output selection algo to all + outputSelectionAlgorithm = wallet.OutputSelectionAlgorithmAll + // use the address to make a changeSource + changeSource, err = txhelper.MakeTxChangeSource(maxAmountRecipientAddress) + if err != nil { + log.Errorf("constructTransaction: error preparing change source: %v", err) + return nil, 0, fmt.Errorf("max amount change source error: %v", err) + } } - transactionHash, err := chainhash.NewHash(txHash) + unsignedTx, err := tx.lw.wallet.NewUnsignedTransaction(outputs, txrules.DefaultRelayFeePerKb, tx.sendFromAccount, + tx.requiredConfirmations, outputSelectionAlgorithm, changeSource) if err != nil { - return "", fmt.Errorf("error parsing successful transaction hash: %s", err.Error()) + return nil, 0, err } - return transactionHash.String(), nil + return unsignedTx.Tx, unsignedTx.EstimatedSignedSerializeSize, nil } -func (lw *LibWallet) SignAndPublishTransaction(serializedTx, privPass []byte) ([]byte, error) { - n, err := lw.wallet.NetworkBackend() - if err != nil { - log.Error(err) - return nil, err - } - defer func() { - for i := range privPass { - privPass[i] = 0 +func (tx *TxAuthor) constructCustomTransaction() (*wire.MsgTx, int, error) { + // Used to generate an internal address for change, + // if no change destination is provided and + // no recipient is set to receive max amount. + changeAddressFn := func() (string, error) { + addr, err := tx.lw.wallet.NewChangeAddress(tx.sendFromAccount) + if err != nil { + return "", err } - }() - - var tx wire.MsgTx - err = tx.Deserialize(bytes.NewReader(serializedTx)) - if err != nil { - log.Error(err) - //Bytes do not represent a valid raw transaction - return nil, err + return addr.EncodeAddress(), nil } - lock := make(chan time.Time, 1) - defer func() { - lock <- time.Time{} - }() - - err = lw.wallet.Unlock(privPass, lock) - if err != nil { - log.Error(err) - return nil, errors.New(ErrInvalidPassphrase) - } - - var additionalPkScripts map[wire.OutPoint][]byte - - invalidSigs, err := lw.wallet.SignTransaction(&tx, txscript.SigHashAll, additionalPkScripts, nil, nil) - if err != nil { - log.Error(err) - return nil, err - } - - invalidInputIndexes := make([]uint32, len(invalidSigs)) - for i, e := range invalidSigs { - invalidInputIndexes[i] = e.InputIndex - } - - var serializedTransaction bytes.Buffer - serializedTransaction.Grow(tx.SerializeSize()) - err = tx.Serialize(&serializedTransaction) - if err != nil { - log.Error(err) - return nil, err - } - - var msgTx wire.MsgTx - err = msgTx.Deserialize(bytes.NewReader(serializedTransaction.Bytes())) - if err != nil { - //Invalid tx - log.Error(err) - return nil, err - } - - txHash, err := lw.wallet.PublishTransaction(&msgTx, serializedTransaction.Bytes(), n) - if err != nil { - return nil, translateError(err) - } - return txHash[:], nil + return txhelper.NewUnsignedTx(tx.inputs, tx.destinations, tx.changeDestinations, changeAddressFn) } diff --git a/txhelper/changeamount.go b/txhelper/changeamount.go deleted file mode 100644 index 2b37fa88..00000000 --- a/txhelper/changeamount.go +++ /dev/null @@ -1,44 +0,0 @@ -package txhelper - -import ( - "errors" - "fmt" - - "github.com/decred/dcrd/dcrutil" - "github.com/decred/dcrd/txscript" - "github.com/decred/dcrd/wire" - "github.com/decred/dcrwallet/wallet/txrules" - "github.com/raedahgroup/dcrlibwallet/txhelper/internal/txsizes" -) - -func EstimateChangeWithOutputs(numberOfInputs int, totalInputAmount int64, outputs []*wire.TxOut, totalSendAmount int64, changeAddresses []string) (int64, error) { - if totalSendAmount >= totalInputAmount { - return 0, fmt.Errorf("total send amount (%s) is higher than or equal to the total input amount (%s)", - dcrutil.Amount(totalSendAmount).String(), dcrutil.Amount(totalInputAmount).String()) - } - - totalChangeScriptSize, err := calculateChangeScriptSize(changeAddresses) - if err != nil { - return 0, err - } - - scriptSizes := make([]int, numberOfInputs) - for i := 0; i < numberOfInputs; i++ { - scriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize - } - - relayFeePerKb := txrules.DefaultRelayFeePerKb - maxSignedSize := txsizes.EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) - maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) - changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) - - // if change amount is valid, check if the script size exceeds maximum script size - if changeAmount > 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), totalChangeScriptSize, relayFeePerKb) { - maxChangeScriptSize := len(changeAddresses) * txscript.MaxScriptElementSize - if totalChangeScriptSize > maxChangeScriptSize { - return 0, errors.New("script size exceed maximum bytes pushable to the stack") - } - } - - return changeAmount, nil -} diff --git a/txhelper/outputs.go b/txhelper/outputs.go index fbd8eaa6..15f29b08 100644 --- a/txhelper/outputs.go +++ b/txhelper/outputs.go @@ -6,6 +6,7 @@ import ( "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/errors" "github.com/raedahgroup/dcrlibwallet/addresshelper" ) @@ -23,85 +24,41 @@ func MakeTxOutput(address string, amountInAtom int64) (output *wire.TxOut, err e return } -// TxOutputsExtractMaxChangeDestination checks the provided txDestinations -// if there is 1 and not more than 1 recipient set to receive max amount. -// Returns an error if more than 1 max amount recipients identified. -// Returns the outputs for the tx excluding the max amount recipient, -// and also returns the change object for the max amount recipient address if there is a max recipient address. -func TxOutputsExtractMaxChangeDestination(nInputs int, totalInputAmount int64, txDestinations []TransactionDestination) ( - outputs []*wire.TxOut, totalSendAmount int64, maxChangeDestinations []TransactionDestination, err error) { +// ParseOutputsAndChangeDestination generates and returns TxOuts +// using the provided slice of transaction destinations. +// Any destination set to receive max amount is not included in the TxOuts returned, +// but is instead returned as a change destination. +// Returns an error if more than 1 max amount recipients identified or +// if any other error is encountered while processing the addresses and amounts. +func ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ([]*wire.TxOut, int64, string, error) { + var outputs = make([]*wire.TxOut, 0) + var totalSendAmount int64 + var maxAmountRecipientAddress string - outputs, totalSendAmount, maxAmountRecipientAddress, err := TxOutputsExtractMaxDestinationAddress(txDestinations) - if err != nil { - return - } - - if maxAmountRecipientAddress != "" { - // use as change address - changeAddresses := []string{maxAmountRecipientAddress} - changeAmount, err := EstimateChangeWithOutputs(nInputs, totalInputAmount, outputs, totalSendAmount, changeAddresses) - if err != nil { - return nil, 0, nil, err - } - - if changeAmount < 0 { - excessSpending := 0 - changeAmount // equivalent to math.Abs() - err = fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", - dcrutil.Amount(excessSpending).String()) - return nil, 0, nil, err + for _, destination := range txDestinations { + // validate the amount to send to this destination address + if !destination.SendMax && (destination.AtomAmount <= 0 || destination.AtomAmount > dcrutil.MaxAmount) { + return nil, 0, "", errors.E(errors.Invalid, "invalid amount") } - maxChangeDestinations = []TransactionDestination{ - { - Address: maxAmountRecipientAddress, - Amount: dcrutil.Amount(changeAmount).ToCoin(), - }, + // check if multiple destinations are set to receive max amount + if destination.SendMax && maxAmountRecipientAddress != "" { + return nil, 0, "", fmt.Errorf("cannot send max amount to multiple recipients") } - } - return -} - -// TxOutputsExtractMaxDestinationAddress checks the provided txDestinations -// if there is 1 and not more than 1 recipient set to receive max amount. -// Returns an error if more than 1 max amount recipients identified. -// Returns the outputs for the tx excluding the max amount recipient, -// and also returns the max amount recipient address if there is one. -func TxOutputsExtractMaxDestinationAddress(txDestinations []TransactionDestination) ( - outputs []*wire.TxOut, totalSendAmount int64, maxAmountRecipientAddress string, err error) { - - // check if there's a max amount recipient, and not more than 1 such recipient - nOutputs := len(txDestinations) - for _, destination := range txDestinations { - if destination.SendMax && maxAmountRecipientAddress != "" { - err = fmt.Errorf("cannot send max amount to multiple recipients") - return - } else if destination.SendMax { + if destination.SendMax { maxAmountRecipientAddress = destination.Address - nOutputs-- + continue // do not prepare a tx output for this destination } - } - // create transaction outputs for all destination addresses and amounts, excluding destination for send max - outputs = make([]*wire.TxOut, 0, nOutputs) - var output *wire.TxOut - for _, destination := range txDestinations { - if !destination.SendMax { - amount, err := dcrutil.NewAmount(destination.Amount) - if err != nil { - err = fmt.Errorf("invalid amount: %v", destination.Amount) - return nil, 0, "", err - } - - output, err = MakeTxOutput(destination.Address, int64(amount)) - if err != nil { - return - } - - outputs = append(outputs, output) - totalSendAmount += output.Value + output, err := MakeTxOutput(destination.Address, destination.AtomAmount) + if err != nil { + return nil, 0, "", fmt.Errorf("make tx output error: %v", err) } + + totalSendAmount += output.Value + outputs = append(outputs, output) } - return + return outputs, totalSendAmount, maxAmountRecipientAddress, nil } diff --git a/txhelper/types.go b/txhelper/types.go index ef193766..028e2501 100644 --- a/txhelper/types.go +++ b/txhelper/types.go @@ -14,7 +14,7 @@ const ( ) type TransactionDestination struct { - Address string - Amount float64 - SendMax bool + Address string + AtomAmount int64 + SendMax bool } diff --git a/txhelper/unsignedtx.go b/txhelper/unsignedtx.go index b2ae6076..21dd1608 100644 --- a/txhelper/unsignedtx.go +++ b/txhelper/unsignedtx.go @@ -6,119 +6,132 @@ import ( "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/errors" + "github.com/decred/dcrwallet/wallet/txauthor" "github.com/decred/dcrwallet/wallet/txrules" "github.com/raedahgroup/dcrlibwallet/txhelper/internal/txsizes" ) -type GenerateAddressFunc func() (address string, err error) +type NextAddressFunc func() (address string, err error) // NewUnsignedTx uses the inputs to prepare a tx with outputs for the provided send destinations and change destinations. -// If any of the send destinations is set to receive max amount, that destination address is used as single change destination. +// If any of the send destinations is set to receive max amount, +// that destination address is used as single change destination. // If no change destinations are provided and no recipient is set to receive max amount, -// a single change destination is created for an address gotten by calling `generateAccountAddress()`. +// a single change destination is created for an address gotten by calling `nextInternalAddress()`. func NewUnsignedTx(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, - generateAccountAddress GenerateAddressFunc) (*wire.MsgTx, error) { + nextInternalAddress NextAddressFunc) (*wire.MsgTx, int, error) { - var totalInputAmount int64 - scriptSizes := make([]int, 0, len(inputs)) - for _, txIn := range inputs { - totalInputAmount += txIn.ValueIn - scriptSizes = append(scriptSizes, txsizes.RedeemP2PKHSigScriptSize) - } - - outputs, totalSendAmount, maxChangeDestinations, err := TxOutputsExtractMaxChangeDestination(len(inputs), totalInputAmount, sendDestinations) + outputs, totalSendAmount, maxAmountRecipientAddress, err := ParseOutputsAndChangeDestination(sendDestinations) if err != nil { - return nil, err - } - - if totalSendAmount > totalInputAmount { - return nil, fmt.Errorf("total send amount (%s) is higher than the total input amount (%s)", - dcrutil.Amount(totalSendAmount).String(), dcrutil.Amount(totalInputAmount).String()) + return nil, 0, err } - // if a max change destination is returned, use it as the only change destination - if len(maxChangeDestinations) == 1 { - changeDestinations = maxChangeDestinations + if maxAmountRecipientAddress != "" && len(changeDestinations) > 0 { + return nil, 0, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ + " change destinations must not be provided") } - // create a default change destination if none is specified and there is a change amount from this tx - if len(changeDestinations) == 0 { - changeAddress, err := generateAccountAddress() + if maxAmountRecipientAddress == "" && len(changeDestinations) == 0 { + // no change specified, generate new internal address to use as change (max amount recipient) + maxAmountRecipientAddress, err = nextInternalAddress() if err != nil { - return nil, fmt.Errorf("error generating change address for tx: %s", err.Error()) + return nil, 0, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) } + } - changeAmount, err := EstimateChangeWithOutputs(len(inputs), totalInputAmount, outputs, totalSendAmount, []string{changeAddress}) - if err != nil { - return nil, fmt.Errorf("error in getting change amount: %s", err.Error()) - } - if changeAmount > 0 { - changeDestinations = append(changeDestinations, TransactionDestination{ - Address: changeAddress, - Amount: dcrutil.Amount(changeAmount).ToCoin(), - }) - } + var totalInputAmount int64 + inputScriptSizes := make([]int, len(inputs)) + inputScripts := make([][]byte, len(inputs)) + for i, input := range inputs { + totalInputAmount += input.ValueIn + inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize + inputScripts[i] = input.SignatureScript } - changeAddresses := make([]string, len(changeDestinations)) - for i, changeDestination := range changeDestinations { - changeAddresses[i] = changeDestination.Address + var changeScriptSize int + if maxAmountRecipientAddress != "" { + changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress) + } else { + changeScriptSize, err = calculateMultipleChangeScriptSize(changeDestinations) } - totalChangeScriptSize, err := calculateChangeScriptSize(changeAddresses) if err != nil { - return nil, fmt.Errorf("error processing change outputs: %s", err.Error()) + return nil, 0, err } - maxSignedSize := txsizes.EstimateSerializeSize(scriptSizes, outputs, totalChangeScriptSize) + maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) if changeAmount < 0 { excessSpending := 0 - changeAmount // equivalent to math.Abs() - return nil, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", + return nil, 0, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", dcrutil.Amount(excessSpending).String()) } - if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), totalChangeScriptSize, txrules.DefaultRelayFeePerKb) { - maxAcceptableChangeScriptSize := len(changeDestinations) * txscript.MaxScriptElementSize - if totalChangeScriptSize > maxAcceptableChangeScriptSize { - return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") + if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), changeScriptSize, txrules.DefaultRelayFeePerKb) { + if changeScriptSize > txscript.MaxScriptElementSize { + return nil, 0, fmt.Errorf("script size exceed maximum bytes pushable to the stack") } - changeOutputs, totalChangeAmount, err := makeTxOutputs(changeDestinations) - if err != nil { - return nil, fmt.Errorf("error creating change outputs for tx: %s", err.Error()) + if maxAmountRecipientAddress != "" { + singleChangeDestination := TransactionDestination{ + Address: maxAmountRecipientAddress, + AtomAmount: changeAmount, + } + changeDestinations = []TransactionDestination{singleChangeDestination} } - if totalChangeAmount > changeAmount { - return nil, fmt.Errorf("total amount allocated to change addresses (%s) is higher than actual change amount for transaction (%s)", - dcrutil.Amount(totalChangeAmount).String(), dcrutil.Amount(changeAmount).String()) + var totalChangeAmount int64 + for _, changeDestination := range changeDestinations { + changeOutput, err := MakeTxOutput(changeDestination.Address, changeDestination.AtomAmount) + if err != nil { + return nil, 0, fmt.Errorf("change address error: %v", err) + } + + totalChangeAmount += changeOutput.Value + outputs = append(outputs, changeOutput) + + // randomize the change output that was just added + changeOutputIndex := len(outputs) - 1 + txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) } - // todo dcrwallet randomizes change position, should look into that as well - outputs = append(outputs, changeOutputs...) + if totalChangeAmount > changeAmount { + return nil, 0, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ + " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), + dcrutil.Amount(changeAmount).String()) + } + } else { + maxSignedSize = txsizes.EstimateSerializeSize(inputScriptSizes, outputs, 0) } - unsignedTransaction := &wire.MsgTx{ + return &wire.MsgTx{ SerType: wire.TxSerializeFull, Version: wire.TxVersion, TxIn: inputs, TxOut: outputs, LockTime: 0, Expiry: 0, - } + }, maxSignedSize, nil +} - return unsignedTransaction, nil +func calculateChangeScriptSize(changeAddress string) (int, error) { + changeSource, err := MakeTxChangeSource(changeAddress) + if err != nil { + return 0, fmt.Errorf("change address error: %v", err) + } + return changeSource.ScriptSize(), nil } -func calculateChangeScriptSize(changeAddresses []string) (int, error) { +func calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination) (int, error) { var totalChangeScriptSize int - for _, changeAddress := range changeAddresses { - changeSource, err := MakeTxChangeSource(changeAddress) + for _, changeDestination := range changeDestinations { + changeScriptSize, err := calculateChangeScriptSize(changeDestination.Address) if err != nil { return 0, err } - totalChangeScriptSize += changeSource.ScriptSize() + totalChangeScriptSize += changeScriptSize } return totalChangeScriptSize, nil } diff --git a/types.go b/types.go index 6f5ad3ee..fce89d68 100644 --- a/types.go +++ b/types.go @@ -176,12 +176,6 @@ type WalletAccount struct { AccountName string `json:"account_name"` } -type TransactionDestination struct { - Address string - AtomAmount int64 - SendMax bool -} - /** end tx-related types */ /** begin ticket-related types */ diff --git a/utils.go b/utils.go index 47b55b29..f44e057f 100644 --- a/utils.go +++ b/utils.go @@ -38,7 +38,6 @@ const ( ) func (lw *LibWallet) listenForShutdown() { - lw.cancelFuncs = make([]context.CancelFunc, 0) lw.shuttingDown = make(chan bool) go func() { From 72dee2a73c686c6ede1f36e6bc7bc0b63ac0d63b Mon Sep 17 00:00:00 2001 From: Wisdom Arerosuoghene Date: Tue, 1 Oct 2019 09:10:15 +0100 Subject: [PATCH 06/15] add props to UTXO struct --- accounts.go | 23 +++++++++++++++++++++-- txauthor.go | 2 +- types.go | 2 ++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/accounts.go b/accounts.go index 2c5e581d..bc6d925e 100644 --- a/accounts.go +++ b/accounts.go @@ -8,6 +8,8 @@ import ( "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrwallet/errors" "github.com/decred/dcrwallet/wallet" + "github.com/raedahgroup/dcrlibwallet/addresshelper" + "strings" ) func (lw *LibWallet) GetAccounts(requiredConfirmations int32) (string, error) { @@ -78,9 +80,9 @@ func (lw *LibWallet) SpendableForAccount(account int32, requiredConfirmations in return int64(bals.Spendable), nil } -func (lw *LibWallet) AllUnspentOutputs(account uint32, requiredConfirmations int32) ([]*UnspentOutput, error) { +func (lw *LibWallet) AllUnspentOutputs(account int32, requiredConfirmations int32) ([]*UnspentOutput, error) { policy := wallet.OutputSelectionPolicy{ - Account: account, + Account: uint32(account), RequiredConfirmations: requiredConfirmations, } @@ -103,6 +105,21 @@ func (lw *LibWallet) AllUnspentOutputs(account uint32, requiredConfirmations int // unique key to identify utxo outputKey := fmt.Sprintf("%s:%d", input.PreviousOutPoint.Hash, input.PreviousOutPoint.Index) + addresses, err := addresshelper.PkScriptAddresses(lw.activeNet.Params, inputDetail.Scripts[i]) + if err != nil { + return nil, fmt.Errorf("error reading address details for unspent output: %v", err) + } + + previousTx, err := lw.GetTransactionRaw(input.PreviousOutPoint.Hash[:]) + if err != nil { + return nil, fmt.Errorf("error reading tx details for unspent output: %v", err) + } + + var confirmations int32 = 0 + if previousTx.BlockHeight != -1 { + confirmations = lw.GetBestBlock() - previousTx.BlockHeight + 1 + } + unspentOutputs[i] = &UnspentOutput{ TransactionHash: input.PreviousOutPoint.Hash[:], OutputIndex: input.PreviousOutPoint.Index, @@ -112,6 +129,8 @@ func (lw *LibWallet) AllUnspentOutputs(account uint32, requiredConfirmations int PkScript: inputDetail.Scripts[i], ReceiveTime: outputInfo.Received.Unix(), FromCoinbase: outputInfo.FromCoinbase, + Address: strings.Join(addresses, ", "), + Confirmations: confirmations, } } diff --git a/txauthor.go b/txauthor.go index fb6c1c4d..a394b43d 100644 --- a/txauthor.go +++ b/txauthor.go @@ -135,7 +135,7 @@ func (tx *TxAuthor) UseInputs(utxoKeys []string) error { // so that an outdated set of inputs isn't used if an error occurs from this function tx.inputs = nil - accountUtxos, err := tx.lw.AllUnspentOutputs(tx.sendFromAccount, tx.requiredConfirmations) + accountUtxos, err := tx.lw.AllUnspentOutputs(int32(tx.sendFromAccount), tx.requiredConfirmations) if err != nil { return fmt.Errorf("error reading unspent outputs in account: %v", err) } diff --git a/types.go b/types.go index fce89d68..8267f5cc 100644 --- a/types.go +++ b/types.go @@ -231,4 +231,6 @@ type UnspentOutput struct { FromCoinbase bool Tree int32 PkScript []byte + Address string + Confirmations int32 } From 981bde2d3a6be03a1f513cbad9bac5df58a51566 Mon Sep 17 00:00:00 2001 From: song50119 Date: Thu, 24 Sep 2020 19:11:14 +0700 Subject: [PATCH 07/15] Refactor code (*Wallet).UnspentOutputs method --- accounts.go | 21 +++------ txauthor.go | 109 ++++++++++++++++++++++++++++++++++++++++++++-- unsignedtx.go | 118 +++----------------------------------------------- 3 files changed, 117 insertions(+), 131 deletions(-) diff --git a/accounts.go b/accounts.go index 15cc1946..bebcb45d 100644 --- a/accounts.go +++ b/accounts.go @@ -124,10 +124,10 @@ func (wallet *Wallet) SpendableForAccount(account int32) (int64, error) { return int64(bals.Spendable), nil } -func (wallet *Wallet) AllUnspentOutputs(account int32, requiredConfirmations int32) ([]*UnspentOutput, error) { +func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { policy := dcrwallet.OutputSelectionPolicy{ Account: uint32(account), - RequiredConfirmations: requiredConfirmations, + RequiredConfirmations: wallet.RequiredConfirmations(), } // fetch all utxos in account to extract details for the utxos selected by user @@ -149,19 +149,19 @@ func (wallet *Wallet) AllUnspentOutputs(account int32, requiredConfirmations int // unique key to identify utxo outputKey := fmt.Sprintf("%s:%d", input.PreviousOutPoint.Hash, input.PreviousOutPoint.Index) - addresses, err := addresshelper.PkScriptAddresses(lw.activeNet.Params, inputDetail.Scripts[i]) + addresses, err := addresshelper.PkScriptAddresses(wallet.chainParams, inputDetail.Scripts[i]) if err != nil { return nil, fmt.Errorf("error reading address details for unspent output: %v", err) } - previousTx, err := lw.GetTransactionRaw(input.PreviousOutPoint.Hash[:]) + previousTx, err := wallet.GetTransactionRaw(input.PreviousOutPoint.Hash[:]) if err != nil { return nil, fmt.Errorf("error reading tx details for unspent output: %v", err) } var confirmations int32 = 0 if previousTx.BlockHeight != -1 { - confirmations = lw.GetBestBlock() - previousTx.BlockHeight + 1 + confirmations = wallet.GetBestBlock() - previousTx.BlockHeight + 1 } unspentOutputs[i] = &UnspentOutput{ @@ -181,16 +181,7 @@ func (wallet *Wallet) AllUnspentOutputs(account int32, requiredConfirmations int return unspentOutputs, nil } -func (wallet *Wallet) NextAccount(accountName string, privPass []byte) error { - _, err := wallet.internal.NextAccount(accountName) - if err != nil { - log.Error(err) - return err - } - return nil -} - -func (lw *LibWallet) NextAccountRaw(accountName string, privPass []byte) (uint32, error) { +func (wallet *Wallet) NextAccount(accountName string, privPass []byte) (int32, error) { lock := make(chan time.Time, 1) defer func() { for i := range privPass { diff --git a/txauthor.go b/txauthor.go index aed56cc5..bbb2f9ec 100644 --- a/txauthor.go +++ b/txauthor.go @@ -14,6 +14,7 @@ import ( w "github.com/decred/dcrwallet/wallet/v3" "github.com/decred/dcrwallet/wallet/v3/txauthor" "github.com/decred/dcrwallet/wallet/v3/txrules" + "github.com/decred/dcrwallet/wallet/v3/txsizes" "github.com/planetdecred/dcrlibwallet/txhelper" ) @@ -120,8 +121,7 @@ func (tx *TxAuthor) UseInputs(utxoKeys []string) error { // first clear any previously set inputs // so that an outdated set of inputs isn't used if an error occurs from this function tx.inputs = nil - requiredConfirmations := tx.sourceWallet.RequiredConfirmations() - accountUtxos, err := tx.sourceWallet.AllUnspentOutputs(int32(tx.sourceAccountNumber), requiredConfirmations) + accountUtxos, err := tx.sourceWallet.UnspentOutputs(int32(tx.sourceAccountNumber)) if err != nil { return fmt.Errorf("error reading unspent outputs in account: %v", err) } @@ -333,7 +333,7 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { // Used to generate an internal address for change, // if no change destination is provided and // no recipient is set to receive max amount. - changeAddressFn := func() (string, error) { + nextInternalAddress := func() (string, error) { ctx := tx.sourceWallet.shutdownContext() addr, err := tx.sourceWallet.internal.NewChangeAddress(ctx, tx.sourceAccountNumber) if err != nil { @@ -342,5 +342,106 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { return addr.Address(), nil } - return NewUnsignedTx(tx.inputs, tx.destinations, tx.changeDestinations, changeAddressFn) + outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(tx.destinations) + if err != nil { + return nil, err + } + + if maxAmountRecipientAddress != "" && len(tx.changeDestinations) > 0 { + return nil, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ + " change destinations must not be provided") + } + + if maxAmountRecipientAddress == "" && len(tx.changeDestinations) == 0 { + // no change specified, generate new internal address to use as change (max amount recipient) + maxAmountRecipientAddress, err = nextInternalAddress() + if err != nil { + return nil, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) + } + } + + var totalInputAmount int64 + inputScriptSizes := make([]int, len(tx.inputs)) + inputScripts := make([][]byte, len(tx.inputs)) + for i, input := range tx.inputs { + totalInputAmount += input.ValueIn + inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize + inputScripts[i] = input.SignatureScript + } + + var changeScriptSize int + if maxAmountRecipientAddress != "" { + changeScriptSize, err = tx.calculateChangeScriptSize(maxAmountRecipientAddress) + } else { + changeScriptSize, err = tx.calculateMultipleChangeScriptSize(tx.changeDestinations) + } + if err != nil { + return nil, err + } + + maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) + maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) + changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) + + if changeAmount < 0 { + excessSpending := 0 - changeAmount // equivalent to math.Abs() + return nil, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", + dcrutil.Amount(excessSpending).String()) + } + + if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), changeScriptSize, txrules.DefaultRelayFeePerKb) { + if changeScriptSize > txscript.MaxScriptElementSize { + return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") + } + + if maxAmountRecipientAddress != "" { + singleChangeDestination := TransactionDestination{ + Address: maxAmountRecipientAddress, + AtomAmount: changeAmount, + } + tx.changeDestinations = []TransactionDestination{singleChangeDestination} + } + + var totalChangeAmount int64 + for _, changeDestination := range tx.changeDestinations { + changeOutput, err := txhelper.MakeTxOutput(changeDestination.Address, changeDestination.AtomAmount, tx.sourceWallet.chainParams) + if err != nil { + return nil, fmt.Errorf("change address error: %v", err) + } + + totalChangeAmount += changeOutput.Value + outputs = append(outputs, changeOutput) + + // randomize the change output that was just added + changeOutputIndex := len(outputs) - 1 + txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) + } + + if totalChangeAmount > changeAmount { + return nil, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ + " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), + dcrutil.Amount(changeAmount).String()) + } + } + // else { + // maxSignedSize = txsizes.EstimateSerializeSize(inputScriptSizes, outputs, 0) + // } + + return &txauthor.AuthoredTx{ + Tx: &wire.MsgTx{ + SerType: wire.TxSerializeFull, + Version: wire.TxVersion, + TxIn: tx.inputs, + TxOut: outputs, + LockTime: 0, + Expiry: 0, + }}, nil + // return &wire.MsgTx{ + // SerType: wire.TxSerializeFull, + // Version: wire.TxVersion, + // TxIn: tx.inputs, + // TxOut: outputs, + // LockTime: 0, + // Expiry: 0, + // }, nil } diff --git a/unsignedtx.go b/unsignedtx.go index 734c828f..cece2b10 100644 --- a/unsignedtx.go +++ b/unsignedtx.go @@ -4,131 +4,25 @@ import ( "fmt" "github.com/decred/dcrd/dcrutil/v2" - "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" "github.com/decred/dcrwallet/errors/v2" - "github.com/decred/dcrwallet/wallet/txauthor" - "github.com/decred/dcrwallet/wallet/txrules" - "github.com/decred/dcrwallet/wallet/v3/txsizes" "github.com/planetdecred/dcrlibwallet/txhelper" ) type NextAddressFunc func() (address string, err error) -// NewUnsignedTx uses the inputs to prepare a tx with outputs for the provided send destinations and change destinations. -// If any of the send destinations is set to receive max amount, -// that destination address is used as single change destination. -// If no change destinations are provided and no recipient is set to receive max amount, -// a single change destination is created for an address gotten by calling `nextInternalAddress()`. -func NewUnsignedTx(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, - nextInternalAddress NextAddressFunc) (*wire.MsgTx, int, error) { - - outputs, totalSendAmount, maxAmountRecipientAddress, err := ParseOutputsAndChangeDestination(sendDestinations) - if err != nil { - return nil, 0, err - } - - if maxAmountRecipientAddress != "" && len(changeDestinations) > 0 { - return nil, 0, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ - " change destinations must not be provided") - } - - if maxAmountRecipientAddress == "" && len(changeDestinations) == 0 { - // no change specified, generate new internal address to use as change (max amount recipient) - maxAmountRecipientAddress, err = nextInternalAddress() - if err != nil { - return nil, 0, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) - } - } - - var totalInputAmount int64 - inputScriptSizes := make([]int, len(inputs)) - inputScripts := make([][]byte, len(inputs)) - for i, input := range inputs { - totalInputAmount += input.ValueIn - inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize - inputScripts[i] = input.SignatureScript - } - - var changeScriptSize int - if maxAmountRecipientAddress != "" { - changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress) - } else { - changeScriptSize, err = calculateMultipleChangeScriptSize(changeDestinations) - } - if err != nil { - return nil, 0, err - } - - maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) - maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) - changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) - - if changeAmount < 0 { - excessSpending := 0 - changeAmount // equivalent to math.Abs() - return nil, 0, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", - dcrutil.Amount(excessSpending).String()) - } - - if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), changeScriptSize, txrules.DefaultRelayFeePerKb) { - if changeScriptSize > txscript.MaxScriptElementSize { - return nil, 0, fmt.Errorf("script size exceed maximum bytes pushable to the stack") - } - - if maxAmountRecipientAddress != "" { - singleChangeDestination := TransactionDestination{ - Address: maxAmountRecipientAddress, - AtomAmount: changeAmount, - } - changeDestinations = []TransactionDestination{singleChangeDestination} - } - - var totalChangeAmount int64 - for _, changeDestination := range changeDestinations { - changeOutput, err := txhelper.MakeTxOutput(changeDestination.Address, changeDestination.AtomAmount) - if err != nil { - return nil, 0, fmt.Errorf("change address error: %v", err) - } - - totalChangeAmount += changeOutput.Value - outputs = append(outputs, changeOutput) - - // randomize the change output that was just added - changeOutputIndex := len(outputs) - 1 - txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) - } - - if totalChangeAmount > changeAmount { - return nil, 0, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ - " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), - dcrutil.Amount(changeAmount).String()) - } - } else { - maxSignedSize = txsizes.EstimateSerializeSize(inputScriptSizes, outputs, 0) - } - - return &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: wire.TxVersion, - TxIn: inputs, - TxOut: outputs, - LockTime: 0, - Expiry: 0, - }, maxSignedSize, nil -} - -func calculateChangeScriptSize(changeAddress string) (int, error) { - changeSource, err := txhelper.MakeTxChangeSource(changeAddress) +func (tx *TxAuthor) calculateChangeScriptSize(changeAddress string) (int, error) { + changeSource, err := txhelper.MakeTxChangeSource(changeAddress, tx.sourceWallet.chainParams) if err != nil { return 0, fmt.Errorf("change address error: %v", err) } return changeSource.ScriptSize(), nil } -func calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination) (int, error) { +func (tx *TxAuthor) calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination) (int, error) { var totalChangeScriptSize int for _, changeDestination := range changeDestinations { - changeScriptSize, err := calculateChangeScriptSize(changeDestination.Address) + changeScriptSize, err := tx.calculateChangeScriptSize(changeDestination.Address) if err != nil { return 0, err } @@ -143,7 +37,7 @@ func calculateMultipleChangeScriptSize(changeDestinations []TransactionDestinati // but is instead returned as a change destination. // Returns an error if more than 1 max amount recipients identified or // if any other error is encountered while processing the addresses and amounts. -func ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ([]*wire.TxOut, int64, string, error) { +func (tx *TxAuthor) ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ([]*wire.TxOut, int64, string, error) { var outputs = make([]*wire.TxOut, 0) var totalSendAmount int64 var maxAmountRecipientAddress string @@ -164,7 +58,7 @@ func ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ( continue // do not prepare a tx output for this destination } - output, err := txhelper.MakeTxOutput(destination.Address, destination.AtomAmount) + output, err := txhelper.MakeTxOutput(destination.Address, destination.AtomAmount, tx.sourceWallet.chainParams) if err != nil { return nil, 0, "", fmt.Errorf("make tx output error: %v", err) } From 4062b62feab4fc51619ffe1a77f54b87b947a191 Mon Sep 17 00:00:00 2001 From: song50119 Date: Thu, 1 Oct 2020 19:42:24 +0700 Subject: [PATCH 08/15] Calculate amount and return txAuthor --- accounts.go | 1 - txauthor.go | 127 +------------------------------ unsignedtx.go | 71 ----------------- utxo.go | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 195 deletions(-) delete mode 100644 unsignedtx.go create mode 100644 utxo.go diff --git a/accounts.go b/accounts.go index bebcb45d..74bde7de 100644 --- a/accounts.go +++ b/accounts.go @@ -163,7 +163,6 @@ func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { if previousTx.BlockHeight != -1 { confirmations = wallet.GetBestBlock() - previousTx.BlockHeight + 1 } - unspentOutputs[i] = &UnspentOutput{ TransactionHash: input.PreviousOutPoint.Hash[:], OutputIndex: input.PreviousOutPoint.Index, diff --git a/txauthor.go b/txauthor.go index bbb2f9ec..00971cc7 100644 --- a/txauthor.go +++ b/txauthor.go @@ -14,7 +14,6 @@ import ( w "github.com/decred/dcrwallet/wallet/v3" "github.com/decred/dcrwallet/wallet/v3/txauthor" "github.com/decred/dcrwallet/wallet/v3/txrules" - "github.com/decred/dcrwallet/wallet/v3/txsizes" "github.com/planetdecred/dcrlibwallet/txhelper" ) @@ -243,16 +242,15 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { } func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { + if len(tx.inputs) != 0 { + return tx.constructCustomTransaction() + } + var err error var outputs = make([]*wire.TxOut, 0) var outputSelectionAlgorithm w.OutputSelectionAlgorithm = w.OutputSelectionAlgorithmDefault var changeSource txauthor.ChangeSource - if len(tx.inputs) != 0 { - // custom transaction - return tx.constructCustomTransaction() - } - ctx := tx.sourceWallet.shutdownContext() for _, destination := range tx.destinations { @@ -328,120 +326,3 @@ func (tx *TxAuthor) changeSource(ctx context.Context) (txauthor.ChangeSource, er return changeSource, nil } - -func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { - // Used to generate an internal address for change, - // if no change destination is provided and - // no recipient is set to receive max amount. - nextInternalAddress := func() (string, error) { - ctx := tx.sourceWallet.shutdownContext() - addr, err := tx.sourceWallet.internal.NewChangeAddress(ctx, tx.sourceAccountNumber) - if err != nil { - return "", err - } - return addr.Address(), nil - } - - outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(tx.destinations) - if err != nil { - return nil, err - } - - if maxAmountRecipientAddress != "" && len(tx.changeDestinations) > 0 { - return nil, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ - " change destinations must not be provided") - } - - if maxAmountRecipientAddress == "" && len(tx.changeDestinations) == 0 { - // no change specified, generate new internal address to use as change (max amount recipient) - maxAmountRecipientAddress, err = nextInternalAddress() - if err != nil { - return nil, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) - } - } - - var totalInputAmount int64 - inputScriptSizes := make([]int, len(tx.inputs)) - inputScripts := make([][]byte, len(tx.inputs)) - for i, input := range tx.inputs { - totalInputAmount += input.ValueIn - inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize - inputScripts[i] = input.SignatureScript - } - - var changeScriptSize int - if maxAmountRecipientAddress != "" { - changeScriptSize, err = tx.calculateChangeScriptSize(maxAmountRecipientAddress) - } else { - changeScriptSize, err = tx.calculateMultipleChangeScriptSize(tx.changeDestinations) - } - if err != nil { - return nil, err - } - - maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) - maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) - changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) - - if changeAmount < 0 { - excessSpending := 0 - changeAmount // equivalent to math.Abs() - return nil, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", - dcrutil.Amount(excessSpending).String()) - } - - if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), changeScriptSize, txrules.DefaultRelayFeePerKb) { - if changeScriptSize > txscript.MaxScriptElementSize { - return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") - } - - if maxAmountRecipientAddress != "" { - singleChangeDestination := TransactionDestination{ - Address: maxAmountRecipientAddress, - AtomAmount: changeAmount, - } - tx.changeDestinations = []TransactionDestination{singleChangeDestination} - } - - var totalChangeAmount int64 - for _, changeDestination := range tx.changeDestinations { - changeOutput, err := txhelper.MakeTxOutput(changeDestination.Address, changeDestination.AtomAmount, tx.sourceWallet.chainParams) - if err != nil { - return nil, fmt.Errorf("change address error: %v", err) - } - - totalChangeAmount += changeOutput.Value - outputs = append(outputs, changeOutput) - - // randomize the change output that was just added - changeOutputIndex := len(outputs) - 1 - txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) - } - - if totalChangeAmount > changeAmount { - return nil, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ - " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), - dcrutil.Amount(changeAmount).String()) - } - } - // else { - // maxSignedSize = txsizes.EstimateSerializeSize(inputScriptSizes, outputs, 0) - // } - - return &txauthor.AuthoredTx{ - Tx: &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: wire.TxVersion, - TxIn: tx.inputs, - TxOut: outputs, - LockTime: 0, - Expiry: 0, - }}, nil - // return &wire.MsgTx{ - // SerType: wire.TxSerializeFull, - // Version: wire.TxVersion, - // TxIn: tx.inputs, - // TxOut: outputs, - // LockTime: 0, - // Expiry: 0, - // }, nil -} diff --git a/unsignedtx.go b/unsignedtx.go deleted file mode 100644 index cece2b10..00000000 --- a/unsignedtx.go +++ /dev/null @@ -1,71 +0,0 @@ -package dcrlibwallet - -import ( - "fmt" - - "github.com/decred/dcrd/dcrutil/v2" - "github.com/decred/dcrd/wire" - "github.com/decred/dcrwallet/errors/v2" - "github.com/planetdecred/dcrlibwallet/txhelper" -) - -type NextAddressFunc func() (address string, err error) - -func (tx *TxAuthor) calculateChangeScriptSize(changeAddress string) (int, error) { - changeSource, err := txhelper.MakeTxChangeSource(changeAddress, tx.sourceWallet.chainParams) - if err != nil { - return 0, fmt.Errorf("change address error: %v", err) - } - return changeSource.ScriptSize(), nil -} - -func (tx *TxAuthor) calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination) (int, error) { - var totalChangeScriptSize int - for _, changeDestination := range changeDestinations { - changeScriptSize, err := tx.calculateChangeScriptSize(changeDestination.Address) - if err != nil { - return 0, err - } - totalChangeScriptSize += changeScriptSize - } - return totalChangeScriptSize, nil -} - -// ParseOutputsAndChangeDestination generates and returns TxOuts -// using the provided slice of transaction destinations. -// Any destination set to receive max amount is not included in the TxOuts returned, -// but is instead returned as a change destination. -// Returns an error if more than 1 max amount recipients identified or -// if any other error is encountered while processing the addresses and amounts. -func (tx *TxAuthor) ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ([]*wire.TxOut, int64, string, error) { - var outputs = make([]*wire.TxOut, 0) - var totalSendAmount int64 - var maxAmountRecipientAddress string - - for _, destination := range txDestinations { - // validate the amount to send to this destination address - if !destination.SendMax && (destination.AtomAmount <= 0 || destination.AtomAmount > dcrutil.MaxAmount) { - return nil, 0, "", errors.E(errors.Invalid, "invalid amount") - } - - // check if multiple destinations are set to receive max amount - if destination.SendMax && maxAmountRecipientAddress != "" { - return nil, 0, "", fmt.Errorf("cannot send max amount to multiple recipients") - } - - if destination.SendMax { - maxAmountRecipientAddress = destination.Address - continue // do not prepare a tx output for this destination - } - - output, err := txhelper.MakeTxOutput(destination.Address, destination.AtomAmount, tx.sourceWallet.chainParams) - if err != nil { - return nil, 0, "", fmt.Errorf("make tx output error: %v", err) - } - - totalSendAmount += output.Value - outputs = append(outputs, output) - } - - return outputs, totalSendAmount, maxAmountRecipientAddress, nil -} diff --git a/utxo.go b/utxo.go new file mode 100644 index 00000000..b5a315d5 --- /dev/null +++ b/utxo.go @@ -0,0 +1,205 @@ +package dcrlibwallet + +import ( + "fmt" + + "github.com/decred/dcrd/dcrutil/v2" + "github.com/decred/dcrd/txscript" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/errors/v2" + "github.com/decred/dcrwallet/wallet/v3/txauthor" + "github.com/decred/dcrwallet/wallet/v3/txrules" + "github.com/decred/dcrwallet/wallet/v3/txsizes" + "github.com/planetdecred/dcrlibwallet/txhelper" +) + +type NextAddressFunc func() (address string, err error) + +func (tx *TxAuthor) calculateChangeScriptSize(changeAddress string) (int, error) { + changeSource, err := txhelper.MakeTxChangeSource(changeAddress, tx.sourceWallet.chainParams) + if err != nil { + return 0, fmt.Errorf("change address error: %v", err) + } + return changeSource.ScriptSize(), nil +} + +func (tx *TxAuthor) calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination) (int, error) { + var totalChangeScriptSize int + for _, changeDestination := range changeDestinations { + changeScriptSize, err := tx.calculateChangeScriptSize(changeDestination.Address) + if err != nil { + return 0, err + } + totalChangeScriptSize += changeScriptSize + } + return totalChangeScriptSize, nil +} + +// ParseOutputsAndChangeDestination generates and returns TxOuts +// using the provided slice of transaction destinations. +// Any destination set to receive max amount is not included in the TxOuts returned, +// but is instead returned as a change destination. +// Returns an error if more than 1 max amount recipients identified or +// if any other error is encountered while processing the addresses and amounts. +func (tx *TxAuthor) ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ([]*wire.TxOut, int64, string, error) { + var outputs = make([]*wire.TxOut, 0) + var totalSendAmount int64 + var maxAmountRecipientAddress string + + for _, destination := range txDestinations { + // validate the amount to send to this destination address + if !destination.SendMax && (destination.AtomAmount <= 0 || destination.AtomAmount > dcrutil.MaxAmount) { + return nil, 0, "", errors.E(errors.Invalid, "invalid amount") + } + + // check if multiple destinations are set to receive max amount + if destination.SendMax && maxAmountRecipientAddress != "" { + return nil, 0, "", fmt.Errorf("cannot send max amount to multiple recipients") + } + + if destination.SendMax { + maxAmountRecipientAddress = destination.Address + continue // do not prepare a tx output for this destination + } + + output, err := txhelper.MakeTxOutput(destination.Address, destination.AtomAmount, tx.sourceWallet.chainParams) + if err != nil { + return nil, 0, "", fmt.Errorf("make tx output error: %v", err) + } + + totalSendAmount += output.Value + outputs = append(outputs, output) + } + + return outputs, totalSendAmount, maxAmountRecipientAddress, nil +} + +func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { + // Used to generate an internal address for change, + // if no change destination is provided and + // no recipient is set to receive max amount. + nextInternalAddress := func() (string, error) { + ctx := tx.sourceWallet.shutdownContext() + addr, err := tx.sourceWallet.internal.NewChangeAddress(ctx, tx.sourceAccountNumber) + if err != nil { + return "", err + } + return addr.Address(), nil + } + var totalInputAmount int64 + + msgTx, maxSignedSize, err := func(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, + nextInternalAddress NextAddressFunc) (*wire.MsgTx, int, error) { + outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(sendDestinations) + if err != nil { + return nil, 0, err + } + + if maxAmountRecipientAddress != "" && len(changeDestinations) > 0 { + return nil, 0, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ + " change destinations must not be provided") + } + + if maxAmountRecipientAddress == "" && len(changeDestinations) == 0 { + // no change specified, generate new internal address to use as change (max amount recipient) + maxAmountRecipientAddress, err = nextInternalAddress() + if err != nil { + return nil, 0, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) + } + } + + inputScriptSizes := make([]int, len(inputs)) + inputScripts := make([][]byte, len(inputs)) + for i, input := range inputs { + totalInputAmount += input.ValueIn + inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize + inputScripts[i] = input.SignatureScript + } + + log.Info("________________________________") + log.Info("[totalInputAmount]", dcrutil.Amount(totalInputAmount).String()) + log.Info("[totalSendAmount]", dcrutil.Amount(totalSendAmount).String()) + + var changeScriptSize int + if maxAmountRecipientAddress != "" { + changeScriptSize, err = tx.calculateChangeScriptSize(maxAmountRecipientAddress) + } else { + changeScriptSize, err = tx.calculateMultipleChangeScriptSize(changeDestinations) + } + if err != nil { + return nil, 0, err + } + + maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) + maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) + changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) + + log.Info("[changeAmount]", dcrutil.Amount(changeAmount).String()) + if changeAmount < 0 { + excessSpending := 0 - changeAmount // equivalent to math.Abs() + return nil, 0, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", + dcrutil.Amount(excessSpending).String()) + } + + if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), + changeScriptSize, txrules.DefaultRelayFeePerKb) { + if changeScriptSize > txscript.MaxScriptElementSize { + return nil, 0, fmt.Errorf("script size exceed maximum bytes pushable to the stack") + } + + if maxAmountRecipientAddress != "" { + singleChangeDestination := TransactionDestination{ + Address: maxAmountRecipientAddress, + AtomAmount: changeAmount, + } + changeDestinations = []TransactionDestination{singleChangeDestination} + } + + var totalChangeAmount int64 + for _, changeDestination := range changeDestinations { + log.Info("[maxAmountRecipientAddress]", changeDestination.Address, changeDestination.AtomAmount) + changeOutput, err := txhelper.MakeTxOutput(changeDestination.Address, + changeDestination.AtomAmount, tx.sourceWallet.chainParams) + if err != nil { + return nil, 0, fmt.Errorf("change address error: %v", err) + } + + log.Info("[changeOutput.Value]", changeOutput.Value) + totalChangeAmount += changeOutput.Value + outputs = append(outputs, changeOutput) + + // randomize the change output that was just added + changeOutputIndex := len(outputs) - 1 + txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) + } + + log.Info("[totalChangeAmount]", dcrutil.Amount(totalChangeAmount).String()) + + if totalChangeAmount > changeAmount { + return nil, 0, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ + " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), + dcrutil.Amount(changeAmount).String()) + } + } else { + maxSignedSize = txsizes.EstimateSerializeSize(inputScriptSizes, outputs, 0) + } + + return &wire.MsgTx{ + SerType: wire.TxSerializeFull, + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + Expiry: 0, + }, maxSignedSize, nil + }(tx.inputs, tx.destinations, tx.changeDestinations, nextInternalAddress) + + if err != nil { + return nil, err + } + + return &txauthor.AuthoredTx{ + TotalInput: dcrutil.Amount(totalInputAmount), + EstimatedSignedSerializeSize: maxSignedSize, Tx: msgTx, + }, nil +} From 6313fddec18433b7f0051316f2b1fa65b0c19935 Mon Sep 17 00:00:00 2001 From: song50119 Date: Thu, 22 Oct 2020 20:01:00 +0700 Subject: [PATCH 09/15] Update txauthor * Adds (*TxAuthor).AddChangeDestination * Adds (*TxAuthor).UpdateChangeDestination * Adds (*TxAuthor).RemoveChangeDestination --- txauthor.go | 20 ++++ txhelper/internal/txsizes/size.go | 188 ------------------------------ utxo.go | 23 ++-- 3 files changed, 28 insertions(+), 203 deletions(-) delete mode 100644 txhelper/internal/txsizes/size.go diff --git a/txauthor.go b/txauthor.go index 00971cc7..ab4f86c0 100644 --- a/txauthor.go +++ b/txauthor.go @@ -67,6 +67,26 @@ func (tx *TxAuthor) SendDestination(atIndex int) *TransactionDestination { return &tx.destinations[atIndex] } +func (tx *TxAuthor) AddChangeDestination(address string, atomAmount int64) { + tx.changeDestinations = append(tx.changeDestinations, TransactionDestination{ + Address: address, + AtomAmount: atomAmount, + }) +} + +func (tx *TxAuthor) UpdateChangeDestination(index int, address string, atomAmount int64) { + tx.changeDestinations[index] = TransactionDestination{ + Address: address, + AtomAmount: atomAmount, + } +} + +func (tx *TxAuthor) RemoveChangeDestination(index int) { + if len(tx.changeDestinations) > index { + tx.changeDestinations = append(tx.changeDestinations[:index], tx.changeDestinations[index+1:]...) + } +} + func (tx *TxAuthor) TotalSendAmount() *Amount { var totalSendAmountAtom int64 = 0 for _, destination := range tx.destinations { diff --git a/txhelper/internal/txsizes/size.go b/txhelper/internal/txsizes/size.go deleted file mode 100644 index 1ac4a187..00000000 --- a/txhelper/internal/txsizes/size.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 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. - -// from "github.com/decred/dcrwallet/wallet/internal/txsizes" - -package txsizes - -import "github.com/decred/dcrd/wire" - -// Worst case script and input/output size estimates. -const ( - // RedeemP2PKSigScriptSize is the worst case (largest) serialize size - // of a transaction input script that redeems a compressed P2PK output. - // It is calculated as: - // - // - OP_DATA_73 - // - 72 bytes DER signature + 1 byte sighash - RedeemP2PKSigScriptSize = 1 + 73 - - // RedeemP2PKHSigScriptSize is the worst case (largest) serialize size - // of a transaction input script that redeems a compressed P2PKH output. - // It is calculated as: - // - // - OP_DATA_73 - // - 72 bytes DER signature + 1 byte sighash - // - OP_DATA_33 - // - 33 bytes serialized compressed pubkey - RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 - - // RedeemP2SHSigScriptSize is the worst case (largest) serialize size - // of a transaction input script that redeems a P2SH output. - // It is calculated as: - // - // - OP_DATA_73 - // - 73-byte signature - // - OP_DATA_35 - // - OP_DATA_33 - // - 33 bytes serialized compressed pubkey - // - OP_CHECKSIG - RedeemP2SHSigScriptSize = 1 + 73 + 1 + 1 + 33 + 1 - - // RedeemP2PKHInputSize is the worst case (largest) serialize size of a - // transaction input redeeming a compressed P2PKH output. It is - // calculated as: - // - // - 32 bytes previous tx - // - 4 bytes output index - // - 1 byte tree - // - 8 bytes amount - // - 4 bytes block height - // - 4 bytes block index - // - 1 byte compact int encoding value 107 - // - 107 bytes signature script - // - 4 bytes sequence - RedeemP2PKHInputSize = 32 + 4 + 1 + 8 + 4 + 4 + 1 + RedeemP2PKHSigScriptSize + 4 - - // P2PKHPkScriptSize is the size of a transaction output script that - // pays to a compressed pubkey hash. It is calculated as: - // - // - OP_DUP - // - OP_HASH160 - // - OP_DATA_20 - // - 20 bytes pubkey hash - // - OP_EQUALVERIFY - // - OP_CHECKSIG - P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 - - // P2SHPkScriptSize is the size of a transaction output script that - // pays to a script hash. It is calculated as: - // - // - OP_HASH160 - // - OP_DATA_20 - // - 20 bytes script hash - // - OP_EQUAL - P2SHPkScriptSize = 1 + 1 + 20 + 1 - - // TicketCommitmentScriptSize is the size of a ticket purchase commitment - // script. It is calculated as: - // - // - OP_RETURN - // - OP_DATA_30 - // - 20 bytes P2SH/P2PKH - // - 8 byte amount - // - 2 byte fee range limits - TicketCommitmentScriptSize = 1 + 1 + 20 + 8 + 2 - - // P2PKHOutputSize is the serialize size of a transaction output with a - // P2PKH output script. It is calculated as: - // - // - 8 bytes output value - // - 2 bytes version - // - 1 byte compact int encoding value 25 - // - 25 bytes P2PKH output script - P2PKHOutputSize = 8 + 2 + 1 + 25 -) - -func sumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { - for _, txOut := range outputs { - serializeSize += txOut.SerializeSize() - } - return serializeSize -} - -// EstimateSerializeSize returns a worst case serialize size estimate for a -// signed transaction that spends a number of outputs and contains each -// transaction output from txOuts. The estimated size is incremented for an -// additional change output if changeScriptSize is greater than 0. Passing 0 -// does not add a change output. -func EstimateSerializeSize(scriptSizes []int, txOuts []*wire.TxOut, changeScriptSize int) int { - // Generate and sum up the estimated sizes of the inputs. - txInsSize := 0 - for _, size := range scriptSizes { - txInsSize += EstimateInputSize(size) - } - - inputCount := len(scriptSizes) - outputCount := len(txOuts) - changeSize := 0 - if changeScriptSize > 0 { - changeSize = EstimateOutputSize(changeScriptSize) - outputCount++ - } - - // 12 additional bytes are for version, locktime and expiry. - return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + - wire.VarIntSerializeSize(uint64(outputCount)) + - txInsSize + - sumOutputSerializeSizes(txOuts) + - changeSize -} - -// EstimateSerializeSizeFromScriptSizes returns a worst case serialize size -// estimate for a signed transaction that spends len(inputSizes) previous -// outputs and pays to len(outputSizes) outputs with scripts of the provided -// worst-case sizes. The estimated size is incremented for an additional -// change output if changeScriptSize is greater than 0. Passing 0 does not -// add a change output. -func EstimateSerializeSizeFromScriptSizes(inputSizes []int, outputSizes []int, changeScriptSize int) int { - // Generate and sum up the estimated sizes of the inputs. - txInsSize := 0 - for _, inputSize := range inputSizes { - txInsSize += EstimateInputSize(inputSize) - } - - // Generate and sum up the estimated sizes of the outputs. - txOutsSize := 0 - for _, outputSize := range outputSizes { - txOutsSize += EstimateOutputSize(outputSize) - } - - inputCount := len(inputSizes) - outputCount := len(outputSizes) - changeSize := 0 - if changeScriptSize > 0 { - changeSize = EstimateOutputSize(changeScriptSize) - outputCount++ - } - - // 12 additional bytes are for version, locktime and expiry. - return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + - wire.VarIntSerializeSize(uint64(outputCount)) + - txInsSize + txOutsSize + changeSize -} - -// EstimateInputSize returns the worst case serialize size estimate for a tx input -// - 32 bytes previous tx -// - 4 bytes output index -// - 1 byte tree -// - 8 bytes amount -// - 4 bytes block height -// - 4 bytes block index -// - the compact int representation of the script size -// - the supplied script size -// - 4 bytes sequence -func EstimateInputSize(scriptSize int) int { - return 32 + 4 + 1 + 8 + 4 + 4 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize + 4 -} - -// EstimateOutputSize returns the worst case serialize size estimate for a tx output -// - 8 bytes amount -// - 2 bytes version -// - the compact int representation of the script size -// - the supplied script size -func EstimateOutputSize(scriptSize int) int { - return 8 + 2 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize -} diff --git a/utxo.go b/utxo.go index b5a315d5..8243c86a 100644 --- a/utxo.go +++ b/utxo.go @@ -3,6 +3,7 @@ package dcrlibwallet import ( "fmt" + "github.com/decred/dcrd/chaincfg/v2" "github.com/decred/dcrd/dcrutil/v2" "github.com/decred/dcrd/txscript" "github.com/decred/dcrd/wire" @@ -15,18 +16,19 @@ import ( type NextAddressFunc func() (address string, err error) -func (tx *TxAuthor) calculateChangeScriptSize(changeAddress string) (int, error) { - changeSource, err := txhelper.MakeTxChangeSource(changeAddress, tx.sourceWallet.chainParams) +func calculateChangeScriptSize(changeAddress string, chainParams *chaincfg.Params) (int, error) { + changeSource, err := txhelper.MakeTxChangeSource(changeAddress, chainParams) if err != nil { return 0, fmt.Errorf("change address error: %v", err) } return changeSource.ScriptSize(), nil } -func (tx *TxAuthor) calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination) (int, error) { +func calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination, + chainParams *chaincfg.Params) (int, error) { var totalChangeScriptSize int for _, changeDestination := range changeDestinations { - changeScriptSize, err := tx.calculateChangeScriptSize(changeDestination.Address) + changeScriptSize, err := calculateChangeScriptSize(changeDestination.Address, chainParams) if err != nil { return 0, err } @@ -116,15 +118,11 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { inputScripts[i] = input.SignatureScript } - log.Info("________________________________") - log.Info("[totalInputAmount]", dcrutil.Amount(totalInputAmount).String()) - log.Info("[totalSendAmount]", dcrutil.Amount(totalSendAmount).String()) - var changeScriptSize int if maxAmountRecipientAddress != "" { - changeScriptSize, err = tx.calculateChangeScriptSize(maxAmountRecipientAddress) + changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress, tx.sourceWallet.chainParams) } else { - changeScriptSize, err = tx.calculateMultipleChangeScriptSize(changeDestinations) + changeScriptSize, err = calculateMultipleChangeScriptSize(changeDestinations, tx.sourceWallet.chainParams) } if err != nil { return nil, 0, err @@ -134,7 +132,6 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) - log.Info("[changeAmount]", dcrutil.Amount(changeAmount).String()) if changeAmount < 0 { excessSpending := 0 - changeAmount // equivalent to math.Abs() return nil, 0, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", @@ -157,14 +154,12 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { var totalChangeAmount int64 for _, changeDestination := range changeDestinations { - log.Info("[maxAmountRecipientAddress]", changeDestination.Address, changeDestination.AtomAmount) changeOutput, err := txhelper.MakeTxOutput(changeDestination.Address, changeDestination.AtomAmount, tx.sourceWallet.chainParams) if err != nil { return nil, 0, fmt.Errorf("change address error: %v", err) } - log.Info("[changeOutput.Value]", changeOutput.Value) totalChangeAmount += changeOutput.Value outputs = append(outputs, changeOutput) @@ -173,8 +168,6 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) } - log.Info("[totalChangeAmount]", dcrutil.Amount(totalChangeAmount).String()) - if totalChangeAmount > changeAmount { return nil, 0, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), From 6d8efc387f551a8d2dc543f7d724127338d0c50a Mon Sep 17 00:00:00 2001 From: song50119 Date: Fri, 23 Oct 2020 23:35:11 +0700 Subject: [PATCH 10/15] fix lint errors --- txauthor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/txauthor.go b/txauthor.go index ab4f86c0..bea02ca8 100644 --- a/txauthor.go +++ b/txauthor.go @@ -168,7 +168,7 @@ func (tx *TxAuthor) UseInputs(utxoKeys []string) error { txHash, _ := chainhash.NewHash(utxo.TransactionHash) outpoint := wire.NewOutPoint(txHash, utxo.OutputIndex, int8(utxo.Tree)) - input := wire.NewTxIn(outpoint, int64(utxo.Amount), nil) + input := wire.NewTxIn(outpoint, utxo.Amount, nil) inputs = append(inputs, input) } From f3785c80f2f344a10f07715c3bac9ac4ede37712 Mon Sep 17 00:00:00 2001 From: song50119 Date: Sun, 25 Oct 2020 15:42:29 +0700 Subject: [PATCH 11/15] Update go.mod --- go.mod | 3 --- go.sum | 25 ------------------------- 2 files changed, 28 deletions(-) diff --git a/go.mod b/go.mod index 263749f8..d514229b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/decred/dcrd/chaincfg/v2 v2.3.0 github.com/decred/dcrd/connmgr/v2 v2.0.0 github.com/decred/dcrd/dcrec v1.0.0 - github.com/decred/dcrd/dcrutil v1.4.0 github.com/decred/dcrd/dcrutil/v2 v2.0.1 github.com/decred/dcrd/hdkeychain/v2 v2.1.0 github.com/decred/dcrd/rpcclient/v2 v2.1.0 // indirect @@ -22,11 +21,9 @@ require ( github.com/decred/dcrdata/txhelpers v1.1.0 github.com/decred/dcrwallet/errors v1.1.0 github.com/decred/dcrwallet/errors/v2 v2.0.0 - github.com/decred/dcrwallet/internal/helpers v1.0.1 github.com/decred/dcrwallet/p2p/v2 v2.0.0 github.com/decred/dcrwallet/rpc/client/dcrd v1.0.0 github.com/decred/dcrwallet/ticketbuyer/v4 v4.0.0 - github.com/decred/dcrwallet/wallet v1.3.0 github.com/decred/dcrwallet/wallet/v3 v3.2.1-badger github.com/decred/dcrwallet/walletseed v1.0.1 github.com/decred/slog v1.0.0 diff --git a/go.sum b/go.sum index bcae0fd1..99ed07b6 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,6 @@ github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7I github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/asdine/storm v0.0.0-20190216191021-fe89819f6282 h1:DmSVc81daQAPvXwcCZi0W6A14sTCYQ1QI21C0E37KoY= github.com/asdine/storm v0.0.0-20190216191021-fe89819f6282/go.mod h1:cMLKpjHSP4q0P133fV15ojQgwWWB2IMv+hrFsmBF/wI= -github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= @@ -36,7 +35,6 @@ github.com/decred/dcrd/addrmgr v1.0.2 h1:BfJoFEkdDDhaQSsx9NkVOTiOTUbEevbVf+aYRQS github.com/decred/dcrd/addrmgr v1.0.2/go.mod h1:gNnmTuf/Xkg8ZX3j5GXbajzPrSdf5bA7HitO2bjmq0Q= github.com/decred/dcrd/addrmgr v1.1.0 h1:VQkn1qmafZypfN2u7yi7J/girwz4ZDicquo7JzsoxdQ= github.com/decred/dcrd/addrmgr v1.1.0/go.mod h1:exghL+0+QeVvO4MXezWJ1C2tcpBn3ngfuP6S1R+adB8= -github.com/decred/dcrd/blockchain v1.0.1/go.mod h1:R/4XnwNOTj5IP8jQIUzrJ8zhr/7EOk09IMODwBamZoI= github.com/decred/dcrd/blockchain v1.1.1 h1:CWr90sZ2YLQz84EGT+X/pzU+9AZB1eXQUy+4fsJSt5w= github.com/decred/dcrd/blockchain v1.1.1/go.mod h1:zxi/41LgzHitpz/CZu0gxHyFHz8+ysd3lH8E3P5Uifg= github.com/decred/dcrd/blockchain/stake v1.0.1/go.mod h1:hgoGmWMIu2LLApBbcguVpzCEEfX7M2YhuMrQdpohJzc= @@ -87,7 +85,6 @@ github.com/decred/dcrd/dcrec v0.0.0-20180721005212-59fe2b293f69/go.mod h1:cRAH1S github.com/decred/dcrd/dcrec v0.0.0-20180721005914-d26200ec716b/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v0.0.0-20180721031028-5369a485acf6/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v0.0.0-20180801202239-0761de129164/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= -github.com/decred/dcrd/dcrec v0.0.0-20181212181811-1a370d38d671/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v0.0.0-20190130161649-59ed4247a1d5/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v0.0.0-20190402182842-879eebce3333/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= @@ -111,9 +108,6 @@ github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 h1:awk7sYJ4pGWmtkiGHFfctztJjHMKGLV github.com/decred/dcrd/dcrec/secp256k1 v1.0.2/go.mod h1:CHTUIVfmDDd0KFVFpNX1pFVCBUegxW387nN0IGwNKR0= github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= -github.com/decred/dcrd/dcrjson v1.0.0/go.mod h1:ozddIaeF+EAvZZvFuB3zpfxhyxBGfvbt22crQh+PYuI= -github.com/decred/dcrd/dcrjson v1.1.0 h1:pFpbay3cWACkgloFxWjHBwlXWG2+S2QCJJzNxL40hwg= -github.com/decred/dcrd/dcrjson v1.1.0/go.mod h1:ozddIaeF+EAvZZvFuB3zpfxhyxBGfvbt22crQh+PYuI= github.com/decred/dcrd/dcrjson/v2 v2.0.0 h1:W0q4Alh36c5N318eUpfmU8kXoCNgImMLI87NIXni9Us= github.com/decred/dcrd/dcrjson/v2 v2.0.0/go.mod h1:FYueNy8BREAFq04YNEwcTsmGFcNqY+ehUUO81w2igi4= github.com/decred/dcrd/dcrjson/v2 v2.2.0 h1:u0ON3IZ8/fqoA624HPNBsWYjIgBtC82DGMtq35bthhI= @@ -145,16 +139,10 @@ github.com/decred/dcrd/hdkeychain/v2 v2.0.0 h1:b6GklXT+LeDumc0bDqMHkss+p2Bu+mgiU github.com/decred/dcrd/hdkeychain/v2 v2.0.0/go.mod h1:tG+VpXfloIkNGHGd6NeoTElHWA68Wf1aP87zegXDGEw= github.com/decred/dcrd/hdkeychain/v2 v2.1.0 h1:NVNIz36HPukOnaysBDsLO+2kWqijLM4tvLUsLLyLfME= github.com/decred/dcrd/hdkeychain/v2 v2.1.0/go.mod h1:DR+lD4uV8G0i3c9qnUJwjiGaaEWK+nSrbWCz1BRHBL8= -github.com/decred/dcrd/mempool v1.1.1 h1:ysFIS3HzEIJ88B1Y4OfL6wjzBurlChbKkzq54hPglGo= -github.com/decred/dcrd/mempool v1.1.1/go.mod h1:u1I2KRv9UHhx2crlbZXYoLDabWyQ8VnnHDSG53UdhCA= -github.com/decred/dcrd/mining v1.1.0 h1:9Wtla+i+pEjfYsNCfixsipmyyoB26DgL4LSXWAin/zw= -github.com/decred/dcrd/mining v1.1.0/go.mod h1:NQEtX604XgNwKcPFId1hVTTiBqmVQDlnqV1yNqGl4oU= github.com/decred/dcrd/rpc/jsonrpc/types v1.0.0 h1:d5ptnjuSADTQMa3i83VpeJNoMRTOJZZBqk7P+E41VXM= github.com/decred/dcrd/rpc/jsonrpc/types v1.0.0/go.mod h1:0dwmpIP21tJxjg/UuUHWIFMbfoLv2ifCBMokNKlOxpo= github.com/decred/dcrd/rpc/jsonrpc/types v1.0.1 h1:sWsGtWzdmrna6aysDCHwjANTJh+Lxt2xp6S10ahP79Y= github.com/decred/dcrd/rpc/jsonrpc/types v1.0.1/go.mod h1:dJUp9PoyFYklzmlImpVkVLOr6j4zKuUv66YgemP2sd8= -github.com/decred/dcrd/rpcclient v1.1.0 h1:nQZ1qOJaLYoOTM1oQ2dLaqocb5TWI7gNBK+BTY7UVXk= -github.com/decred/dcrd/rpcclient v1.1.0/go.mod h1:SCwBs4d+aqRV2ChnriIZ1y/LgNVHG/2ieEC1vIop82s= github.com/decred/dcrd/rpcclient/v2 v2.0.0/go.mod h1:9XjbRHBSNqN+DXz8I47gUZszvVjvugqLGK8TZQ4c/u0= github.com/decred/dcrd/rpcclient/v2 v2.1.0 h1:oaHR2ZIe6TyINbLgmrRkS8xddLr5gDmkX6BkWwSFy6Q= github.com/decred/dcrd/rpcclient/v2 v2.1.0/go.mod h1:tVaa1C6a0Cqdcpi393nm2AQZeZdxKwrCfToashLIv8w= @@ -179,9 +167,6 @@ github.com/decred/dcrdata/semver v1.0.0 h1:DBqYU/x+4LqHq/3r4xKdF6xG5ewktG2KDC+g/ github.com/decred/dcrdata/semver v1.0.0/go.mod h1:z+nQqiAd9fYkHhBLbejysZ2FPHtgkrErWDgMf+JlZWE= github.com/decred/dcrdata/txhelpers v1.1.0 h1:xKK+1/OASkSxMe/KDLaLJeJjJdopgC+FV8+EFPpJAGk= github.com/decred/dcrdata/txhelpers v1.1.0/go.mod h1:IyxuqUxIPrHnOmRy9DCl90oqw7FxYHde/fkIkF21B7w= -github.com/decred/dcrwallet v1.5.1 h1:+XBpgSfP8fC3RK5NBGdwIA238HxaqnCDfjTjdCrQljE= -github.com/decred/dcrwallet/deployments v1.1.0 h1:83arg+7ct7PS1H2IYhuePnrBK2rkVpEYWKqrJpCwtHA= -github.com/decred/dcrwallet/deployments v1.1.0/go.mod h1:8Sasryu8SX23Jvqr6maZ7MoS7wFIGXupWzbsVtcZsUg= github.com/decred/dcrwallet/deployments/v2 v2.0.0 h1:sSjkc87hcDFGoLMTIwNt5ze+rCHbholqyM8Z3H9k5CE= github.com/decred/dcrwallet/deployments/v2 v2.0.0/go.mod h1:fY1HV1vIeeY5bHjrMknUhB/ZOVIfthBiUlSgRqFFKrg= github.com/decred/dcrwallet/errors v1.0.0 h1:XjSILZ2mK5HqWYlhdBpsm+CimFDqDB+hY3tuX0Yh0Jo= @@ -192,10 +177,6 @@ github.com/decred/dcrwallet/errors v1.1.0 h1:xDzE4l8AGLcL1CGigPR9vYHP/rBmMm34Zat github.com/decred/dcrwallet/errors v1.1.0/go.mod h1:XUm95dWmm9XmQGvneBXJkkIaFeRsQVBB6ni/KTy1hrY= github.com/decred/dcrwallet/errors/v2 v2.0.0 h1:b3QHoQNjKkrcO0GSpueeHvFKp5eqtRv9aw649MDyejA= github.com/decred/dcrwallet/errors/v2 v2.0.0/go.mod h1:2HYvtRuCE9XqDNCWhKmBuzLG364xUgcUIsJu02r0F5Q= -github.com/decred/dcrwallet/internal/helpers v1.0.1 h1:oEqfJPs1uI7QI0Aejx2MUWq/yD609cc9Z8P7vQ6cDz8= -github.com/decred/dcrwallet/internal/helpers v1.0.1/go.mod h1:qIXcze8VZ+A3sEgZou7PTOe4Vsnmks54SGTSGZ6084g= -github.com/decred/dcrwallet/internal/zero v1.0.1 h1:hO7orPk13AFp7pFTL739CbVLKImSNorI2J9/tiucNQY= -github.com/decred/dcrwallet/internal/zero v1.0.1/go.mod h1:mXUIsKATE1pIaNAJQ4lhSTX6c9N5sYoSrlScgRCaMJs= github.com/decred/dcrwallet/lru v1.0.0 h1:vz71/Wa2890CUQeWsOTI6u6iGGfXGAhIQ/hnqMUh6Xc= github.com/decred/dcrwallet/lru v1.0.0/go.mod h1:jEty7mdT5VaaV06DEV2Avv0R3HpGvUwvDW4lw8ECtiY= github.com/decred/dcrwallet/p2p/v2 v2.0.0 h1:YFnzIhJITUmFcTU1PzuJ0Wenz/1s8ijDtu0LtpIo4z4= @@ -212,13 +193,10 @@ github.com/decred/dcrwallet/rpc/jsonrpc/types v1.3.0 h1:yCxtFqK7X6GvZWQzHXjCwoGC github.com/decred/dcrwallet/rpc/jsonrpc/types v1.3.0/go.mod h1:Xvekb43GtfMiRbyIY4ZJ9Uhd9HRIAcnp46f3q2eIExU= github.com/decred/dcrwallet/ticketbuyer/v4 v4.0.0 h1:pVlYm2yWYZxc6OGjvAq9jBHZ1gimzBNiLQiHccr/nf0= github.com/decred/dcrwallet/ticketbuyer/v4 v4.0.0/go.mod h1:5UXlcyVPthPem5PY5mrbLM9tPBYAIm+wLnzvHlZHqms= -github.com/decred/dcrwallet/validate v1.0.2/go.mod h1:1ur2sRZkQ23ECalUKdwgx6rdIiP8rIiaSQAz1Y9LQsI= github.com/decred/dcrwallet/validate v1.1.1 h1:hoHrHaJTQoANN/ZW37HbeTQSJ+N4rMFFLz6LT/FACJQ= github.com/decred/dcrwallet/validate v1.1.1/go.mod h1:T++tlVcCOh2oSrEq4r5CKCvmftaQdq9uZwO7jSNYZaw= github.com/decred/dcrwallet/version v1.0.1 h1:gAz1lDkcJ+oAbg0tOn/J0KwZBVWIlhWmHhSUi9GbB2E= github.com/decred/dcrwallet/version v1.0.1/go.mod h1:rXeMsUaI03WtlQrSol7Q7sJ8HBOB+tZvT7YQRXD5Y7M= -github.com/decred/dcrwallet/wallet v1.3.0 h1:RQ4uHIfbDF0ppyeVDkTr7GapeABCddUUKOSexTN7X1M= -github.com/decred/dcrwallet/wallet v1.3.0/go.mod h1:ItOhnw3C4znuLQVWACSq8jCLy221v9X0Xo0b/j5WqgU= github.com/decred/dcrwallet/walletseed v1.0.1 h1:gxvlj0GRw+H0VumCxTlEysu+/nltcp9+lgzVgzsnI/Y= github.com/decred/dcrwallet/walletseed v1.0.1/go.mod h1:ENlwTabC2JVmT4S1eCP44fnwX4+9y2RLsnfSU21CJ+4= github.com/decred/go-socks v1.0.0 h1:gd5+vE9LmL+FYiN5cxAMtaAVb791mG8E/+Jo411M/BE= @@ -241,7 +219,6 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= @@ -278,7 +255,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/raedahgroup/dcrlibwallet v1.5.2 h1:N9/eTiNBienyIzMTP+uEZhdqaXLbfVzwLNJQNFWQEq8= github.com/raedahgroup/dcrwallet/wallet/v3 v3.2.1-badger h1:VeMaBDPPCLtt/gT7irdR+v26LhC7m6LWdiGGJJBEVIE= github.com/raedahgroup/dcrwallet/wallet/v3 v3.2.1-badger/go.mod h1:SJ+++gtMdcUeqMv6iIO3gVGlGJfM+4iY2QSaAakhbUw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -293,7 +269,6 @@ go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180718160520-a2144134853f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 39ec79372d0a9f286e559c7987f0445780e67628 Mon Sep 17 00:00:00 2001 From: song50119 Date: Wed, 28 Oct 2020 22:40:32 +0700 Subject: [PATCH 12/15] Update UTXO * Refactor (*TxAuthor).UseInputs find utxo by txn outpoint * Adds (*TxAuthor).newUnsignedTxUTXO * Change UnspentOutput.Address to UnspentOutput.Addresses --- accounts.go | 17 +++--- errors.go | 1 + txauthor.go | 64 ++++++++++++--------- types.go | 2 +- utxo.go | 158 +++++++++++++++++++++++++--------------------------- 5 files changed, 122 insertions(+), 120 deletions(-) diff --git a/accounts.go b/accounts.go index 74bde7de..39e52a32 100644 --- a/accounts.go +++ b/accounts.go @@ -10,7 +10,7 @@ import ( "github.com/decred/dcrd/chaincfg/v2" "github.com/decred/dcrd/dcrutil/v2" "github.com/decred/dcrwallet/errors/v2" - dcrwallet "github.com/decred/dcrwallet/wallet/v3" + w "github.com/decred/dcrwallet/wallet/v3" "github.com/planetdecred/dcrlibwallet/addresshelper" ) @@ -125,7 +125,7 @@ func (wallet *Wallet) SpendableForAccount(account int32) (int64, error) { } func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { - policy := dcrwallet.OutputSelectionPolicy{ + policy := w.OutputSelectionPolicy{ Account: uint32(account), RequiredConfirmations: wallet.RequiredConfirmations(), } @@ -154,15 +154,12 @@ func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { return nil, fmt.Errorf("error reading address details for unspent output: %v", err) } - previousTx, err := wallet.GetTransactionRaw(input.PreviousOutPoint.Hash[:]) - if err != nil { - return nil, fmt.Errorf("error reading tx details for unspent output: %v", err) + var confirmations int32 + inputBlockHeight := int32(input.BlockHeight) + if inputBlockHeight != -1 { + confirmations = wallet.GetBestBlock() - inputBlockHeight + 1 } - var confirmations int32 = 0 - if previousTx.BlockHeight != -1 { - confirmations = wallet.GetBestBlock() - previousTx.BlockHeight + 1 - } unspentOutputs[i] = &UnspentOutput{ TransactionHash: input.PreviousOutPoint.Hash[:], OutputIndex: input.PreviousOutPoint.Index, @@ -172,7 +169,7 @@ func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { PkScript: inputDetail.Scripts[i], ReceiveTime: outputInfo.Received.Unix(), FromCoinbase: outputInfo.FromCoinbase, - Address: strings.Join(addresses, ", "), + Addresses: strings.Join(addresses, ", "), Confirmations: confirmations, } } diff --git a/errors.go b/errors.go index 0f98a4dc..2daa21db 100644 --- a/errors.go +++ b/errors.go @@ -33,6 +33,7 @@ const ( ErrAddressDiscoveryNotDone = "address_discovery_not_done" ErrChangingPassphrase = "err_changing_passphrase" ErrSavingWallet = "err_saving_wallet" + ErrIndexOutOfRange = "err_index_out_of_range" ) // todo, should update this method to translate more error kinds. diff --git a/txauthor.go b/txauthor.go index bea02ca8..7ddb56bc 100644 --- a/txauthor.go +++ b/txauthor.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "strconv" + "strings" "time" "github.com/decred/dcrd/chaincfg/chainhash" @@ -67,24 +69,32 @@ func (tx *TxAuthor) SendDestination(atIndex int) *TransactionDestination { return &tx.destinations[atIndex] } -func (tx *TxAuthor) AddChangeDestination(address string, atomAmount int64) { +func (tx *TxAuthor) AddChangeDestination(address string) int { tx.changeDestinations = append(tx.changeDestinations, TransactionDestination{ - Address: address, - AtomAmount: atomAmount, + Address: address, }) + return len(tx.changeDestinations) - 1 } -func (tx *TxAuthor) UpdateChangeDestination(index int, address string, atomAmount int64) { +func (tx *TxAuthor) UpdateChangeDestination(index int, address string, atomAmount int64) error { + if len(tx.changeDestinations) < index { + return errors.New(ErrIndexOutOfRange) + } tx.changeDestinations[index] = TransactionDestination{ Address: address, AtomAmount: atomAmount, } + return nil } -func (tx *TxAuthor) RemoveChangeDestination(index int) { +func (tx *TxAuthor) RemoveChangeDestination(index int) error { + if len(tx.changeDestinations) < index { + return errors.New(ErrIndexOutOfRange) + } if len(tx.changeDestinations) > index { tx.changeDestinations = append(tx.changeDestinations[:index], tx.changeDestinations[index+1:]...) } + return nil } func (tx *TxAuthor) TotalSendAmount() *Amount { @@ -140,35 +150,35 @@ func (tx *TxAuthor) UseInputs(utxoKeys []string) error { // first clear any previously set inputs // so that an outdated set of inputs isn't used if an error occurs from this function tx.inputs = nil - accountUtxos, err := tx.sourceWallet.UnspentOutputs(int32(tx.sourceAccountNumber)) - if err != nil { - return fmt.Errorf("error reading unspent outputs in account: %v", err) - } + inputs := make([]*wire.TxIn, 0, len(utxoKeys)) + for _, utxoKey := range utxoKeys { + idx := strings.Index(utxoKey, ":") + hash := utxoKey[:idx] + hashIndex := utxoKey[idx+1:] + index, err := strconv.Atoi(hashIndex) + if err != nil { + return fmt.Errorf("no valid utxo found for '%s' in the source account at index %d", utxoKey, index) + } - retrieveAccountUtxo := func(utxoKey string) *UnspentOutput { - for _, accountUtxo := range accountUtxos { - if accountUtxo.OutputKey == utxoKey { - return accountUtxo - } + txHash, err := chainhash.NewHashFromStr(hash) + if err != nil { + return err } - return nil - } - inputs := make([]*wire.TxIn, 0, len(utxoKeys)) + op := &wire.OutPoint{ + Hash: *txHash, + Index: uint32(index), + } + outputInfo, err := tx.sourceWallet.internal.OutputInfo(tx.sourceWallet.shutdownContext(), op) + if err != nil { + return err + } - // retrieve utxo details for each key in the provided slice of utxoKeys - for _, utxoKey := range utxoKeys { - utxo := retrieveAccountUtxo(utxoKey) - if utxo == nil { + if err != nil { return fmt.Errorf("no valid utxo found for '%s' in the source account", utxoKey) } - // this is a reverse conversion and should not throw an error - // this []byte was originally converted from chainhash.Hash using chainhash.Hash[:] - txHash, _ := chainhash.NewHash(utxo.TransactionHash) - - outpoint := wire.NewOutPoint(txHash, utxo.OutputIndex, int8(utxo.Tree)) - input := wire.NewTxIn(outpoint, utxo.Amount, nil) + input := wire.NewTxIn(op, int64(outputInfo.Amount), nil) inputs = append(inputs, input) } diff --git a/types.go b/types.go index 8c57932c..034b6da0 100644 --- a/types.go +++ b/types.go @@ -319,6 +319,6 @@ type UnspentOutput struct { FromCoinbase bool Tree int32 PkScript []byte - Address string + Addresses string // separated by commas Confirmations int32 } diff --git a/utxo.go b/utxo.go index 8243c86a..885f101e 100644 --- a/utxo.go +++ b/utxo.go @@ -14,7 +14,7 @@ import ( "github.com/planetdecred/dcrlibwallet/txhelper" ) -type NextAddressFunc func() (address string, err error) +type nextAddressFunc func() (address string, err error) func calculateChangeScriptSize(changeAddress string, chainParams *chaincfg.Params) (int, error) { changeSource, err := txhelper.MakeTxChangeSource(changeAddress, chainParams) @@ -88,111 +88,105 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { } return addr.Address(), nil } - var totalInputAmount int64 - - msgTx, maxSignedSize, err := func(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, - nextInternalAddress NextAddressFunc) (*wire.MsgTx, int, error) { - outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(sendDestinations) - if err != nil { - return nil, 0, err - } - if maxAmountRecipientAddress != "" && len(changeDestinations) > 0 { - return nil, 0, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ - " change destinations must not be provided") - } + return tx.newUnsignedTxUTXO(tx.inputs, tx.destinations, tx.changeDestinations, nextInternalAddress) +} - if maxAmountRecipientAddress == "" && len(changeDestinations) == 0 { - // no change specified, generate new internal address to use as change (max amount recipient) - maxAmountRecipientAddress, err = nextInternalAddress() - if err != nil { - return nil, 0, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) - } - } +func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, + nextInternalAddress nextAddressFunc) (*txauthor.AuthoredTx, error) { + outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(sendDestinations) + if err != nil { + return nil, err + } - inputScriptSizes := make([]int, len(inputs)) - inputScripts := make([][]byte, len(inputs)) - for i, input := range inputs { - totalInputAmount += input.ValueIn - inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize - inputScripts[i] = input.SignatureScript - } + if maxAmountRecipientAddress != "" && len(changeDestinations) > 0 { + return nil, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ + " change destinations must not be provided") + } - var changeScriptSize int - if maxAmountRecipientAddress != "" { - changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress, tx.sourceWallet.chainParams) - } else { - changeScriptSize, err = calculateMultipleChangeScriptSize(changeDestinations, tx.sourceWallet.chainParams) - } + if maxAmountRecipientAddress == "" && len(changeDestinations) == 0 { + // no change specified, generate new internal address to use as change (max amount recipient) + maxAmountRecipientAddress, err = nextInternalAddress() if err != nil { - return nil, 0, err + return nil, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) } + } - maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) - maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) - changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) + var totalInputAmount int64 + inputScriptSizes := make([]int, len(inputs)) + inputScripts := make([][]byte, len(inputs)) + for i, input := range inputs { + totalInputAmount += input.ValueIn + inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize + inputScripts[i] = input.SignatureScript + } - if changeAmount < 0 { - excessSpending := 0 - changeAmount // equivalent to math.Abs() - return nil, 0, fmt.Errorf("total send amount plus tx fee is higher than the total input amount by %s", - dcrutil.Amount(excessSpending).String()) - } + var changeScriptSize int + if maxAmountRecipientAddress != "" { + changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress, tx.sourceWallet.chainParams) + } else { + changeScriptSize, err = calculateMultipleChangeScriptSize(changeDestinations, tx.sourceWallet.chainParams) + } + if err != nil { + return nil, err + } - if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), - changeScriptSize, txrules.DefaultRelayFeePerKb) { - if changeScriptSize > txscript.MaxScriptElementSize { - return nil, 0, fmt.Errorf("script size exceed maximum bytes pushable to the stack") - } + maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) + maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) + changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) - if maxAmountRecipientAddress != "" { - singleChangeDestination := TransactionDestination{ - Address: maxAmountRecipientAddress, - AtomAmount: changeAmount, - } - changeDestinations = []TransactionDestination{singleChangeDestination} - } + if changeAmount < 0 { + return nil, errors.New(ErrInsufficientBalance) + } - var totalChangeAmount int64 + if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), changeScriptSize, txrules.DefaultRelayFeePerKb) { + if changeScriptSize > txscript.MaxScriptElementSize { + return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") + } + var totalChangeAmount int64 + if maxAmountRecipientAddress != "" { + totalChangeAmount, outputs, err = tx.changeOutput(changeAmount, maxAmountRecipientAddress, outputs) + if err != nil { + return nil, fmt.Errorf("change address error: %v", err) + } + } else if len(changeDestinations) > 0 { for _, changeDestination := range changeDestinations { - changeOutput, err := txhelper.MakeTxOutput(changeDestination.Address, - changeDestination.AtomAmount, tx.sourceWallet.chainParams) + newChangeAmount, newOutputs, err := tx.changeOutput(changeDestination.AtomAmount, changeDestination.Address, outputs) if err != nil { - return nil, 0, fmt.Errorf("change address error: %v", err) + return nil, fmt.Errorf("change address error: %v", err) } - - totalChangeAmount += changeOutput.Value - outputs = append(outputs, changeOutput) - - // randomize the change output that was just added - changeOutputIndex := len(outputs) - 1 - txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) - } - - if totalChangeAmount > changeAmount { - return nil, 0, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ - " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), - dcrutil.Amount(changeAmount).String()) + totalChangeAmount += newChangeAmount + outputs = newOutputs } - } else { - maxSignedSize = txsizes.EstimateSerializeSize(inputScriptSizes, outputs, 0) } + if totalChangeAmount > changeAmount { + return nil, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ + " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), + dcrutil.Amount(changeAmount).String()) + } + } - return &wire.MsgTx{ + return &txauthor.AuthoredTx{ + TotalInput: dcrutil.Amount(totalInputAmount), + EstimatedSignedSerializeSize: maxSignedSize, + Tx: &wire.MsgTx{ SerType: wire.TxSerializeFull, Version: wire.TxVersion, TxIn: inputs, TxOut: outputs, LockTime: 0, Expiry: 0, - }, maxSignedSize, nil - }(tx.inputs, tx.destinations, tx.changeDestinations, nextInternalAddress) + }, + }, nil +} +func (tx *TxAuthor) changeOutput(changeAmount int64, maxAmountRecipientAddress string, outputs []*wire.TxOut) (int64, []*wire.TxOut, error) { + changeOutput, err := txhelper.MakeTxOutput(maxAmountRecipientAddress, changeAmount, tx.sourceWallet.chainParams) if err != nil { - return nil, err + return 0, nil, err } - - return &txauthor.AuthoredTx{ - TotalInput: dcrutil.Amount(totalInputAmount), - EstimatedSignedSerializeSize: maxSignedSize, Tx: msgTx, - }, nil + outputs = append(outputs, changeOutput) + changeOutputIndex := len(outputs) - 1 + txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) + return changeOutput.Value, outputs, nil } From f1488744385e850f0f411de7508edd0598e81879 Mon Sep 17 00:00:00 2001 From: song50119 Date: Wed, 4 Nov 2020 23:41:12 +0700 Subject: [PATCH 13/15] (*TxAuthor).changeOutput should not return amount --- txauthor.go | 9 ++------- utxo.go | 17 +++++++++-------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/txauthor.go b/txauthor.go index 7ddb56bc..5adac206 100644 --- a/txauthor.go +++ b/txauthor.go @@ -76,13 +76,12 @@ func (tx *TxAuthor) AddChangeDestination(address string) int { return len(tx.changeDestinations) - 1 } -func (tx *TxAuthor) UpdateChangeDestination(index int, address string, atomAmount int64) error { +func (tx *TxAuthor) UpdateChangeDestination(index int, address string) error { if len(tx.changeDestinations) < index { return errors.New(ErrIndexOutOfRange) } tx.changeDestinations[index] = TransactionDestination{ - Address: address, - AtomAmount: atomAmount, + Address: address, } return nil } @@ -170,10 +169,6 @@ func (tx *TxAuthor) UseInputs(utxoKeys []string) error { Index: uint32(index), } outputInfo, err := tx.sourceWallet.internal.OutputInfo(tx.sourceWallet.shutdownContext(), op) - if err != nil { - return err - } - if err != nil { return fmt.Errorf("no valid utxo found for '%s' in the source account", utxoKey) } diff --git a/utxo.go b/utxo.go index 885f101e..3f7cd125 100644 --- a/utxo.go +++ b/utxo.go @@ -145,17 +145,19 @@ func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations, cha } var totalChangeAmount int64 if maxAmountRecipientAddress != "" { - totalChangeAmount, outputs, err = tx.changeOutput(changeAmount, maxAmountRecipientAddress, outputs) + outputs, err = tx.changeOutput(changeAmount, maxAmountRecipientAddress, outputs) if err != nil { return nil, fmt.Errorf("change address error: %v", err) } + totalChangeAmount = changeAmount } else if len(changeDestinations) > 0 { + changeDestinations[0].AtomAmount = changeAmount for _, changeDestination := range changeDestinations { - newChangeAmount, newOutputs, err := tx.changeOutput(changeDestination.AtomAmount, changeDestination.Address, outputs) + newOutputs, err := tx.changeOutput(changeDestination.AtomAmount, changeDestination.Address, outputs) if err != nil { return nil, fmt.Errorf("change address error: %v", err) } - totalChangeAmount += newChangeAmount + totalChangeAmount += changeDestination.AtomAmount outputs = newOutputs } } @@ -180,13 +182,12 @@ func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations, cha }, nil } -func (tx *TxAuthor) changeOutput(changeAmount int64, maxAmountRecipientAddress string, outputs []*wire.TxOut) (int64, []*wire.TxOut, error) { +func (tx *TxAuthor) changeOutput(changeAmount int64, maxAmountRecipientAddress string, outputs []*wire.TxOut) ([]*wire.TxOut, error) { changeOutput, err := txhelper.MakeTxOutput(maxAmountRecipientAddress, changeAmount, tx.sourceWallet.chainParams) if err != nil { - return 0, nil, err + return nil, err } outputs = append(outputs, changeOutput) - changeOutputIndex := len(outputs) - 1 - txauthor.RandomizeOutputPosition(outputs, changeOutputIndex) - return changeOutput.Value, outputs, nil + txauthor.RandomizeOutputPosition(outputs, len(outputs)-1) + return outputs, nil } From 28d2c1e1d26fe06b49c4386d485a12e61bf7507c Mon Sep 17 00:00:00 2001 From: song50119 Date: Mon, 9 Nov 2020 20:24:36 +0700 Subject: [PATCH 14/15] Update utxo * Change (*TxAuthor).changeDestinations slice to single changeDestination * Adds (*TxAuthor).validateAmount && git push origin send-custom-inputs --- txauthor.go | 55 ++++++++++++++++++++++++++++------------------------- utxo.go | 51 +++++++++++-------------------------------------- 2 files changed, 40 insertions(+), 66 deletions(-) diff --git a/txauthor.go b/txauthor.go index 5adac206..d2b3e11a 100644 --- a/txauthor.go +++ b/txauthor.go @@ -25,7 +25,7 @@ type TxAuthor struct { destinations []TransactionDestination changeAddress string inputs []*wire.TxIn - changeDestinations []TransactionDestination + changeDestination *TransactionDestination } func (mw *MultiWallet) NewUnsignedTx(sourceWallet *Wallet, sourceAccountNumber int32) *TxAuthor { @@ -42,6 +42,10 @@ func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax return translateError(err) } + if err := tx.validateAmount(sendMax, atomAmount); err != nil { + return err + } + tx.destinations = append(tx.destinations, TransactionDestination{ Address: address, AtomAmount: atomAmount, @@ -51,12 +55,21 @@ func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax return nil } -func (tx *TxAuthor) UpdateSendDestination(index int, address string, atomAmount int64, sendMax bool) { +func (tx *TxAuthor) UpdateSendDestination(index int, address string, atomAmount int64, sendMax bool) error { + if err := tx.validateAmount(sendMax, atomAmount); err != nil { + return err + } + + if len(tx.destinations) < index { + return errors.New(ErrIndexOutOfRange) + } + tx.destinations[index] = TransactionDestination{ Address: address, AtomAmount: atomAmount, SendMax: sendMax, } + return nil } func (tx *TxAuthor) RemoveSendDestination(index int) { @@ -69,31 +82,14 @@ func (tx *TxAuthor) SendDestination(atIndex int) *TransactionDestination { return &tx.destinations[atIndex] } -func (tx *TxAuthor) AddChangeDestination(address string) int { - tx.changeDestinations = append(tx.changeDestinations, TransactionDestination{ - Address: address, - }) - return len(tx.changeDestinations) - 1 -} - -func (tx *TxAuthor) UpdateChangeDestination(index int, address string) error { - if len(tx.changeDestinations) < index { - return errors.New(ErrIndexOutOfRange) - } - tx.changeDestinations[index] = TransactionDestination{ +func (tx *TxAuthor) SetChangeDestination(address string) { + tx.changeDestination = &TransactionDestination{ Address: address, } - return nil } -func (tx *TxAuthor) RemoveChangeDestination(index int) error { - if len(tx.changeDestinations) < index { - return errors.New(ErrIndexOutOfRange) - } - if len(tx.changeDestinations) > index { - tx.changeDestinations = append(tx.changeDestinations[:index], tx.changeDestinations[index+1:]...) - } - return nil +func (tx *TxAuthor) RemoveChangeDestination() { + tx.changeDestination = nil } func (tx *TxAuthor) TotalSendAmount() *Amount { @@ -279,9 +275,8 @@ func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { ctx := tx.sourceWallet.shutdownContext() for _, destination := range tx.destinations { - // validate the amount to send to this destination address - if !destination.SendMax && (destination.AtomAmount <= 0 || destination.AtomAmount > MaxAmountAtom) { - return nil, errors.E(errors.Invalid, "invalid amount") + if err := tx.validateAmount(destination.SendMax, destination.AtomAmount); err != nil { + return nil, err } // check if multiple destinations are set to receive max amount @@ -351,3 +346,11 @@ func (tx *TxAuthor) changeSource(ctx context.Context) (txauthor.ChangeSource, er return changeSource, nil } + +// validateAmount validate the amount to send to a destination address +func (tx *TxAuthor) validateAmount(sendMax bool, atomAmount int64) error { + if !sendMax && (atomAmount <= 0 || atomAmount > MaxAmountAtom) { + return errors.E(errors.Invalid, "invalid amount") + } + return nil +} diff --git a/utxo.go b/utxo.go index 3f7cd125..c5280f8f 100644 --- a/utxo.go +++ b/utxo.go @@ -24,19 +24,6 @@ func calculateChangeScriptSize(changeAddress string, chainParams *chaincfg.Param return changeSource.ScriptSize(), nil } -func calculateMultipleChangeScriptSize(changeDestinations []TransactionDestination, - chainParams *chaincfg.Params) (int, error) { - var totalChangeScriptSize int - for _, changeDestination := range changeDestinations { - changeScriptSize, err := calculateChangeScriptSize(changeDestination.Address, chainParams) - if err != nil { - return 0, err - } - totalChangeScriptSize += changeScriptSize - } - return totalChangeScriptSize, nil -} - // ParseOutputsAndChangeDestination generates and returns TxOuts // using the provided slice of transaction destinations. // Any destination set to receive max amount is not included in the TxOuts returned, @@ -49,9 +36,8 @@ func (tx *TxAuthor) ParseOutputsAndChangeDestination(txDestinations []Transactio var maxAmountRecipientAddress string for _, destination := range txDestinations { - // validate the amount to send to this destination address - if !destination.SendMax && (destination.AtomAmount <= 0 || destination.AtomAmount > dcrutil.MaxAmount) { - return nil, 0, "", errors.E(errors.Invalid, "invalid amount") + if err := tx.validateAmount(destination.SendMax, destination.AtomAmount); err != nil { + return nil, 0, "", err } // check if multiple destinations are set to receive max amount @@ -89,22 +75,22 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { return addr.Address(), nil } - return tx.newUnsignedTxUTXO(tx.inputs, tx.destinations, tx.changeDestinations, nextInternalAddress) + return tx.newUnsignedTxUTXO(tx.inputs, tx.destinations, tx.changeDestination, nextInternalAddress) } -func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations, changeDestinations []TransactionDestination, +func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations []TransactionDestination, changeDestination *TransactionDestination, nextInternalAddress nextAddressFunc) (*txauthor.AuthoredTx, error) { outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(sendDestinations) if err != nil { return nil, err } - if maxAmountRecipientAddress != "" && len(changeDestinations) > 0 { + if maxAmountRecipientAddress != "" && changeDestination != nil { return nil, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ " change destinations must not be provided") } - if maxAmountRecipientAddress == "" && len(changeDestinations) == 0 { + if maxAmountRecipientAddress == "" && changeDestination == nil { // no change specified, generate new internal address to use as change (max amount recipient) maxAmountRecipientAddress, err = nextInternalAddress() if err != nil { @@ -125,7 +111,7 @@ func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations, cha if maxAmountRecipientAddress != "" { changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress, tx.sourceWallet.chainParams) } else { - changeScriptSize, err = calculateMultipleChangeScriptSize(changeDestinations, tx.sourceWallet.chainParams) + changeScriptSize, err = calculateChangeScriptSize(changeDestination.Address, tx.sourceWallet.chainParams) } if err != nil { return nil, err @@ -143,28 +129,13 @@ func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations, cha if changeScriptSize > txscript.MaxScriptElementSize { return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") } - var totalChangeAmount int64 if maxAmountRecipientAddress != "" { outputs, err = tx.changeOutput(changeAmount, maxAmountRecipientAddress, outputs) - if err != nil { - return nil, fmt.Errorf("change address error: %v", err) - } - totalChangeAmount = changeAmount - } else if len(changeDestinations) > 0 { - changeDestinations[0].AtomAmount = changeAmount - for _, changeDestination := range changeDestinations { - newOutputs, err := tx.changeOutput(changeDestination.AtomAmount, changeDestination.Address, outputs) - if err != nil { - return nil, fmt.Errorf("change address error: %v", err) - } - totalChangeAmount += changeDestination.AtomAmount - outputs = newOutputs - } + } else if changeDestination != nil { + outputs, err = tx.changeOutput(changeAmount, changeDestination.Address, outputs) } - if totalChangeAmount > changeAmount { - return nil, fmt.Errorf("total amount allocated to change addresses (%s) is higher than"+ - " actual change amount for transaction (%s)", dcrutil.Amount(totalChangeAmount).String(), - dcrutil.Amount(changeAmount).String()) + if err != nil { + return nil, fmt.Errorf("change address error: %v", err) } } From e07dc51cac94006ff5d5e1100d5acca67e259d18 Mon Sep 17 00:00:00 2001 From: song50119 Date: Mon, 9 Nov 2020 20:26:34 +0700 Subject: [PATCH 15/15] Change validateAmount to validateSendAmount --- txauthor.go | 10 +++++----- utxo.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/txauthor.go b/txauthor.go index d2b3e11a..f0795758 100644 --- a/txauthor.go +++ b/txauthor.go @@ -42,7 +42,7 @@ func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax return translateError(err) } - if err := tx.validateAmount(sendMax, atomAmount); err != nil { + if err := tx.validateSendAmount(sendMax, atomAmount); err != nil { return err } @@ -56,7 +56,7 @@ func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax } func (tx *TxAuthor) UpdateSendDestination(index int, address string, atomAmount int64, sendMax bool) error { - if err := tx.validateAmount(sendMax, atomAmount); err != nil { + if err := tx.validateSendAmount(sendMax, atomAmount); err != nil { return err } @@ -275,7 +275,7 @@ func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { ctx := tx.sourceWallet.shutdownContext() for _, destination := range tx.destinations { - if err := tx.validateAmount(destination.SendMax, destination.AtomAmount); err != nil { + if err := tx.validateSendAmount(destination.SendMax, destination.AtomAmount); err != nil { return nil, err } @@ -347,8 +347,8 @@ func (tx *TxAuthor) changeSource(ctx context.Context) (txauthor.ChangeSource, er return changeSource, nil } -// validateAmount validate the amount to send to a destination address -func (tx *TxAuthor) validateAmount(sendMax bool, atomAmount int64) error { +// validateSendAmount validate the amount to send to a destination address +func (tx *TxAuthor) validateSendAmount(sendMax bool, atomAmount int64) error { if !sendMax && (atomAmount <= 0 || atomAmount > MaxAmountAtom) { return errors.E(errors.Invalid, "invalid amount") } diff --git a/utxo.go b/utxo.go index c5280f8f..2fde8a21 100644 --- a/utxo.go +++ b/utxo.go @@ -36,7 +36,7 @@ func (tx *TxAuthor) ParseOutputsAndChangeDestination(txDestinations []Transactio var maxAmountRecipientAddress string for _, destination := range txDestinations { - if err := tx.validateAmount(destination.SendMax, destination.AtomAmount); err != nil { + if err := tx.validateSendAmount(destination.SendMax, destination.AtomAmount); err != nil { return nil, 0, "", err }