Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send custom inputs #165

Merged
merged 16 commits into from
Nov 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package dcrlibwallet

import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"

"github.com/decred/dcrd/chaincfg/v2"
"github.com/decred/dcrd/dcrutil/v2"
"github.com/decred/dcrwallet/errors/v2"
w "github.com/decred/dcrwallet/wallet/v3"
"github.com/planetdecred/dcrlibwallet/addresshelper"
)

func (wallet *Wallet) GetAccounts() (string, error) {
Expand Down Expand Up @@ -119,6 +124,59 @@ func (wallet *Wallet) SpendableForAccount(account int32) (int64, error) {
return int64(bals.Spendable), nil
}

func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) {
policy := w.OutputSelectionPolicy{
Account: uint32(account),
RequiredConfirmations: wallet.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 := wallet.internal.SelectInputs(wallet.shutdownContext(), dcrutil.Amount(0), policy)

if err != nil {
return nil, err
}

unspentOutputs := make([]*UnspentOutput, len(inputDetail.Inputs))

for i, input := range inputDetail.Inputs {
outputInfo, err := wallet.internal.OutputInfo(wallet.shutdownContext(), &input.PreviousOutPoint)
if err != nil {
return nil, err
}

// unique key to identify utxo
outputKey := fmt.Sprintf("%s:%d", input.PreviousOutPoint.Hash, input.PreviousOutPoint.Index)

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)
}

var confirmations int32
inputBlockHeight := int32(input.BlockHeight)
if inputBlockHeight != -1 {
confirmations = wallet.GetBestBlock() - inputBlockHeight + 1
}

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,
Addresses: strings.Join(addresses, ", "),
Confirmations: confirmations,
}
}

return unspentOutputs, nil
}

func (wallet *Wallet) NextAccount(accountName string, privPass []byte) (int32, error) {
lock := make(chan time.Time, 1)
defer func() {
Expand Down
1 change: 1 addition & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
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
github.com/decred/dcrd/txscript v1.1.0
github.com/decred/dcrd/txscript/v2 v2.1.0
github.com/decred/dcrd/wire v1.3.0
github.com/decred/dcrdata/txhelpers v1.1.0
Expand Down
83 changes: 79 additions & 4 deletions txauthor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"bytes"
"context"
"fmt"
"strconv"
"strings"
"time"

"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrutil/v2"
"github.com/decred/dcrd/txscript/v2"
"github.com/decred/dcrd/wire"
Expand All @@ -21,6 +24,8 @@ type TxAuthor struct {
sourceAccountNumber uint32
destinations []TransactionDestination
changeAddress string
inputs []*wire.TxIn
changeDestination *TransactionDestination
}

func (mw *MultiWallet) NewUnsignedTx(sourceWallet *Wallet, sourceAccountNumber int32) *TxAuthor {
Expand All @@ -37,6 +42,10 @@ func (tx *TxAuthor) AddSendDestination(address string, atomAmount int64, sendMax
return translateError(err)
}

if err := tx.validateSendAmount(sendMax, atomAmount); err != nil {
return err
}

tx.destinations = append(tx.destinations, TransactionDestination{
Address: address,
AtomAmount: atomAmount,
Expand All @@ -46,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.validateSendAmount(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) {
Expand All @@ -64,6 +82,16 @@ func (tx *TxAuthor) SendDestination(atIndex int) *TransactionDestination {
return &tx.destinations[atIndex]
}

func (tx *TxAuthor) SetChangeDestination(address string) {
tx.changeDestination = &TransactionDestination{
Address: address,
}
}

func (tx *TxAuthor) RemoveChangeDestination() {
tx.changeDestination = nil
}

func (tx *TxAuthor) TotalSendAmount() *Amount {
var totalSendAmountAtom int64 = 0
for _, destination := range tx.destinations {
Expand Down Expand Up @@ -113,6 +141,42 @@ func (tx *TxAuthor) EstimateMaxSendAmount() (*Amount, error) {
}, nil
}

func (tx *TxAuthor) UseInputs(utxoKeys []string) error {
beansgum marked this conversation as resolved.
Show resolved Hide resolved
// 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
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)
}

txHash, err := chainhash.NewHashFromStr(hash)
if err != nil {
return err
}

op := &wire.OutPoint{
Hash: *txHash,
Index: uint32(index),
}
outputInfo, err := tx.sourceWallet.internal.OutputInfo(tx.sourceWallet.shutdownContext(), op)
if err != nil {
return fmt.Errorf("no valid utxo found for '%s' in the source account", utxoKey)
}

input := wire.NewTxIn(op, int64(outputInfo.Amount), nil)
inputs = append(inputs, input)
}

tx.inputs = inputs
return nil
}

func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) {
defer func() {
for i := range privatePassphrase {
Expand Down Expand Up @@ -199,6 +263,10 @@ 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
Expand All @@ -207,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.validateSendAmount(destination.SendMax, destination.AtomAmount); err != nil {
return nil, err
}

// check if multiple destinations are set to receive max amount
Expand Down Expand Up @@ -279,3 +346,11 @@ func (tx *TxAuthor) changeSource(ctx context.Context) (txauthor.ChangeSource, er

return changeSource, nil
}

// 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")
}
return nil
}
13 changes: 13 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,16 @@ type Proposal struct {
type Proposals struct {
Proposals []Proposal `json:"proposals"`
}

type UnspentOutput struct {
TransactionHash []byte
OutputIndex uint32
OutputKey string
ReceiveTime int64
Amount int64
FromCoinbase bool
Tree int32
PkScript []byte
Addresses string // separated by commas
Confirmations int32
}
Loading