Skip to content

Commit

Permalink
Merge pull request btcsuite#912 from Chinwendu20/sendcoin
Browse files Browse the repository at this point in the history
Use selected Utxos while crafting txn
  • Loading branch information
guggero authored and buck54321 committed Apr 21, 2024
1 parent 62d2265 commit b628ee2
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 57 deletions.
103 changes: 80 additions & 23 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ func makeInputSource(eligible []Coin) txauthor.InputSource {
}
}

// constantInputSource creates an input source function that always returns the
// static set of user-selected UTXOs.
func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource {
// Current inputs and their total value. These won't change over
// different invocations as we want our inputs to remain static since
// they're selected by the user.
currentTotal := ltcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(eligible))
currentScripts := make([][]byte, 0, len(eligible))
currentInputValues := make([]ltcutil.Amount, 0, len(eligible))

for _, credit := range eligible {
nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil)
currentTotal += credit.Amount
currentInputs = append(currentInputs, nextInput)
currentScripts = append(currentScripts, credit.PkScript)
currentInputValues = append(currentInputValues, credit.Amount)
}

return func(target ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn,
[]ltcutil.Amount, [][]byte, error) {

return currentTotal, currentInputs, currentInputValues,
currentScripts, nil
}
}

// secretSource is an implementation of txauthor.SecretSource for the wallet's
// address manager.
type secretSource struct {
Expand Down Expand Up @@ -112,8 +139,8 @@ func (s secretSource) GetScript(addr ltcutil.Address) ([]byte, error) {
func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
coinSelectKeyScope, changeKeyScope *waddrmgr.KeyScope,
account uint32, minconf int32, feeSatPerKb ltcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, dryRun bool) (
*txauthor.AuthoredTx, error) {
strategy CoinSelectionStrategy, dryRun bool,
selectedUtxos []wire.OutPoint) (*txauthor.AuthoredTx, error) {

chainClient, err := w.requireChainClient()
if err != nil {
Expand All @@ -127,8 +154,8 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
}

// Fall back to default coin selection strategy if none is supplied.
if coinSelectionStrategy == nil {
coinSelectionStrategy = CoinSelectionLargest
if strategy == nil {
strategy = CoinSelectionLargest
}

var tx *txauthor.AuthoredTx
Expand All @@ -147,27 +174,57 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
return err
}

// Wrap our coins in a type that implements the SelectableCoin
// interface, so we can arrange them according to the selected
// coin selection strategy.
wrappedEligible := make([]Coin, len(eligible))
for i := range eligible {
wrappedEligible[i] = Coin{
TxOut: wire.TxOut{
Value: int64(eligible[i].Amount),
PkScript: eligible[i].PkScript,
},
OutPoint: eligible[i].OutPoint,
var inputSource txauthor.InputSource
if len(selectedUtxos) > 0 {
eligibleByOutpoint := make(
map[wire.OutPoint]wtxmgr.Credit,
)

for _, e := range eligible {
eligibleByOutpoint[e.OutPoint] = e
}
}
arrangedCoins, err := coinSelectionStrategy.ArrangeCoins(
wrappedEligible, feeSatPerKb,
)
if err != nil {
return err
}

inputSource := makeInputSource(arrangedCoins)
var eligibleSelectedUtxo []wtxmgr.Credit
for _, outpoint := range selectedUtxos {
e, ok := eligibleByOutpoint[outpoint]

if !ok {
return fmt.Errorf("selected outpoint "+
"not eligible for "+
"spending: %v", outpoint)
}
eligibleSelectedUtxo = append(
eligibleSelectedUtxo, e,
)
}

inputSource = constantInputSource(eligibleSelectedUtxo)

} else {
// Wrap our coins in a type that implements the
// SelectableCoin interface, so we can arrange them
// according to the selected coin selection strategy.
wrappedEligible := make([]Coin, len(eligible))
for i := range eligible {
wrappedEligible[i] = Coin{
TxOut: wire.TxOut{
Value: int64(
eligible[i].Amount,
),
PkScript: eligible[i].PkScript,
},
OutPoint: eligible[i].OutPoint,
}
}

arrangedCoins, err := strategy.ArrangeCoins(
wrappedEligible, feeSatPerKb,
)
if err != nil {
return err
}
inputSource = makeInputSource(arrangedCoins)
}

tx, err = txauthor.NewUnsignedTransaction(
outputs, feeSatPerKb, inputSource, changeSource,
Expand Down
93 changes: 90 additions & 3 deletions wallet/createtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// database us not inflated.
dryRunTx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true,
nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand All @@ -96,6 +97,7 @@ func TestTxToOutputsDryRun(t *testing.T) {

dryRunTx2, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true,
nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand Down Expand Up @@ -131,6 +133,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// to the database.
tx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, false,
nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand Down Expand Up @@ -280,7 +283,7 @@ func TestTxToOutputsRandom(t *testing.T) {
createTx := func() *txauthor.AuthoredTx {
tx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, feeSatPerKb,
CoinSelectionRandom, true,
CoinSelectionRandom, true, nil,
)
require.NoError(t, err)
return tx
Expand Down Expand Up @@ -352,7 +355,7 @@ func TestCreateSimpleCustomChange(t *testing.T) {
}
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000,
CoinSelectionLargest, true,
CoinSelectionLargest, true, nil,
)
require.NoError(t, err)

Expand All @@ -378,7 +381,7 @@ func TestCreateSimpleCustomChange(t *testing.T) {
tx2, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, &waddrmgr.KeyScopeBIP0086,
&waddrmgr.KeyScopeBIP0084, 0, 1, 1000, CoinSelectionLargest,
true,
true, nil,
)
require.NoError(t, err)

Expand All @@ -399,3 +402,87 @@ func TestCreateSimpleCustomChange(t *testing.T) {
require.Equal(t, scriptType, txscript.WitnessV0PubKeyHashTy)
}
}

// TestSelectUtxosTxoToOutpoint tests that it is possible to use passed
// selected utxos to craft a transaction in `txToOutpoint`.
func TestSelectUtxosTxoToOutpoint(t *testing.T) {
t.Parallel()

w, cleanup := testWallet(t)
defer cleanup()

// First, we'll make a P2TR and a P2WKH address to send some coins to.
p2wkhAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084)
require.NoError(t, err)

p2trAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0086)
require.NoError(t, err)

// We'll now make a transaction that'll send coins to both outputs,
// then "credit" the wallet for that send.
p2wkhScript, err := txscript.PayToAddrScript(p2wkhAddr)
require.NoError(t, err)

p2trScript, err := txscript.PayToAddrScript(p2trAddr)
require.NoError(t, err)

incomingTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{},
},
TxOut: []*wire.TxOut{
wire.NewTxOut(1_000_000, p2wkhScript),
wire.NewTxOut(2_000_000, p2trScript),
wire.NewTxOut(3_000_000, p2trScript),
wire.NewTxOut(7_000_000, p2trScript),
},
}
addUtxo(t, w, incomingTx)

// We expect 4 unspent utxos.
unspent, err := w.ListUnspent(0, 80, "")
require.NoError(t, err, "unexpected error while calling "+
"list unspent")

require.Len(t, unspent, 4, "expected 4 unspent "+
"utxos")

selectUtxos := []wire.OutPoint{
{
Hash: incomingTx.TxHash(),
Index: 1,
},
{
Hash: incomingTx.TxHash(),
Index: 2,
},
}

// Test by sending 200_000.
targetTxOut := &wire.TxOut{
Value: 200_000,
PkScript: p2trScript,
}
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000,
CoinSelectionLargest, true, selectUtxos,
)
require.NoError(t, err)

