diff --git a/integration/chain_test.go b/integration/chain_test.go new file mode 100644 index 00000000..0f5cd94c --- /dev/null +++ b/integration/chain_test.go @@ -0,0 +1,146 @@ +//go:build rpctest +// +build rpctest + +package integration + +import ( + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/require" +) + +// TestGetTxSpendingPrevOut checks that `GetTxSpendingPrevOut` behaves as +// expected. +// - an error is returned when invalid params are used. +// - orphan tx is rejected. +// - fee rate above the max is rejected. +// - a mixed of both allowed and rejected can be returned in the same response. +func TestGetTxSpendingPrevOut(t *testing.T) { + t.Parallel() + + // Boilerplate codetestDir to make a pruned node. + btcdCfg := []string{"--rejectnonstd", "--debuglevel=debug"} + r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") + require.NoError(t, err) + + // Setup the node. + require.NoError(t, r.SetUp(true, 100)) + t.Cleanup(func() { + require.NoError(t, r.TearDown()) + }) + + // Create a tx and testing outpoints. + tx := createTxInMempool(t, r) + opInMempool := tx.TxIn[0].PreviousOutPoint + opNotInMempool := wire.OutPoint{ + Hash: tx.TxHash(), + Index: 0, + } + + testCases := []struct { + name string + outpoints []wire.OutPoint + expectedErr error + expectedResult []*btcjson.GetTxSpendingPrevOutResult + }{ + { + // When no outpoints are provided, the method should + // return an error. + name: "empty outpoints", + expectedErr: rpcclient.ErrInvalidParam, + expectedResult: nil, + }, + { + // When there are outpoints provided, check the + // expceted results are returned. + name: "outpoints", + outpoints: []wire.OutPoint{ + opInMempool, opNotInMempool, + }, + expectedErr: nil, + expectedResult: []*btcjson.GetTxSpendingPrevOutResult{ + { + Txid: opInMempool.Hash.String(), + Vout: opInMempool.Index, + SpendingTxid: tx.TxHash().String(), + }, + { + Txid: opNotInMempool.Hash.String(), + Vout: opNotInMempool.Index, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + results, err := r.Client.GetTxSpendingPrevOut( + tc.outpoints, + ) + + require.ErrorIs(err, tc.expectedErr) + require.Len(results, len(tc.expectedResult)) + + // Check each item is returned as expected. + for i, r := range results { + e := tc.expectedResult[i] + + require.Equal(e.Txid, r.Txid) + require.Equal(e.Vout, r.Vout) + require.Equal(e.SpendingTxid, r.SpendingTxid) + } + }) + } +} + +// createTxInMempool creates a tx and puts it in the mempool. +func createTxInMempool(t *testing.T, r *rpctest.Harness) *wire.MsgTx { + // Create a fresh output for usage within the test below. + const outputValue = btcutil.SatoshiPerBitcoin + outputKey, testOutput, testPkScript, err := makeTestOutput( + r, t, outputValue, + ) + require.NoError(t, err) + + // Create a new transaction with a lock-time past the current known + // MTP. + tx := wire.NewMsgTx(1) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *testOutput, + }) + + // Fetch a fresh address from the harness, we'll use this address to + // send funds back into the Harness. + addr, err := r.NewAddress() + require.NoError(t, err) + + addrScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + tx.AddTxOut(&wire.TxOut{ + PkScript: addrScript, + Value: outputValue - 1000, + }) + + sigScript, err := txscript.SignatureScript( + tx, 0, testPkScript, txscript.SigHashAll, outputKey, true, + ) + require.NoError(t, err) + tx.TxIn[0].SignatureScript = sigScript + + // Send the tx. + _, err = r.Client.SendRawTransaction(tx, true) + require.NoError(t, err) + + return tx +} diff --git a/rpcclient/rawtransactions.go b/rpcclient/rawtransactions.go index a6830219..4737aee5 100644 --- a/rpcclient/rawtransactions.go +++ b/rpcclient/rawtransactions.go @@ -1012,3 +1012,71 @@ func (c *Client) TestMempoolAccept(txns []*wire.MsgTx, return c.TestMempoolAcceptAsync(txns, maxFeeRate).Receive() } + +// FutureGetTxSpendingPrevOut is a future promise to deliver the result of a +// GetTxSpendingPrevOut RPC invocation (or an applicable error). +type FutureGetTxSpendingPrevOut chan *Response + +// Receive waits for the Response promised by the future and returns the +// response from GetTxSpendingPrevOut. +func (r FutureGetTxSpendingPrevOut) Receive() ( + []*btcjson.GetTxSpendingPrevOutResult, error) { + + response, err := ReceiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal as an array of GetTxSpendingPrevOutResult items. + var results []*btcjson.GetTxSpendingPrevOutResult + + err = json.Unmarshal(response, &results) + if err != nil { + return nil, err + } + + return results, nil +} + +// GetTxSpendingPrevOutAsync returns an instance of a type that can be used to +// get the result of the RPC at some future time by invoking the Receive +// function on the returned instance. +// +// See GetTxSpendingPrevOut for the blocking version and more details. +func (c *Client) GetTxSpendingPrevOutAsync( + outpoints []wire.OutPoint) FutureGetTxSpendingPrevOut { + + // Due to differences in the testmempoolaccept API for different + // backends, we'll need to inspect our version and construct the + // appropriate request. + version, err := c.BackendVersion() + if err != nil { + return newFutureError(err) + } + + log.Debugf("GetTxSpendingPrevOutAsync: backend version %s", version) + + // Exit early if the version is below 24.0.0. + if version < BitcoindPre24 { + err := fmt.Errorf("%w: %v", ErrBitcoindVersion, version) + return newFutureError(err) + } + + // Exit early if an empty array of outpoints is provided. + if len(outpoints) == 0 { + err := fmt.Errorf("%w: no outpoints provided", ErrInvalidParam) + return newFutureError(err) + } + + cmd := btcjson.NewGetTxSpendingPrevOutCmd(outpoints) + + return c.SendCmd(cmd) +} + +// GetTxSpendingPrevOut returns the result from calling `gettxspendingprevout` +// RPC. +func (c *Client) GetTxSpendingPrevOut(outpoints []wire.OutPoint) ( + []*btcjson.GetTxSpendingPrevOutResult, error) { + + return c.GetTxSpendingPrevOutAsync(outpoints).Receive() +}