diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index c0badf35d7..61b1d92c55 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -213,6 +213,47 @@ func addUtxo(t *testing.T, w *Wallet, incomingTx *wire.MsgTx) { } } +// addTxAndCredit adds the given transaction to the wallet's database marked as +// a confirmed UTXO specified by the creditIndex. +func addTxAndCredit(t *testing.T, w *Wallet, tx *wire.MsgTx, + creditIndex uint32) { + + var b bytes.Buffer + require.NoError(t, tx.Serialize(&b), "unable to serialize tx") + + txBytes := b.Bytes() + + rec, err := wtxmgr.NewTxRecord(txBytes, time.Now()) + require.NoError(t, err) + + // The block meta will be inserted to tell the wallet this is a + // confirmed transaction. + block := &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: *testBlockHash, + Height: testBlockHeight, + }, + Time: time.Unix(1387737310, 0), + } + + err = walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { + ns := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) + err = w.TxStore.InsertTx(ns, rec, block) + if err != nil { + return err + } + + // Add the specified output as credit. + err = w.TxStore.AddCredit(ns, rec, block, creditIndex, false) + if err != nil { + return err + } + + return nil + }) + require.NoError(t, err, "failed inserting tx") +} + // TestInputYield verifies the functioning of the inputYieldsPositively. func TestInputYield(t *testing.T) { t.Parallel() diff --git a/wallet/utxos.go b/wallet/utxos.go index 9365f24f80..efe12bc925 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) var ( @@ -174,6 +175,12 @@ func (w *Wallet) FetchOutpointInfo(prevOut *wire.OutPoint) (*wire.MsgTx, numOutputs) } + // Exit early if the output doesn't belong to our wallet. We know it's + // our UTXO iff the `TxDetails` has a credit record on this output. + if !hasOutput(txDetail, prevOut.Index) { + return nil, nil, 0, ErrNotMine + } + pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript // Determine the number of confirmations the output currently has. @@ -224,3 +231,19 @@ func (w *Wallet) FetchDerivationInfo(pkScript []byte) (*psbt.Bip32Derivation, return derivation, nil } + +// hasOutpoint takes an output identified by its output index and determines +// whether the TxDetails contains this output. If the TxDetails doesn't have +// this output, it means this output doesn't belong to our wallet. +// +// TODO(yy): implement this method on `TxDetails` and update the package +// `wtxmgr` instead. +func hasOutput(t *wtxmgr.TxDetails, outputIndex uint32) bool { + for _, cred := range t.Credits { + if outputIndex == cred.Index { + return true + } + } + + return false +} diff --git a/wallet/utxos_test.go b/wallet/utxos_test.go index 28797f4da1..bbd60195ea 100644 --- a/wallet/utxos_test.go +++ b/wallet/utxos_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" @@ -129,6 +130,84 @@ func TestFetchOutpointInfo(t *testing.T) { require.Equal(t, int64(0-testBlockHeight), confirmations) } +// TestFetchOutpointInfoErr checks when the wallet cannot find an output, a +// proper error is returned. +func TestFetchOutpointInfoErr(t *testing.T) { + t.Parallel() + + w, cleanup := testWallet(t) + defer cleanup() + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + require.NoError(t, err) + p2shAddr, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + // Create a tx that has two outputs - output1 belongs to the wallet, + // output2 is external. + output1 := wire.NewTxOut(100000, p2shAddr) + output2 := wire.NewTxOut(100000, p2shAddr) + tx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{ + output1, + output2, + }, + } + + // Add the tx and its first output as the credit. + addTxAndCredit(t, w, tx, 0) + + testCases := []struct { + name string + prevOut *wire.OutPoint + + // TODO(yy): refator `FetchOutpointInfo` to return wrapped + // errors. + errExpected string + }{ + { + name: "no tx details", + prevOut: &wire.OutPoint{ + Hash: chainhash.Hash{1, 2, 3}, + Index: 0, + }, + errExpected: "does not belong to the wallet", + }, + { + name: "invalid output index", + prevOut: &wire.OutPoint{ + Hash: tx.TxHash(), + Index: 1000, + }, + errExpected: "invalid output index", + }, + { + name: "no credit found", + prevOut: &wire.OutPoint{ + Hash: tx.TxHash(), + Index: 1, + }, + errExpected: "does not belong to the wallet", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Look up the UTXO for the outpoint now and compare it + // to the expected error. + tx, out, conf, err := w.FetchOutpointInfo(tc.prevOut) + require.ErrorContains(t, err, tc.errExpected) + require.Nil(t, tx) + require.Nil(t, out) + require.Zero(t, conf) + }) + } +} + // TestFetchDerivationInfo checks that the wallet can gather the derivation // info about an output based on the pkScript. func TestFetchDerivationInfo(t *testing.T) {