diff --git a/wallet/utxos.go b/wallet/utxos.go index a754f99..a29d094 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -6,11 +6,22 @@ package wallet import ( + "errors" + "fmt" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" ) +var ( + // ErrNotMine is an error denoting that a Wallet instance is unable to + // spend a specified output. + ErrNotMine = errors.New("the passed output does not belong to the " + + "wallet") +) + // OutputSelectionPolicy describes the rules for selecting an output from the // wallet. type OutputSelectionPolicy struct { @@ -88,3 +99,73 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut }) return outputResults, err } + +// FetchInputInfo queries for the wallet's knowledge of the passed outpoint. If +// the wallet determines this output is under its control, then the original +// full transaction, the target txout and the number of confirmations are +// returned. Otherwise, a non-nil error value of ErrNotMine is returned instead. +func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, + *wire.TxOut, int64, error) { + + // We manually look up the output within the tx store. + txid := &prevOut.Hash + txDetail, err := UnstableAPI(w).TxDetails(txid) + if err != nil { + return nil, nil, 0, err + } else if txDetail == nil { + return nil, nil, 0, ErrNotMine + } + + // With the output retrieved, we'll make an additional check to ensure + // we actually have control of this output. We do this because the check + // above only guarantees that the transaction is somehow relevant to us, + // like in the event of us being the sender of the transaction. + numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut)) + if prevOut.Index >= numOutputs { + return nil, nil, 0, fmt.Errorf("invalid output index %v for "+ + "transaction with %v outputs", prevOut.Index, + numOutputs) + } + pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript + if _, err := w.fetchOutputAddr(pkScript); err != nil { + return nil, nil, 0, err + } + + // Determine the number of confirmations the output currently has. + _, currentHeight, err := w.chainClient.GetBestBlock() + if err != nil { + return nil, nil, 0, fmt.Errorf("unable to retrieve current "+ + "height: %v", err) + } + confs := int64(0) + if txDetail.Block.Height != -1 { + confs = int64(currentHeight - txDetail.Block.Height) + } + + return &txDetail.TxRecord.MsgTx, &wire.TxOut{ + Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value, + PkScript: pkScript, + }, confs, nil +} + +// fetchOutputAddr attempts to fetch the managed address corresponding to the +// passed output script. This function is used to look up the proper key which +// should be used to sign a specified input. +func (w *Wallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.chainParams) + if err != nil { + return nil, err + } + + // If the case of a multi-sig output, several address may be extracted. + // Therefore, we simply select the key for the first address we know + // of. + for _, addr := range addrs { + addr, err := w.AddressInfo(addr) + if err == nil { + return addr, nil + } + } + + return nil, ErrNotMine +} diff --git a/wallet/utxos_test.go b/wallet/utxos_test.go new file mode 100644 index 0000000..3181c80 --- /dev/null +++ b/wallet/utxos_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// TestFetchInputInfo checks that the wallet can gather information about an +// output based on the address. +func TestFetchInputInfo(t *testing.T) { + w, cleanup := testWallet(t) + defer cleanup() + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2shAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2sh: %v", err) + } + + // Add an output paying to the wallet's address to the database. + utxOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{utxOut}, + } + addUtxo(t, w, incomingTx) + + // Look up the UTXO for the outpoint now and compare it to our + // expectations. + prevOut := &wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 0, + } + tx, out, confirmations, err := w.FetchInputInfo(prevOut) + if err != nil { + t.Fatalf("error fetching input info: %v", err) + } + if !bytes.Equal(out.PkScript, utxOut.PkScript) || out.Value != utxOut.Value { + t.Fatalf("unexpected TX out, got %v wanted %v", out, utxOut) + } + if !bytes.Equal(tx.TxOut[prevOut.Index].PkScript, utxOut.PkScript) { + t.Fatalf("unexpected TX out, got %v wanted %v", + tx.TxOut[prevOut.Index].PkScript, utxOut) + } + if confirmations != int64(0-testBlockHeight) { + t.Fatalf("unexpected number of confirmations, got %d wanted %d", + confirmations, 0-testBlockHeight) + } +}