Repository: decred/dcrd
Pull request #2330 (2020-08-15 11:47:41)

rpcserver: Add handleSearchRawTransactions tests.
This adds test coverage for `handleSearchRawTransactions` and `fetchMempoolTxnsForAddress`.

There is [one error case here](https://github.com/decred/dcrd/blob/3e0e466ac0d9d1632a43cb94451ddcf3fa9425dd/internal/rpcserver/rpcserver.go#L4054) that is unreachable, and the rest of these functions are covered:
```
go test -run="TestHandleSearchRawTransactions" -coverprofile=cov.out &&
go tool cover -func=cov.out | grep -E "handleSearchRawTransactions|fetchMempoolTxnsForAddress"

github.com/decred/dcrd/internal/rpcserver/rpcserver.go:3868:		fetchMempoolTxnsForAddress	100.0%
github.com/decred/dcrd/internal/rpcserver/rpcserver.go:3887:		handleSearchRawTransactions	99.2%
```

Part of https://github.com/decred/dcrd/issues/2069.


Patch:
diff --git a/docs/json_rpc_api.mediawiki b/docs/json_rpc_api.mediawiki
index 7ed9ee89..83ccebb0 100644
--- a/docs/json_rpc_api.mediawiki
+++ b/docs/json_rpc_api.mediawiki
@@ -2224,6 +2224,8 @@ of the best block.
 # <code>skip</code>: <code>(int, optional, default=0)</code> the number of leading transactions to leave out of the final response.
 # <code>count</code>: <code>(int, optional, default=100)</code> the maximum number of transactions to return.
 # <code>vinextra</code>: <code>(int, optional, default=0)</code> specify that extra data from previous output will be returned in vin.
+# <code>reverse</code>: <code>(boolean, optional, default=false)</code> specify that the transactions should be returned in reverse chronological order.
+# <code>filteraddrs</code>: <code>(json array of strings, optional)</code> specify that only inputs or outputs with matching addresses should be returned.
 |-
 !Description
 |Returns raw data for transactions involving the passed address. Returned transactions are pulled from both the database, and transactions currently in the mempool. Transactions pulled from the mempool will have the <code>"confirmations"</code> field set to 0. Usage of this RPC requires the optional <code>--addrindex</code> flag to be activated, otherwise all responses will simply return with an error stating the address index has not yet been built up. Similarly, until the address index has caught up with the current best height, all requests will return an error response in order to avoid serving stale data.
diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go
index 93bda8d6..a9f83cad 100644
--- a/internal/rpcserver/interface.go
+++ b/internal/rpcserver/interface.go
@@ -13,7 +13,9 @@ import (
 	"github.com/decred/dcrd/addrmgr"
 	"github.com/decred/dcrd/blockchain/stake/v3"
 	"github.com/decred/dcrd/blockchain/v3"
+	"github.com/decred/dcrd/blockchain/v3/indexers"
 	"github.com/decred/dcrd/chaincfg/chainhash"
+	"github.com/decred/dcrd/database/v2"
 	"github.com/decred/dcrd/dcrutil/v3"
 	"github.com/decred/dcrd/gcs/v2"
 	"github.com/decred/dcrd/internal/mempool"
@@ -595,3 +597,40 @@ type TxMempooler interface {
 	// and does not include orphans.
 	FetchTransaction(txHash *chainhash.Hash) (*dcrutil.Tx, error)
 }
+
+// AddrIndexer provides an interface for retrieving transactions for a given
+// address.
+//
+// The interface contract requires that all of these methods are safe for
+// concurrent access.
+type AddrIndexer interface {
+	// EntriesForAddress returns a slice of details which identify each transaction,
+	// including a block region, that involves the passed address according to the
+	// specified number to skip, number requested, and whether or not the results
+	// should be reversed.  It also returns the number actually skipped since it
+	// could be less in the case where there are not enough entries.
+	//
+	// NOTE: These results only include transactions confirmed in blocks.  See the
+	// UnconfirmedTxnsForAddress method for obtaining unconfirmed transactions
+	// that involve a given address.
+	EntriesForAddress(dbTx database.Tx, addr dcrutil.Address, numToSkip,
+		numRequested uint32, reverse bool) ([]indexers.TxIndexEntry, uint32, error)
+
+	// UnconfirmedTxnsForAddress returns all transactions currently in the
+	// unconfirmed (memory-only) address index that involve the passed address.
+	// Unsupported address types are ignored and will result in no results.
+	UnconfirmedTxnsForAddress(addr dcrutil.Address) []*dcrutil.Tx
+}
+
+// TxIndexer provides an interface for retrieving details for a given
+// transaction hash.
+//
+// The interface contract requires that all of these methods are safe for
+// concurrent access.
+type TxIndexer interface {
+	// Entry returns details for the provided transaction hash from the transaction
+	// index.  The block region contained in the result can in turn be used to load
+	// the raw transaction bytes.  When there is no entry for the provided hash, nil
+	// must be returned for the both the entry and the error.
+	Entry(hash *chainhash.Hash) (*indexers.TxIndexEntry, error)
+}
diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go
index 19fd9296..a0dd1abc 100644
--- a/internal/rpcserver/rpcserver.go
+++ b/internal/rpcserver/rpcserver.go
@@ -35,7 +35,6 @@ import (
 	"github.com/decred/dcrd/blockchain/stake/v3"
 	"github.com/decred/dcrd/blockchain/standalone/v2"
 	"github.com/decred/dcrd/blockchain/v3"
-	"github.com/decred/dcrd/blockchain/v3/indexers"
 	"github.com/decred/dcrd/chaincfg/chainhash"
 	"github.com/decred/dcrd/chaincfg/v3"
 	"github.com/decred/dcrd/database/v2"
@@ -2398,8 +2397,8 @@ func handleGetInfo(_ context.Context, s *Server, cmd interface{}) (interface{},
 		Difficulty:      getDifficultyRatio(best.Bits, s.cfg.ChainParams),
 		TestNet:         s.cfg.TestNet,
 		RelayFee:        s.cfg.MinRelayTxFee.ToCoin(),
-		AddrIndex:       s.cfg.AddrIndex != nil,
-		TxIndex:         s.cfg.TxIndex != nil,
+		AddrIndex:       s.cfg.AddrIndexer != nil,
+		TxIndex:         s.cfg.TxIndexer != nil,
 	}
 
 	return ret, nil
@@ -2737,15 +2736,14 @@ func handleGetRawTransaction(_ context.Context, s *Server, cmd interface{}) (int
 	var blkIndex uint32
 	tx, err := s.cfg.TxMempooler.FetchTransaction(txHash)
 	if err != nil {
-		txIndex := s.cfg.TxIndex
-		if txIndex == nil {
+		if s.cfg.TxIndexer == nil {
 			return nil, rpcInternalError("The transaction index "+
 				"must be enabled to query the blockchain "+
 				"(specify --txindex)", "Configuration")
 		}
 
 		// Look up the location of the transaction.
-		idxEntry, err := txIndex.Entry(txHash)
+		idxEntry, err := s.cfg.TxIndexer.Entry(txHash)
 		if err != nil {
 			context := "Failed to retrieve transaction location"
 			return nil, rpcInternalError(err.Error(), context)
@@ -3682,7 +3680,7 @@ func fetchInputTxos(s *Server, tx *wire.MsgTx) (map[wire.OutPoint]wire.TxOut, er
 		}
 
 		// Look up the location of the transaction.
-		idxEntry, err := s.cfg.TxIndex.Entry(&origin.Hash)
+		idxEntry, err := s.cfg.TxIndexer.Entry(&origin.Hash)
 		if err != nil {
 			context := "Failed to retrieve transaction location"
 			return nil, rpcInternalError(err.Error(), context)
@@ -3870,7 +3868,7 @@ func createVinListPrevOut(s *Server, mtx *wire.MsgTx, chainParams *chaincfg.Para
 func fetchMempoolTxnsForAddress(s *Server, addr dcrutil.Address, numToSkip, numRequested uint32) ([]*dcrutil.Tx, uint32) {
 	// There are no entries to return when there are less available than
 	// the number being skipped.
-	mpTxns := s.cfg.AddrIndex.UnconfirmedTxnsForAddress(addr)
+	mpTxns := s.cfg.AddrIndexer.UnconfirmedTxnsForAddress(addr)
 	numAvailable := uint32(len(mpTxns))
 	if numToSkip > numAvailable {
 		return nil, numAvailable
@@ -3888,8 +3886,7 @@ func fetchMempoolTxnsForAddress(s *Server, addr dcrutil.Address, numToSkip, numR
 // handleSearchRawTransactions implements the searchrawtransactions command.
 func handleSearchRawTransactions(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
 	// Respond with an error if the address index is not enabled.
-	addrIndex := s.cfg.AddrIndex
-	if addrIndex == nil {
+	if s.cfg.AddrIndexer == nil {
 		return nil, rpcInternalError("Address index must be "+
 			"enabled (--addrindex)", "Configuration")
 	}
@@ -3906,7 +3903,7 @@ func handleSearchRawTransactions(_ context.Context, s *Server, cmd interface{})
 	// transaction index.  Currently the address index relies on the
 	// transaction index, so this check is redundant, but it's better to be
 	// safe in case the address index is ever changed to not rely on it.
-	if vinExtra && s.cfg.TxIndex == nil {
+	if vinExtra && s.cfg.TxIndexer == nil {
 		return nil, rpcInternalError("Transaction index must be "+
 			"enabled (--txindex)", "Configuration")
 	}
@@ -3973,7 +3970,7 @@ func handleSearchRawTransactions(_ context.Context, s *Server, cmd interface{})
 	// are needed.
 	if len(addressTxns) < numRequested {
 		err = s.cfg.DB.View(func(dbTx database.Tx) error {
-			idxEntries, dbSkipped, err := addrIndex.EntriesForAddress(
+			idxEntries, dbSkipped, err := s.cfg.AddrIndexer.EntriesForAddress(
 				dbTx, addr, uint32(numToSkip)-numSkipped,
 				uint32(numRequested-len(addressTxns)), reverse)
 			if err != nil {
@@ -5641,10 +5638,12 @@ type Config struct {
 	BlockTemplater BlockTemplater
 	CPUMiner       CPUMiner
 
-	// These fields define any optional indexes the RPC server can make use
-	// of to provide additional data when queried.
-	TxIndex   *indexers.TxIndex
-	AddrIndex *indexers.AddrIndex
+	// TxIndexer defines the optional transaction indexer for the RPC server to
+	// use.
+	TxIndexer TxIndexer
+
+	// AddrIndexer defines the optional address indexer for the RPC server to use.
+	AddrIndexer AddrIndexer
 
 	// NetInfo defines a slice of the available networks.
 	NetInfo []types.NetworksResult
diff --git a/server.go b/server.go
index c6ec9d4f..8e388101 100644
--- a/server.go
+++ b/server.go
@@ -3289,9 +3289,19 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB, chainP
 				}
 				return &rpcBlockTemplater{s.bg}
 			}(),
-			CPUMiner:             &rpcCPUMiner{s.cpuMiner},
-			TxIndex:              s.txIndex,
-			AddrIndex:            s.addrIndex,
+			CPUMiner: &rpcCPUMiner{s.cpuMiner},
+			TxIndexer: func() rpcserver.TxIndexer {
+				if s.txIndex == nil {
+					return nil
+				}
+				return s.txIndex
+			}(),
+			AddrIndexer: func() rpcserver.AddrIndexer {
+				if s.addrIndex == nil {
+					return nil
+				}
+				return s.addrIndex
+			}(),
 			NetInfo:              cfg.generateNetworkInfo(),
 			MinRelayTxFee:        cfg.minRelayTxFee,
 			Proxy:                cfg.Proxy,


Test patch:
diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go
index 7ce70a65..ef1daca2 100644
--- a/internal/rpcserver/rpcserverhandlers_test.go
+++ b/internal/rpcserver/rpcserverhandlers_test.go
@@ -12,6 +12,7 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"math"
 	"math/big"
 	"net"
@@ -27,8 +28,10 @@ import (
 	"github.com/decred/dcrd/blockchain/stake/v3"
 	"github.com/decred/dcrd/blockchain/standalone/v2"
 	"github.com/decred/dcrd/blockchain/v3"
+	"github.com/decred/dcrd/blockchain/v3/indexers"
 	"github.com/decred/dcrd/chaincfg/chainhash"
 	"github.com/decred/dcrd/chaincfg/v3"
+	"github.com/decred/dcrd/database/v2"
 	"github.com/decred/dcrd/dcrjson/v3"
 	"github.com/decred/dcrd/dcrutil/v3"
 	"github.com/decred/dcrd/gcs/v2"
@@ -117,6 +120,7 @@ type testRPCChain struct {
 	blockHashByHeight               *chainhash.Hash
 	blockHashByHeightErr            error
 	blockHeightByHash               int64
+	blockHeightByHashErr            error
 	calcNextRequiredStakeDifficulty int64
 	calcWantHeight                  int64
 	chainTips                       []blockchain.ChainTipInfo
@@ -182,7 +186,7 @@ func (c *testRPCChain) BlockHashByHeight(height int64) (*chainhash.Hash, error)
 
 // BlockHeightByHash returns a mocked height of the block with the given hash.
 func (c *testRPCChain) BlockHeightByHash(hash *chainhash.Hash) (int64, error) {
-	return c.blockHeightByHash, nil
+	return c.blockHeightByHash, c.blockHeightByHashErr
 }
 
 // CalcNextRequiredStakeDifficulty returns a mocked required stake difficulty.
@@ -512,6 +516,172 @@ func (e *testExistsAddresser) ExistsAddresses(addrs []dcrutil.Address) ([]bool,
 	return e.existsAddresses, e.existsAddressesErr
 }
 
+// testAddrIndexer provides a mock address indexer by implementing the
+// AddrIndexer interface.
+type testAddrIndexer struct {
+	entriesForAddress         []indexers.TxIndexEntry
+	entriesForAddressSkipped  uint32
+	entriesForAddressErr      error
+	unconfirmedTxnsForAddress []*dcrutil.Tx
+}
+
+// EntriesForAddress returns a mocked slice of indexers.TxIndexEntry that
+// involve the given address.
+func (a *testAddrIndexer) EntriesForAddress(dbTx database.Tx,
+	addr dcrutil.Address, numToSkip, numRequested uint32, reverse bool) (
+	[]indexers.TxIndexEntry, uint32, error) {
+	return a.entriesForAddress, a.entriesForAddressSkipped, a.entriesForAddressErr
+}
+
+// UnconfirmedTxnsForAddress returns a mocked slice of transactions that are
+// currently in the unconfirmed (memory-only) address index.
+func (a *testAddrIndexer) UnconfirmedTxnsForAddress(addr dcrutil.Address) []*dcrutil.Tx {
+	return a.unconfirmedTxnsForAddress
+}
+
+// testTxIndexer provides a mock transaction indexer by implementing the
+// TxIndexer interface.
+type testTxIndexer struct {
+	entry func(hash *chainhash.Hash) (*indexers.TxIndexEntry, error)
+}
+
+// Entry returns mocked details for the provided transaction hash from the
+// transaction index.
+func (t *testTxIndexer) Entry(hash *chainhash.Hash) (*indexers.TxIndexEntry, error) {
+	return t.entry(hash)
+}
+
+// testDB provides a mock database by implementing the database.DB interface.
+type testDB struct {
+	dbType   string
+	beginTx  database.Tx
+	beginErr error
+	viewTx   database.Tx
+	updateTx database.Tx
+	closeErr error
+}
+
+// Type returns the mocked database driver type.
+func (d *testDB) Type() string {
+	return d.dbType
+}
+
+// Begin returns a mocked database transaction.
+func (d *testDB) Begin(writable bool) (database.Tx, error) {
+	return d.beginTx, d.beginErr
+}
+
+// View invokes the passed function in the context of a mocked read-only
+// database transaction.
+func (d *testDB) View(fn func(tx database.Tx) error) error {
+	return fn(d.viewTx)
+}
+
+// Update invokes the passed function in the context of a mocked read-write
+// database transaction.
+func (d *testDB) Update(fn func(tx database.Tx) error) error {
+	return fn(d.updateTx)
+}
+
+// Close provides a mock implementation for the shut down of the database.
+func (d *testDB) Close() error {
+	return d.closeErr
+}
+
+// testDatabaseTx provides a mock database transaction by implementing the
+// database.Tx interface.
+type testDatabaseTx struct {
+	metadata             database.Bucket
+	storeBlockErr        error
+	hasBlock             bool
+	hasBlockErr          error
+	hasBlocks            []bool
+	hasBlocksErr         error
+	fetchBlockHeader     []byte
+	fetchBlockHeaderErr  error
+	fetchBlockHeaders    [][]byte
+	fetchBlockHeadersErr error
+	fetchBlock           []byte
+	fetchBlockErr        error
+	fetchBlocks          [][]byte
+	fetchBlocksErr       error
+	fetchBlockRegion     func(region *database.BlockRegion) ([]byte, error)
+	fetchBlockRegions    func(regions []database.BlockRegion) ([][]byte, error)
+	commitErr            error
+	rollbackErr          error
+}
+
+// Metadata returns a mocked top-most bucket for all metadata storage.
+func (t *testDatabaseTx) Metadata() database.Bucket {
+	return t.metadata
+}
+
+// StoreBlock provides a mock implementation for storing the provided block
+// in the database.
+func (t *testDatabaseTx) StoreBlock(block database.BlockSerializer) error {
+	return t.storeBlockErr
+}
+
+// HasBlock returns a mocked bool representing whether or not a block with the
+// given hash exists in the database.
+func (t *testDatabaseTx) HasBlock(hash *chainhash.Hash) (bool, error) {
+	return t.hasBlock, t.hasBlockErr
+}
+
+// HasBlocks returns a mocked slice of bools representing whether or not the
+// blocks with the provided hashes exist in the database.
+func (t *testDatabaseTx) HasBlocks(hashes []chainhash.Hash) ([]bool, error) {
+	return t.hasBlocks, t.hasBlocksErr
+}
+
+// FetchBlockHeader returns mocked raw serialized bytes for the block header
+// identified by the given hash.
+func (t *testDatabaseTx) FetchBlockHeader(hash *chainhash.Hash) ([]byte, error) {
+	return t.fetchBlockHeader, t.fetchBlockHeaderErr
+}
+
+// FetchBlockHeaders returns mocked raw serialized bytes for the block headers
+// identified by the given hashes.
+func (t *testDatabaseTx) FetchBlockHeaders(hashes []chainhash.Hash) ([][]byte, error) {
+	return t.fetchBlockHeaders, t.fetchBlockHeadersErr
+}
+
+// FetchBlock returns mocked raw serialized bytes for the block identified by
+// the given hash.
+func (t *testDatabaseTx) FetchBlock(hash *chainhash.Hash) ([]byte, error) {
+	return t.fetchBlock, t.fetchBlockErr
+}
+
+// FetchBlocks returns mocked raw serialized bytes for the blocks identified by
+// the given hashes.
+func (t *testDatabaseTx) FetchBlocks(hashes []chainhash.Hash) ([][]byte, error) {
+	return t.fetchBlocks, t.fetchBlocksErr
+}
+
+// FetchBlockRegion returns mocked raw serialized bytes for the given block
+// region.
+func (t *testDatabaseTx) FetchBlockRegion(region *database.BlockRegion) ([]byte, error) {
+	return t.fetchBlockRegion(region)
+}
+
+// FetchBlockRegions returns mocked raw serialized bytes for the given block
+// regions.
+func (t *testDatabaseTx) FetchBlockRegions(regions []database.BlockRegion) ([][]byte, error) {
+	return t.fetchBlockRegions(regions)
+}
+
+// Commit provides a mock implementation for committing all changes that have
+// been made.
+func (t *testDatabaseTx) Commit() error {
+	return t.commitErr
+}
+
+// Rollback provides a mock implementation for undoing all changes that have
+// been made.
+func (t *testDatabaseTx) Rollback() error {
+	return t.rollbackErr
+}
+
 // testConnManager provides a mock connection manager by implementing the
 // ConnManager interface.
 type testConnManager struct {
@@ -893,6 +1063,19 @@ func hexToBytes(s string) []byte {
 	return b
 }
 
+// hexToMsgTx converts the passed hex string into a wire.MsgTx and will panic if
+// there is an error.  This is only provided for hard-coded constants so errors
+// in the source code can be detected.  It will only (and must only) be called
+// with hard-coded values.
+func hexToMsgTx(s string) *wire.MsgTx {
+	var msgTx wire.MsgTx
+	err := msgTx.Deserialize(bytes.NewReader(hexToBytes(s)))
+	if err != nil {
+		panic("invalid tx hex in source file: " + s)
+	}
+	return &msgTx
+}
+
 // cloneParams returns a deep copy of the provided parameters so the caller is
 // free to modify them without worrying about interfering with other tests.
 func cloneParams(params *chaincfg.Params) *chaincfg.Params {
@@ -926,6 +1109,17 @@ var block432100 = func() wire.MsgBlock {
 	return block
 }()
 
+// hexFromFile loads a hex string from a file and will panic if there is an
+// error.  This is provided for test files that contain hard-coded data.
+func hexFromFile(filename string) string {
+	filePath := filepath.Join(testDataPath, filename)
+	hex, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		panic(err)
+	}
+	return string(hex)
+}
+
 type rpcTest struct {
 	name                  string
 	handler               commandHandler
@@ -942,6 +1136,11 @@ type rpcTest struct {
 	mockSyncManager       *testSyncManager
 	mockExistsAddresser   *testExistsAddresser
 	setExistsAddresserNil bool
+	mockAddrIndexer       *testAddrIndexer
+	setAddrIndexerNil     bool
+	mockTxIndexer         *testTxIndexer
+	setTxIndexerNil       bool
+	mockDB                *testDB
 	mockConnManager       *testConnManager
 	mockClock             *testClock
 	mockLogManager        *testLogManager
@@ -1158,6 +1357,44 @@ func defaultMockExistsAddresser() *testExistsAddresser {
 	return &testExistsAddresser{}
 }
 
+// defaultMockAddrIndexer provides a default mock address indexer to be
+// used throughout the tests. Tests can override these defaults by calling
+// defaultMockAddrIndexer, updating fields as necessary on the returned
+// *testAddrIndexer, and then setting rpcTest.mockAddrIndexer as that
+// *testAddrIndexer.
+func defaultMockAddrIndexer() *testAddrIndexer {
+	return &testAddrIndexer{}
+}
+
+// defaultMockTxIndexer provides a default mock transaction indexer to be
+// used throughout the tests. Tests can override these defaults by calling
+// defaultMockTxIndexer, updating fields as necessary on the returned
+// *testTxIndexer, and then setting rpcTest.mockTxIndexer as that
+// *testTxIndexer.
+func defaultMockTxIndexer() *testTxIndexer {
+	return &testTxIndexer{}
+}
+
+// defaultMockDB provides a default mock database to be used throughout the
+// tests. Tests can override these defaults by calling defaultMockDB, updating
+// fields as necessary on the returned *testDB, and then setting rpcTest.mockDB
+// as that *testDB.
+func defaultMockDB() *testDB {
+	defaultDatabaseTx := &testDatabaseTx{
+		fetchBlockRegion: func(region *database.BlockRegion) ([]byte, error) {
+			return nil, errors.New("block not found")
+		},
+		fetchBlockRegions: func(regions []database.BlockRegion) ([][]byte, error) {
+			return nil, errors.New("block not found")
+		},
+	}
+	return &testDB{
+		beginTx:  defaultDatabaseTx,
+		viewTx:   defaultDatabaseTx,
+		updateTx: defaultDatabaseTx,
+	}
+}
+
 // defaultMockSyncManager provides a default mock sync manager to be used
 // throughout the tests. Tests can override these defaults by calling
 // defaultMockSyncManager, updating fields as necessary on the returned
@@ -1280,7 +1517,9 @@ func defaultMockCPUMiner() *testCPUMiner {
 // *testTxMempooler, and then setting rpcTest.mockTxMempooler as that
 // *testTxMempooler.
 func defaultMockTxMempooler() *testTxMempooler {
-	return &testTxMempooler{}
+	return &testTxMempooler{
+		fetchTransactionErr: errors.New("transaction is not in the pool"),
+	}
 }
 
 // defaultMockConfig provides a default Config that is used throughout
@@ -1295,6 +1534,9 @@ func defaultMockConfig(chainParams *chaincfg.Params) *Config {
 		FeeEstimator:    defaultMockFeeEstimator(),
 		SyncMgr:         defaultMockSyncManager(),
 		ExistsAddresser: defaultMockExistsAddresser(),
+		AddrIndexer:     defaultMockAddrIndexer(),
+		TxIndexer:       defaultMockTxIndexer(),
+		DB:              defaultMockDB(),
 		ConnMgr:         defaultMockConnManager(),
 		CPUMiner:        defaultMockCPUMiner(),
 		TxMempooler:     defaultMockTxMempooler(),
@@ -3878,8 +4120,8 @@ func TestHandleGetInfo(t *testing.T) {
 			Difficulty:      float64(28147398026.656624),
 			TestNet:         false,
 			RelayFee:        float64(0.0001),
-			AddrIndex:       false,
-			TxIndex:         false,
+			AddrIndex:       true,
+			TxIndex:         true,
 		},
 	}})
 }
@@ -4504,6 +4746,443 @@ func TestHandlePing(t *testing.T) {
 	}})
 }
 
+// testTx holds test transaction info and is used for mocking transaction
+// details for handleSearchRawTransactions.
+type testTx struct {
+	hex        string
+	indexEntry *indexers.TxIndexEntry
+	vinList    []testTx
+}
+
+// searchRawTransactionsResult returns a types.SearchRawTransactionsResult
+// for the given testTx and handleSearchRawTransactions parameters.  This uses
+// createVinListPrevOut and createVoutList, so ideally those should be tested
+// independently as well.
+func searchRawTransactionsResult(t *testing.T, s *Server, tx testTx,
+	vinExtra bool, filterAddr map[string]struct{},
+) types.SearchRawTransactionsResult {
+	msgTx := hexToMsgTx(tx.hex)
+	vinList, err := createVinListPrevOut(s, msgTx, defaultChainParams, vinExtra,
+		filterAddr)
+	if err != nil {
+		t.Fatalf("error creating vinList: %+v", err)
+	}
+	result := types.SearchRawTransactionsResult{
+		Hex:      tx.hex,
+		Txid:     msgTx.TxHash().String(),
+		Version:  int32(msgTx.Version),
+		LockTime: msgTx.LockTime,
+		Expiry:   msgTx.Expiry,
+		Vin:      vinList,
+		Vout:     createVoutList(msgTx, defaultChainParams, filterAddr),
+	}
+	if tx.indexEntry != nil {
+		result.BlockHash = tx.indexEntry.BlockRegion.Hash.String()
+		result.BlockHeight = int64(block432100.Header.Height)
+		result.BlockIndex = tx.indexEntry.BlockIndex
+		result.Confirmations = uint64(1)
+		result.Time = block432100.Header.Timestamp.Unix()
+		result.Blocktime = block432100.Header.Timestamp.Unix()
+	}
+	return result
+}
+
+func TestHandleSearchRawTransactions(t *testing.T) {
+	t.Parallel()
+
+	// Create a testTx that exists in a block.
+	tx0TestTx := testTx{
+		hex: hexFromFile("tx432098-11.hex"),
+		indexEntry: &indexers.TxIndexEntry{
+			BlockRegion: database.BlockRegion{
+				Hash: mustParseHash("00000000000000001fc4c4c7a3f2ec6d552dda16a3a928f27bd6" +
+					"bd16d8f1e9b3"),
+				Offset: 52508,
+				Len:    453,
+			},
+			BlockIndex: 11,
+		},
+	}
+	tx0TestTx.vinList = []testTx{{
+		hex: hexFromFile("tx431841-1.hex"),
+		indexEntry: &indexers.TxIndexEntry{
+			BlockRegion: database.BlockRegion{
+				Hash: mustParseHash("00000000000000000a5f89c78fd0136d6efb065f5a9b8e763df2" +
+					"62fcbb64a93c"),
+				Offset: 55568,
+				Len:    420,
+			},
+			BlockIndex: 1,
+		},
+	}, {
+		hex: hexFromFile("tx432083-2.hex"),
+		indexEntry: &indexers.TxIndexEntry{
+			BlockRegion: database.BlockRegion{
+				Hash: mustParseHash("00000000000000000bcc2bff8a7d978e1ed9b7763e17402cff3e" +
+					"58cb4a1d3aa3"),
+				Offset: 3405,
+				Len:    453,
+			},
+			BlockIndex: 2,
+		},
+	}}
+
+	// Create a testTx that exists in the mempool.
+	tx1TestTx := testTx{
+		hex: hexFromFile("tx432100-1.hex"),
+	}
+	tx1TestTx.vinList = []testTx{{
+		hex: hexFromFile("tx431843-1.hex"),
+		indexEntry: &indexers.TxIndexEntry{
+			BlockRegion: database.BlockRegion{
+				Hash: mustParseHash("0000000000000000107e144664020dc7f503de2f34c5a297e7bf" +
+					"b8ab7ce73ee1"),
+				Offset: 2226,
+				Len:    419,
+			},
+			BlockIndex: 1,
+		},
+	}, {
+		hex: hexFromFile("tx432098-11.hex"),
+		indexEntry: &indexers.TxIndexEntry{
+			BlockRegion: database.BlockRegion{
+				Hash: mustParseHash("00000000000000001fc4c4c7a3f2ec6d552dda16a3a928f27bd6" +
+					"bd16d8f1e9b3"),
+				Offset: 52508,
+				Len:    453,
+			},
+			BlockIndex: 11,
+		},
+	}}
+
+	// Define the test address and testTxs that involve that address.
+	address := "Dsi8CRt85xYyempXs7ZPL1rBxvDdAGZmgsg"
+	testTxs := []testTx{
+		tx0TestTx,
+		tx1TestTx,
+	}
+
+	// Mock AddrIndexer, TxIndexer, and DB methods based on the testTxs.
+	entriesForAddress := make([]indexers.TxIndexEntry, 0, len(testTxs))
+	unconfirmedTxnsForAddress := make([]*dcrutil.Tx, 0, len(testTxs))
+	txIndexEntryMap := make(map[chainhash.Hash]indexers.TxIndexEntry)
+	fetchBlockRegionMap := make(map[database.BlockRegion][]byte)
+	fetchBlockRegions := make([][]byte, 0, len(testTxs))
+	for _, testTx := range testTxs {
+		msgTx := hexToMsgTx(testTx.hex)
+		if testTx.indexEntry != nil {
+			entriesForAddress = append(entriesForAddress, *testTx.indexEntry)
+			fetchBlockRegions = append(fetchBlockRegions, hexToBytes(testTx.hex))
+		} else {
+			unconfirmedTxnsForAddress = append(unconfirmedTxnsForAddress,
+				dcrutil.NewTx(msgTx))
+		}
+		for _, vinTestTx := range testTx.vinList {
+			vinMtx := hexToMsgTx(vinTestTx.hex)
+			txIndexEntryMap[vinMtx.TxHash()] = *vinTestTx.indexEntry
+			blockRegion := vinTestTx.indexEntry.BlockRegion
+			fetchBlockRegionMap[blockRegion] = hexToBytes(vinTestTx.hex)
+		}
+	}
+	addrIndexer := defaultMockAddrIndexer()
+	addrIndexer.entriesForAddress = entriesForAddress
+	addrIndexer.unconfirmedTxnsForAddress = unconfirmedTxnsForAddress
+	txIndexer := defaultMockTxIndexer()
+	txIndexer.entry = func(hash *chainhash.Hash) (*indexers.TxIndexEntry, error) {
+		txIndexEntry := txIndexEntryMap[*hash]
+		return &txIndexEntry, nil
+	}
+	db := defaultMockDB()
+	db.viewTx = &testDatabaseTx{
+		fetchBlockRegion: func(region *database.BlockRegion) ([]byte, error) {
+			return fetchBlockRegionMap[*region], nil
+		},
+		fetchBlockRegions: func(regions []database.BlockRegion) ([][]byte, error) {
+			return fetchBlockRegions, nil
+		},
+	}
+
+	// Set result and filteredResult to the results that are expected.
+	result := make([]types.SearchRawTransactionsResult, len(testTxs))
+	filteredResult := make([]types.SearchRawTransactionsResult, len(testTxs))
+	vinExtra := true
+	filterAddr := map[string]struct{}{address: {}}
+	s := &Server{cfg: *defaultMockConfig(defaultChainParams)}
+	s.cfg.AddrIndexer = addrIndexer
+	s.cfg.DB = db
+	s.cfg.TxIndexer = txIndexer
+	for i, tx := range testTxs {
+		result[i] = searchRawTransactionsResult(t, s, tx, vinExtra, nil)
+		filteredResult[i] = searchRawTransactionsResult(t, s, tx, vinExtra,
+			filterAddr)
+	}
+
+	testRPCServerHandler(t, []rpcTest{{
+		name:    "handleSearchRawTransactions: ok",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(0),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		result: []string{
+			tx0TestTx.hex,
+			tx1TestTx.hex,
+		},
+	}, {
+		name:    "handleSearchRawTransactions: ok verbose",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		mockTxIndexer:   txIndexer,
+		result:          result,
+	}, {
+		name:    "handleSearchRawTransactions: ok verbose with addr filter",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(1),
+			FilterAddrs: &[]string{
+				address,
+			},
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		mockTxIndexer:   txIndexer,
+		result:          filteredResult,
+	}, {
+		name:    "handleSearchRawTransactions: ok with count < 0",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(0),
+			Count:    dcrjson.Int(-1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		result: []string{
+			tx0TestTx.hex,
+		},
+	}, {
+		name:    "handleSearchRawTransactions: ok with skip < 0",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(0),
+			Skip:     dcrjson.Int(-1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		result: []string{
+			tx0TestTx.hex,
+			tx1TestTx.hex,
+		},
+	}, {
+		name:    "handleSearchRawTransactions: ok with skip > available mempool txns",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(0),
+			Skip:     dcrjson.Int(2),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		result: []string{
+			tx0TestTx.hex,
+		},
+	}, {
+		name:    "handleSearchRawTransactions: ok reversed",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(0),
+			Reverse:  dcrjson.Bool(true),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		result: []string{
+			tx1TestTx.hex,
+			tx0TestTx.hex,
+		},
+	}, {
+		name:    "handleSearchRawTransactions: address index not enabled",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+		},
+		setAddrIndexerNil: true,
+		wantErr:           true,
+		errCode:           dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: transaction index not enabled",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+		},
+		setTxIndexerNil: true,
+		wantErr:         true,
+		errCode:         dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: invalid address",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  "invalid",
+			VinExtra: dcrjson.Int(1),
+		},
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInvalidAddressOrKey,
+	}, {
+		name:    "handleSearchRawTransactions: count == 0",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Count:    dcrjson.Int(0),
+		},
+		result: nil,
+	}, {
+		name:    "handleSearchRawTransactions: entries for address err",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+		},
+		mockAddrIndexer: func() *testAddrIndexer {
+			addrIndexer := defaultMockAddrIndexer()
+			addrIndexer.entriesForAddressErr = errors.New("entries for address err")
+			return addrIndexer
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: fetch block regions err",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+		},
+		mockDB: func() *testDB {
+			db := defaultMockDB()
+			db.viewTx = &testDatabaseTx{
+				fetchBlockRegions: func(regions []database.BlockRegion) ([][]byte, error) {
+					return nil, errors.New("fetch block regions err")
+				},
+			}
+			return db
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: no txns available",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+		},
+		mockDB: func() *testDB {
+			db := defaultMockDB()
+			db.viewTx = &testDatabaseTx{
+				fetchBlockRegions: func(regions []database.BlockRegion) ([][]byte, error) {
+					return [][]byte{}, nil
+				},
+			}
+			return db
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: failed to deserialize transaction",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB: func() *testDB {
+			db := defaultMockDB()
+			db.viewTx = &testDatabaseTx{
+				fetchBlockRegions: func(regions []database.BlockRegion) ([][]byte, error) {
+					return [][]byte{hexToBytes("bad1")}, nil
+				},
+			}
+			return db
+		}(),
+		mockTxIndexer: txIndexer,
+		wantErr:       true,
+		errCode:       dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: block not found",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		mockTxIndexer:   txIndexer,
+		mockChain: func() *testRPCChain {
+			chain := defaultMockRPCChain()
+			chain.headerByHashErr = errors.New("block not found")
+			return chain
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCBlockNotFound,
+	}, {
+		name:    "handleSearchRawTransactions: failed to obtain block height",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		mockTxIndexer:   txIndexer,
+		mockChain: func() *testRPCChain {
+			chain := defaultMockRPCChain()
+			chain.blockHeightByHashErr = errors.New("failed to obtain block height")
+			return chain
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleSearchRawTransactions: failed to retrieve tx location",
+		handler: handleSearchRawTransactions,
+		cmd: &types.SearchRawTransactionsCmd{
+			Address:  address,
+			VinExtra: dcrjson.Int(1),
+			Verbose:  dcrjson.Int(1),
+		},
+		mockAddrIndexer: addrIndexer,
+		mockDB:          db,
+		mockTxIndexer: func() *testTxIndexer {
+			txIndexer := defaultMockTxIndexer()
+			txIndexer.entry = func(hash *chainhash.Hash) (*indexers.TxIndexEntry, error) {
+				return nil, errors.New("failed to retrieve tx location")
+			}
+			return txIndexer
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}})
+}
+
 func TestHandleSubmitBlock(t *testing.T) {
 	t.Parallel()
 
@@ -4986,6 +5665,21 @@ func testRPCServerHandler(t *testing.T, tests []rpcTest) {
 			if test.setExistsAddresserNil {
 				rpcserverConfig.ExistsAddresser = nil
 			}
+			if test.mockAddrIndexer != nil {
+				rpcserverConfig.AddrIndexer = test.mockAddrIndexer
+			}
+			if test.setAddrIndexerNil {
+				rpcserverConfig.AddrIndexer = nil
+			}
+			if test.mockTxIndexer != nil {
+				rpcserverConfig.TxIndexer = test.mockTxIndexer
+			}
+			if test.setTxIndexerNil {
+				rpcserverConfig.TxIndexer = nil
+			}
+			if test.mockDB != nil {
+				rpcserverConfig.DB = test.mockDB
+			}
 			if test.mockConnManager != nil {
 				rpcserverConfig.ConnMgr = test.mockConnManager
 			}
diff --git a/internal/rpcserver/testdata/tx431841-1.hex b/internal/rpcserver/testdata/tx431841-1.hex
new file mode 100644
index 00000000..ab07cf10
--- /dev/null
+++ b/internal/rpcserver/testdata/tx431841-1.hex
@@ -0,0 +1,1 @@
+01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff76813944ac024cda9f99fb34403356a4bd4ea396807cfebb7648ab3d4db9ff1d0000000001ffffffff0400000000000000000000266a24b93a17ff14be64756dac12b4985f7f5a86e27749981c8e270000000000000000e096060000000000000000000000086a060100070000001b460b000000000000001abb76a914bee010b0d6369633d6fb12d67559440e2146c8db88ac772383e90200000000001abb76a914b4f547c307543cb91b9169e3ee11b9034061d20188ac000000000000000002fd378f050000000000000000ffffffff0200009631ffe302000000cc7c06000700000091483045022100e4d4a2a2ee86074560905938040183be815a3aa1002e5aded8208687f9bee54e02205aaf1f4c7013548fb5e6c8b0cabaeda7a0c2b64d081ba5f494913a2fceb6a66c014751210291b56f0c534e3754c51ec881fa2034fd228ad2d36f94ec37a34975b1235135cf2103e861db04dec874e59e489885170edfcb35d363adcaf2631c7d54dc09b10a451852ae
\ No newline at end of file
diff --git a/internal/rpcserver/testdata/tx431843-1.hex b/internal/rpcserver/testdata/tx431843-1.hex
new file mode 100644
index 00000000..80c5069b
--- /dev/null
+++ b/internal/rpcserver/testdata/tx431843-1.hex
@@ -0,0 +1,1 @@
+01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff8a5ed6e6eb9be6b6e3d26f3294540087096229a26eafedec5c4f43c4a8606c5c0000000001ffffffff0400000000000000000000266a24c827d0958f34b1d19842c6710f78d5e9708ea6ce4e582a120000000000000000e296060000000000000000000000086a060100070000001c460b000000000000001abb76a914bee010b0d6369633d6fb12d67559440e2146c8db88acc8f1e3d10200000000001abb76a9144ad2bfb09f0faceb9b96cdb65a9d164a3d6ae0b888ac000000000000000002fd378f050000000000000000ffffffff020000e8ff5fcc02000000987f0600100000009047304402206db57d3c70cefbe006b45a440568a5822b72d46273448602ea0d06d9395fb36102203a56a1ffc3280b8f9a7400902e1642e0960ef33d9d690f82cf48202f6484bd1c014751210291b56f0c534e3754c51ec881fa2034fd228ad2d36f94ec37a34975b1235135cf2103e861db04dec874e59e489885170edfcb35d363adcaf2631c7d54dc09b10a451852ae
\ No newline at end of file
diff --git a/internal/rpcserver/testdata/tx432083-2.hex b/internal/rpcserver/testdata/tx432083-2.hex
new file mode 100644
index 00000000..88db95f7
--- /dev/null
+++ b/internal/rpcserver/testdata/tx432083-2.hex
@@ -0,0 +1,1 @@
+010000000297d75063280ff985f7027329f21d7a5a98faf19f16dc76d9d1bf3985831f859f0400000000ffffffffe632d52177a2ea85c5f8b7ea01e029215f1313a487814008abbde4d2db5e25830300000001ffffffff0353170b000000000000001976a914f0a9c1af516719a7a76c433a061ecbce7ae196e788acf747f15b0300000000001976a914f0a9c1af516719a7a76c433a061ecbce7ae196e788ac53d4cf070200000000001976a9141c30d05fc919a7be49dc000f47025d1477cb24d588ac0000000000000000023a741cbd01000000c5970600040000006b483045022100fcee8a83c7c5e4496127d6415cad05c1e8b3d9af8147d497cdd31f909b3feab302207e3ef314374c96bb3f1140a9e7288b62bec874cac1b558a26f6f05fb08943de3012103505b972163bf0faa7f0c0439233913e70eb2d08d375ae586a13984da51e3e32129d1afa603000000d2960600010000006b4830450221008b952f025082a43927a66e55c72d7ed6f20d3b6fd073ae2feabf912fed68b2a302202b70023a7c2051a5e5e9fd5a44b1650549aa0e9affbc750c4a38e5190a5b806c012103fa80f5c013283f5e97acfabe0842afb84b310f53d5a6ac0807a0f2489cb2bc49
\ No newline at end of file
diff --git a/internal/rpcserver/testdata/tx432098-11.hex b/internal/rpcserver/testdata/tx432098-11.hex
new file mode 100644
index 00000000..21088771
--- /dev/null
+++ b/internal/rpcserver/testdata/tx432098-11.hex
@@ -0,0 +1,1 @@
+0100000002b761292042421b09196a2a9cdf56001a95df8c508dacf1170bba8b0c813fc8210300000001ffffffffdca0b996c9078ed14749774aba78d6e1df78e29486deb59d1894ed16a53b74080200000000ffffffff0353170b000000000000001976a914762432e9619f5ddaf122ac663684152ffe9eb0ec88acf747f15b0300000000001976a914762432e9619f5ddaf122ac663684152ffe9eb0ec88acba8656950100000000001976a914bc3c059489f447afbf542ff33432adb9ded7f8e988ac000000000000000002772383e902000000e1960600010000006b483045022100ba5b20f9148273717deba544348f0595750e12cb57d7a818914c66453c9dfb930220486feb328c8f171cce5cfbf88382114282f9041ba53209a12ad48ac86ceb8eb2012102b9ff45cb72132bdf41cf97e96afa90102a4c96ef11fafe43e01983050880aab953d4cf0702000000d3970600020000006b483045022100b29ff29f99ae5b8e3fad72efe6dd13968b7d1c6b3fe2e0fb7eb7656cf7de75f202200888c36da42a0cc948770e3be29b0c75465096bc47cbac27f7c19be7b33287760121039e58379edbbc239e965d7715a9834f6870d9e5e6bf7ebae8d12a30f7282ea5a5
\ No newline at end of file
diff --git a/internal/rpcserver/testdata/tx432100-1.hex b/internal/rpcserver/testdata/tx432100-1.hex
new file mode 100644
index 00000000..247c50ae
--- /dev/null
+++ b/internal/rpcserver/testdata/tx432100-1.hex
@@ -0,0 +1,1 @@
+010000000283067d34267d265ebb85cd83ca30bfd7ec01a8c90828d8945c30bcbb62841e5d0300000001ffffffffd5946c5856ba3d62422df236cd155a53fcf8badb7c605838e145331e99b820c70200000000ffffffff0353170b000000000000001976a9140bb407413316b66d7648f4d938d701159b34dfd688acf747f15b0300000000001976a9140bb407413316b66d7648f4d938d701159b34dfd688ac72073e0b0100000000001976a914065660dc3514f60a08fb32c37b2fa39863dd053788ac000000000000000002c8f1e3d102000000e3960600010000006a4730440220141d93135fdecd27c9e78178a79eed674df2e1086a21487226bbb6cb7500a63102201be67161b2f72496627e89735ff3cbd44c25f5aae4fd82d230ca41bd32b397ab01210202142453cda983d5adfa6c5b015604c1d55f74a3f9eb3196235a3b19cf208d1aba86569501000000e29706000b0000006a47304402203f0968d5605ca45bf9a5c4dbf7b0a8c6ed9568d0d728d24eb070e3f4e971be330220070353a48feb56dee1c2677aee8d78c2b978f5bb5c10d39e57b4c6aa02d88f3301210399d10781e598f7551ce3b8e6feb494c82b3a1d0e01b377eddb52fa75d13c5e78
\ No newline at end of file