// We expect all and only our select utxos to be input in this
// transaction.
require.Len(t, tx1.Tx.TxIn, len(selectUtxos))

lookupSelectUtxos := make(map[wire.OutPoint]struct{})
for _, utxo := range selectUtxos {
lookupSelectUtxos[utxo] = struct{}{}
}

for _, tx := range tx1.Tx.TxIn {
_, ok := lookupSelectUtxos[tx.PreviousOutPoint]
require.True(t, ok, "unexpected outpoint in txin")
}

// Expect two outputs, change and the actual payment to the address.
require.Len(t, tx1.Tx.TxOut, 2)
}
27 changes: 0 additions & 27 deletions wallet/psbt.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,30 +567,3 @@ func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher {

return fetcher
}

// constantInputSource creates an input source function that always returns the
// static set of user-selected UTXOs.
func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource {
// Current inputs and their total value. These won't change over
// different invocations as we want our inputs to remain static since
// they're selected by the user.
currentTotal := ltcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(eligible))
currentScripts := make([][]byte, 0, len(eligible))
currentInputValues := make([]ltcutil.Amount, 0, len(eligible))

for _, credit := range eligible {
nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil)
currentTotal += credit.Amount
currentInputs = append(currentInputs, nextInput)
currentScripts = append(currentScripts, credit.PkScript)
currentInputValues = append(currentInputValues, credit.Amount)
}

