Skip to content
Open
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
51 changes: 36 additions & 15 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
account uint32, minconf int32, feeSatPerKb btcutil.Amount,
strategy CoinSelectionStrategy, dryRun bool,
selectedUtxos []wire.OutPoint,
allowUtxo func(utxo wtxmgr.Credit) bool) (
allowUtxo func(utxo wtxmgr.Credit) bool,
changeAddr btcutil.Address) (
*txauthor.AuthoredTx, error) {

chainClient, err := w.requireChainClient()
Expand Down Expand Up @@ -176,7 +177,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
var tx *txauthor.AuthoredTx
err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error {
addrmgrNs, changeSource, err := w.addrMgrWithChangeSource(
dbtx, changeKeyScope, account,
dbtx, changeKeyScope, account, changeAddr,
)
if err != nil {
return err
Expand Down Expand Up @@ -319,8 +320,11 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,

// Finally, we'll request the backend to notify us of the
// transaction that pays to the change address, if there is one,
// when it confirms.
if tx.ChangeIndex >= 0 {
// when it confirms. We skip this for external change addresses
// since they are not wallet-owned — registering them would cause
// future payments to that address to be treated as wallet-relevant
// transactions.
if tx.ChangeIndex >= 0 && changeAddr == nil {
changePkScript := tx.Tx.TxOut[tx.ChangeIndex].PkScript
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
changePkScript, w.chainParams,
Expand Down Expand Up @@ -429,14 +433,32 @@ func inputYieldsPositively(credit *wire.TxOut,
}

// addrMgrWithChangeSource returns the address manager bucket and a change
// source that returns change addresses from said address manager. The change
// addresses will come from the specified key scope and account, unless a key
// scope is not specified. In that case, change addresses will always come from
// the P2WKH key scope.
// source. If changeAddr is non-nil, the change source always pays to that
// address. Otherwise, change addresses are derived from the wallet using the
// specified key scope and account (defaulting to P2TR if no scope is given).
func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
changeKeyScope *waddrmgr.KeyScope, account uint32) (
changeKeyScope *waddrmgr.KeyScope, account uint32,
changeAddr btcutil.Address) (
walletdb.ReadWriteBucket, *txauthor.ChangeSource, error) {

addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)

if changeAddr != nil {
Comment thread
murraystewart96 marked this conversation as resolved.
if !changeAddr.IsForNet(w.chainParams) {
return nil, nil, fmt.Errorf("change address %v is not "+
"for network %v", changeAddr, w.chainParams.Name)
}

pkScript, err := txscript.PayToAddrScript(changeAddr)
Comment thread
murraystewart96 marked this conversation as resolved.
if err != nil {
return nil, nil, err
}
return addrmgrNs, &txauthor.ChangeSource{
Comment thread
murraystewart96 marked this conversation as resolved.
ScriptSize: len(pkScript),
NewScript: func() ([]byte, error) { return pkScript, nil },
}, nil
}

// Determine the address type for change addresses of the given
// account.
if changeKeyScope == nil {
Expand All @@ -446,7 +468,6 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,

// It's possible for the account to have an address schema override, so
// prefer that if it exists.
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
scopeMgr, err := w.Manager.FetchScopedKeyManager(*changeKeyScope)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -480,22 +501,22 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
// from the imported account, change addresses are created from
// account 0.
var (
changeAddr btcutil.Address
err error
derivedAddr btcutil.Address
err error
)
if account == waddrmgr.ImportedAddrAccount {
changeAddr, err = w.newChangeAddress(
derivedAddr, err = w.newChangeAddress(
addrmgrNs, 0, *changeKeyScope,
)
} else {
changeAddr, err = w.newChangeAddress(
derivedAddr, err = w.newChangeAddress(
addrmgrNs, account, *changeKeyScope,
)
}
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(changeAddr)
return txscript.PayToAddrScript(derivedAddr)
}

return addrmgrNs, &txauthor.ChangeSource{
Expand Down
68 changes: 61 additions & 7 deletions wallet/createtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// database us not inflated.
dryRunTx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true,
nil, alwaysAllowUtxo,
nil, alwaysAllowUtxo, nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand All @@ -99,7 +99,7 @@ func TestTxToOutputsDryRun(t *testing.T) {

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

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

Expand All @@ -446,6 +446,60 @@ func TestCreateSimpleCustomChange(t *testing.T) {
}
}

// TestTxToOutputsCustomChangeAddr tests that when a custom change address is
// passed to txToOutputs, the resulting transaction sends change to that address
// rather than a wallet-derived one.
func TestTxToOutputsCustomChangeAddr(t *testing.T) {
t.Parallel()

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

// Fund the wallet with a P2WKH output.
p2wkhAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084)
require.NoError(t, err)

p2wkhScript, err := txscript.PayToAddrScript(p2wkhAddr)
require.NoError(t, err)

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

// Derive an external address to use as the custom change destination.
// This address is not wallet-managed — it simulates a user-supplied
// address. Must be a testnet address since testWallet uses TestNet3Params.
changeAddr, err := btcutil.DecodeAddress(
"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
&chaincfg.TestNet3Params,
)
require.NoError(t, err)

changeScript, err := txscript.PayToAddrScript(changeAddr)
require.NoError(t, err)

// Send 500_000 sats — less than the funded amount so a change output
// must be produced.
targetTxOut := &wire.TxOut{
Value: 500_000,
PkScript: p2wkhScript,
}
tx, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000,
CoinSelectionLargest, true, nil, alwaysAllowUtxo, changeAddr,
)
require.NoError(t, err)
require.NotEqual(t, tx.ChangeIndex, -1, "expected a change output")

// The change output must use the caller-supplied script.
changeOut := tx.Tx.TxOut[tx.ChangeIndex]
require.Equal(t, changeScript, changeOut.PkScript,
"change output does not use the custom change address")
}

// TestSelectUtxosTxoToOutpoint tests that it is possible to use passed
// selected utxos to craft a transaction in `txToOutpoint`.
func TestSelectUtxosTxoToOutpoint(t *testing.T) {
Expand Down Expand Up @@ -562,7 +616,7 @@ func TestSelectUtxosTxoToOutpoint(t *testing.T) {
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1,
1000, CoinSelectionLargest, true,
tc.selectUTXOs, alwaysAllowUtxo,
tc.selectUTXOs, alwaysAllowUtxo, nil,
)
if tc.errString != "" {
require.ErrorContains(t, err, tc.errString)
Expand Down
6 changes: 4 additions & 2 deletions wallet/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,17 @@ type Interface interface {
SendOutputs(outputs []*wire.TxOut,
coinSelectKeyScope *waddrmgr.KeyScope, account uint32,
minconf int32, satPerKb btcutil.Amount,
strategy CoinSelectionStrategy, label string) (*wire.MsgTx, error)
strategy CoinSelectionStrategy, label string,
optFuncs ...TxCreateOption) (*wire.MsgTx, error)

// SendOutputsWithInput is a variant of SendOutputs that allows
// specifying a particular input to use for the transaction.
SendOutputsWithInput(outputs []*wire.TxOut,
coinSelectKeyScope *waddrmgr.KeyScope, account uint32,
minconf int32, satPerKb btcutil.Amount,
strategy CoinSelectionStrategy, label string,
inputs []wire.OutPoint) (*wire.MsgTx, error)
inputs []wire.OutPoint,
optFuncs ...TxCreateOption) (*wire.MsgTx, error)

// PublishTransaction broadcasts a transaction to the network.
PublishTransaction(tx *wire.MsgTx, label string) error
Expand Down
13 changes: 12 additions & 1 deletion wallet/psbt.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
coinSelectionStrategy CoinSelectionStrategy,
optFuncs ...TxCreateOption) (int32, error) {

// WithChangeAddress is not supported for PSBT funding.
// It requires the address to be wallet-owned.
opts := defaultTxCreateOptions()
for _, f := range optFuncs {
f(opts)
}
if opts.changeAddr != nil {
return 0, fmt.Errorf("WithChangeAddress is not supported for " +
"PSBT funding")
}

// Make sure the packet is well formed. We only require there to be at
// least one input or output.
err := psbt.VerifyInputOutputLen(packet, false, false)
Expand Down Expand Up @@ -182,7 +193,7 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
// a new change address into the database.
err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error {
_, changeSource, err := w.addrMgrWithChangeSource(
dbtx, opts.changeKeyScope, account,
dbtx, opts.changeKeyScope, account, nil,
Comment thread
murraystewart96 marked this conversation as resolved.
)
if err != nil {
return err
Expand Down
36 changes: 27 additions & 9 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ type (
resp chan createTxResponse
selectUtxos []wire.OutPoint
allowUtxo func(wtxmgr.Credit) bool
changeAddr btcutil.Address
}
createTxResponse struct {
tx *txauthor.AuthoredTx
Expand Down Expand Up @@ -1256,6 +1257,7 @@ out:
txr.changeKeyScope, txr.account, txr.minconf,
txr.feeSatPerKB, txr.coinSelectionStrategy,
txr.dryRun, txr.selectUtxos, txr.allowUtxo,
txr.changeAddr,
)

release()
Expand All @@ -1274,6 +1276,7 @@ type txCreateOptions struct {
changeKeyScope *waddrmgr.KeyScope
selectUtxos []wire.OutPoint
allowUtxo func(wtxmgr.Credit) bool
changeAddr btcutil.Address
}

// TxCreateOption is a set of optional arguments to modify the tx creation
Expand Down Expand Up @@ -1313,6 +1316,15 @@ func WithUtxoFilter(allowUtxo func(utxo wtxmgr.Credit) bool) TxCreateOption {
}
}

// WithChangeAddress can be used to specify a custom address for the change
// output. If unspecified, a change address is derived from the wallet's HD
// tree using the configured change key scope.
func WithChangeAddress(addr btcutil.Address) TxCreateOption {
Comment thread
murraystewart96 marked this conversation as resolved.
return func(opts *txCreateOptions) {
opts.changeAddr = addr
}
}

// 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 @@ -1358,6 +1370,7 @@ func (w *Wallet) CreateSimpleTx(coinSelectKeyScope *waddrmgr.KeyScope,
resp: make(chan createTxResponse),
selectUtxos: opts.selectUtxos,
allowUtxo: opts.allowUtxo,
changeAddr: opts.changeAddr,
}
w.createTxRequests <- req
resp := <-req.resp
Expand Down Expand Up @@ -3524,12 +3537,12 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
// returns the transaction upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string) (*wire.MsgTx,
error) {
coinSelectionStrategy CoinSelectionStrategy, label string,
optFuncs ...TxCreateOption) (*wire.MsgTx, error) {

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

Expand All @@ -3539,18 +3552,20 @@ func (w *Wallet) SendOutputsWithInput(outputs []*wire.TxOut,
keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string,
selectedUtxos []wire.OutPoint) (*wire.MsgTx, error) {
selectedUtxos []wire.OutPoint,
optFuncs ...TxCreateOption) (*wire.MsgTx, error) {

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

// 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 btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string,
selectedUtxos ...wire.OutPoint) (*wire.MsgTx, error) {
selectedUtxos []wire.OutPoint, optFuncs ...TxCreateOption) (*wire.MsgTx,
error) {

// Ensure the outputs to be created adhere to the network's consensus
// rules.
Expand All @@ -3563,15 +3578,18 @@ func (w *Wallet) sendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
}
}

allOpts := append(
[]TxCreateOption{WithCustomSelectUtxos(selectedUtxos)},
optFuncs...,
)

// Create the transaction and broadcast it to the network. The
// transaction will be added to the database in order to ensure that we
// continue to re-broadcast the transaction upon restarts until it has
// been confirmed.
createdTx, err := w.CreateSimpleTx(
keyScope, account, outputs, minconf, satPerKb,
coinSelectionStrategy, false, WithCustomSelectUtxos(
selectedUtxos,
),
coinSelectionStrategy, false, allOpts...,
)
if err != nil {
return nil, err
Expand Down