return func(target ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn,
[]ltcutil.Amount, [][]byte, error) {

return currentTotal, currentInputs, currentInputValues,
currentScripts, nil
}
}
46 changes: 42 additions & 4 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ type (
coinSelectionStrategy CoinSelectionStrategy
dryRun bool
resp chan createTxResponse
selectUtxos []wire.OutPoint
}
createTxResponse struct {
tx *txauthor.AuthoredTx
Expand Down Expand Up @@ -1240,7 +1241,7 @@ out:
tx, err := w.txToOutputs(
txr.outputs, txr.coinSelectKeyScope, txr.changeKeyScope,
txr.account, txr.minconf, txr.feeSatPerKB,
txr.coinSelectionStrategy, txr.dryRun,
txr.coinSelectionStrategy, txr.dryRun, txr.selectUtxos,
)

release()
Expand All @@ -1257,6 +1258,7 @@ out:
// scope, which otherwise will default to the specified coin selection scope.
type txCreateOptions struct {
changeKeyScope *waddrmgr.KeyScope
selectUtxos []wire.OutPoint
}

// TxCreateOption is a set of optional arguments to modify the tx creation
Expand All @@ -1279,6 +1281,14 @@ func WithCustomChangeScope(changeScope *waddrmgr.KeyScope) TxCreateOption {
}
}

// WithCustomSelectUtxos is used to specify the inputs to be used while
// creating txns.
func WithCustomSelectUtxos(utxos []wire.OutPoint) TxCreateOption {
return func(opts *txCreateOptions) {
opts.selectUtxos = utxos
}
}

// CreateSimpleTx creates a new signed transaction spending unspent outputs with
// at least minconf confirmations spending to any number of address/amount
// pairs. Only unspent outputs belonging to the given key scope and account will
Expand Down Expand Up @@ -1322,6 +1332,7 @@ func (w *Wallet) CreateSimpleTx(coinSelectKeyScope *waddrmgr.KeyScope,
coinSelectionStrategy: coinSelectionStrategy,
dryRun: dryRun,
resp: make(chan createTxResponse),
selectUtxos: opts.selectUtxos,
}
w.createTxRequests <- req
resp := <-req.resp
Expand Down Expand Up @@ -3382,8 +3393,33 @@ func (w *Wallet) TotalReceivedForAddr(addr ltcutil.Address, minConf int32) (ltcu
// returns the transaction upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb ltcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string) (
*wire.MsgTx, error) {
coinSelectionStrategy CoinSelectionStrategy, label string) (*wire.MsgTx,
error) {

return w.sendOutputs(
outputs, keyScope, account, minconf, satPerKb,
coinSelectionStrategy, label,
)
}

// SendOutputsWithInput creates and sends payment transactions using the
// provided selected utxos. It returns the transaction upon success.
func (w *Wallet) SendOutputsWithInput(outputs []*wire.TxOut,
keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb ltcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string,
selectedUtxos []wire.OutPoint) (*wire.MsgTx, error) {

return w.sendOutputs(outputs, keyScope, account, minconf, satPerKb,
coinSelectionStrategy, label, selectedUtxos...)
}

// sendOutputs creates and sends payment transactions. It returns the
// transaction upon success.
func (w *Wallet) sendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb ltcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string,
selectedUtxos ...wire.OutPoint) (*wire.MsgTx, error) {

// Ensure the outputs to be created adhere to the network's consensus
// rules.
Expand All @@ -3402,7 +3438,9 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
// been confirmed.
createdTx, err := w.CreateSimpleTx(
keyScope, account, outputs, minconf, satPerKb,
coinSelectionStrategy, false,
coinSelectionStrategy, false, WithCustomSelectUtxos(
selectedUtxos,
),
)
if err != nil {
return nil, err
Expand Down

0 comments on commit b628ee2

Please sign in to comment.