diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 90441e6e..c16e97ab 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -549,12 +549,13 @@ func NewReconsiderBlockCmd(blockHash string) *ReconsiderBlockCmd { // SearchRawTransactionsCmd defines the searchrawtransactions JSON-RPC command. type SearchRawTransactionsCmd struct { - Address string - Verbose *int `jsonrpcdefault:"1"` - Skip *int `jsonrpcdefault:"0"` - Count *int `jsonrpcdefault:"100"` - VinExtra *int `jsonrpcdefault:"0"` - Reverse *bool `jsonrpcdefault:"false"` + Address string + Verbose *int `jsonrpcdefault:"1"` + Skip *int `jsonrpcdefault:"0"` + Count *int `jsonrpcdefault:"100"` + VinExtra *int `jsonrpcdefault:"0"` + Reverse *bool `jsonrpcdefault:"false"` + FilterAddrs *[]string } // NewSearchRawTransactionsCmd returns a new instance which can be used to issue a @@ -562,14 +563,15 @@ type SearchRawTransactionsCmd struct { // // The parameters which are pointers indicate they are optional. Passing nil // for optional parameters will use the default value. -func NewSearchRawTransactionsCmd(address string, verbose, skip, count *int, vinExtra *int, reverse *bool) *SearchRawTransactionsCmd { +func NewSearchRawTransactionsCmd(address string, verbose, skip, count *int, vinExtra *int, reverse *bool, filterAddrs *[]string) *SearchRawTransactionsCmd { return &SearchRawTransactionsCmd{ - Address: address, - Verbose: verbose, - Skip: skip, - Count: count, - VinExtra: vinExtra, - Reverse: reverse, + Address: address, + Verbose: verbose, + Skip: skip, + Count: count, + VinExtra: vinExtra, + Reverse: reverse, + FilterAddrs: filterAddrs, } } diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index 527bc0ba..5fed0ccc 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -700,16 +700,17 @@ func TestChainSvrCmds(t *testing.T) { return btcjson.NewCmd("searchrawtransactions", "1Address") }, staticCmd: func() interface{} { - return btcjson.NewSearchRawTransactionsCmd("1Address", nil, nil, nil, nil, nil) + return btcjson.NewSearchRawTransactionsCmd("1Address", nil, nil, nil, nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address"],"id":1}`, unmarshalled: &btcjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: btcjson.Int(1), - Skip: btcjson.Int(0), - Count: btcjson.Int(100), - VinExtra: btcjson.Int(0), - Reverse: btcjson.Bool(false), + Address: "1Address", + Verbose: btcjson.Int(1), + Skip: btcjson.Int(0), + Count: btcjson.Int(100), + VinExtra: btcjson.Int(0), + Reverse: btcjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -719,16 +720,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return btcjson.NewSearchRawTransactionsCmd("1Address", - btcjson.Int(0), nil, nil, nil, nil) + btcjson.Int(0), nil, nil, nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0],"id":1}`, unmarshalled: &btcjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: btcjson.Int(0), - Skip: btcjson.Int(0), - Count: btcjson.Int(100), - VinExtra: btcjson.Int(0), - Reverse: btcjson.Bool(false), + Address: "1Address", + Verbose: btcjson.Int(0), + Skip: btcjson.Int(0), + Count: btcjson.Int(100), + VinExtra: btcjson.Int(0), + Reverse: btcjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -738,16 +740,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return btcjson.NewSearchRawTransactionsCmd("1Address", - btcjson.Int(0), btcjson.Int(5), nil, nil, nil) + btcjson.Int(0), btcjson.Int(5), nil, nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5],"id":1}`, unmarshalled: &btcjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: btcjson.Int(0), - Skip: btcjson.Int(5), - Count: btcjson.Int(100), - VinExtra: btcjson.Int(0), - Reverse: btcjson.Bool(false), + Address: "1Address", + Verbose: btcjson.Int(0), + Skip: btcjson.Int(5), + Count: btcjson.Int(100), + VinExtra: btcjson.Int(0), + Reverse: btcjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -757,16 +760,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return btcjson.NewSearchRawTransactionsCmd("1Address", - btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), nil, nil) + btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10],"id":1}`, unmarshalled: &btcjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: btcjson.Int(0), - Skip: btcjson.Int(5), - Count: btcjson.Int(10), - VinExtra: btcjson.Int(0), - Reverse: btcjson.Bool(false), + Address: "1Address", + Verbose: btcjson.Int(0), + Skip: btcjson.Int(5), + Count: btcjson.Int(10), + VinExtra: btcjson.Int(0), + Reverse: btcjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -776,16 +780,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return btcjson.NewSearchRawTransactionsCmd("1Address", - btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), btcjson.Int(1), nil) + btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), btcjson.Int(1), nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10,1],"id":1}`, unmarshalled: &btcjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: btcjson.Int(0), - Skip: btcjson.Int(5), - Count: btcjson.Int(10), - VinExtra: btcjson.Int(1), - Reverse: btcjson.Bool(false), + Address: "1Address", + Verbose: btcjson.Int(0), + Skip: btcjson.Int(5), + Count: btcjson.Int(10), + VinExtra: btcjson.Int(1), + Reverse: btcjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -795,16 +800,37 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return btcjson.NewSearchRawTransactionsCmd("1Address", - btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), btcjson.Int(1), btcjson.Bool(true)) + btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), btcjson.Int(1), btcjson.Bool(true), nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10,1,true],"id":1}`, unmarshalled: &btcjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: btcjson.Int(0), - Skip: btcjson.Int(5), - Count: btcjson.Int(10), - VinExtra: btcjson.Int(1), - Reverse: btcjson.Bool(true), + Address: "1Address", + Verbose: btcjson.Int(0), + Skip: btcjson.Int(5), + Count: btcjson.Int(10), + VinExtra: btcjson.Int(1), + Reverse: btcjson.Bool(true), + FilterAddrs: nil, + }, + }, + { + name: "searchrawtransactions", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("searchrawtransactions", "1Address", 0, 5, 10, 1, true, []string{"1Address"}) + }, + staticCmd: func() interface{} { + return btcjson.NewSearchRawTransactionsCmd("1Address", + btcjson.Int(0), btcjson.Int(5), btcjson.Int(10), btcjson.Int(1), btcjson.Bool(true), &[]string{"1Address"}) + }, + marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10,1,true,["1Address"]],"id":1}`, + unmarshalled: &btcjson.SearchRawTransactionsCmd{ + Address: "1Address", + Verbose: btcjson.Int(0), + Skip: btcjson.Int(5), + Count: btcjson.Int(10), + VinExtra: btcjson.Int(1), + Reverse: btcjson.Bool(true), + FilterAddrs: &[]string{"1Address"}, }, }, { diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 6ba33d27..29d4c096 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -400,7 +400,7 @@ type TxRawResult struct { // SearchRawTransactionsResult models the data from the searchrawtransaction // command. type SearchRawTransactionsResult struct { - Hex string `json:"hex"` + Hex string `json:"hex,omitempty"` Txid string `json:"txid"` Version int32 `json:"version"` LockTime uint32 `json:"locktime"` diff --git a/rpcserver.go b/rpcserver.go index e4365ad0..60a758e4 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -664,22 +664,41 @@ func createVinList(mtx *wire.MsgTx) []btcjson.Vin { return vinList } +// stringInSlice returns true if string a is found in array list. +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + // createVinList returns a slice of JSON objects for the inputs of the passed // transaction. -func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.Params, vinExtra int) []btcjson.VinPrevOut { +func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.Params, vinExtra int, filterAddrMap map[string]struct{}) []btcjson.VinPrevOut { + // We use a dynamically sized list to accomodate address filter. + vinList := make([]btcjson.VinPrevOut, 0, len(mtx.TxIn)) + // Coinbase transactions only have a single txin by definition. - vinList := make([]btcjson.VinPrevOut, len(mtx.TxIn)) if blockchain.IsCoinBaseTx(mtx) { + // include tx only if filterAddrMap is empty because coinbase has no address + // and so would never match a non-empty filter. + if len(filterAddrMap) != 0 { + return vinList + } + var vinEntry btcjson.VinPrevOut txIn := mtx.TxIn[0] - vinList[0].Coinbase = hex.EncodeToString(txIn.SignatureScript) - vinList[0].Sequence = txIn.Sequence + vinEntry.Coinbase = hex.EncodeToString(txIn.SignatureScript) + vinEntry.Sequence = txIn.Sequence + vinList = append(vinList, vinEntry) return vinList } // Lookup all of the referenced transactions needed to populate the // previous output information if requested. var txStore blockchain.TxStore - if vinExtra != 0 { + if vinExtra != 0 || len(filterAddrMap) > 0 { tx := btcutil.NewTx(mtx) txStoreNew, err := s.server.txMemPool.fetchInputTransactions(tx, true) if err == nil { @@ -687,26 +706,15 @@ func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.P } } - for i, txIn := range mtx.TxIn { + for _, txIn := range mtx.TxIn { + // reset filter flag for each. + passesFilter := len(filterAddrMap) == 0 + // The disassembled string will contain [error] inline // if the script doesn't fully parse, so ignore the // error here. disbuf, _ := txscript.DisasmString(txIn.SignatureScript) - vinEntry := &vinList[i] - vinEntry.Txid = txIn.PreviousOutPoint.Hash.String() - vinEntry.Vout = txIn.PreviousOutPoint.Index - vinEntry.Sequence = txIn.Sequence - vinEntry.ScriptSig = &btcjson.ScriptSig{ - Asm: disbuf, - Hex: hex.EncodeToString(txIn.SignatureScript), - } - - // Only populate previous output information if requested and - // available. - if vinExtra == 0 || len(txStore) == 0 { - continue - } txData := txStore[txIn.PreviousOutPoint.Hash] if txData == nil { continue @@ -717,20 +725,42 @@ func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.P // Ignore the error here since an error means the script // couldn't parse and there is no additional information about // it anyways. - var strAddrs []string _, addrs, _, _ := txscript.ExtractPkScriptAddrs( originTxOut.PkScript, chainParams) - if addrs != nil { - strAddrs = make([]string, len(addrs)) - for j, addr := range addrs { - strAddrs[j] = addr.EncodeAddress() + + encodedAddrs := make([]string, len(addrs)) + for j, addr := range addrs { + encodedAddrs[j] = addr.EncodeAddress() + + if len(filterAddrMap) > 0 { + if _, exists := filterAddrMap[encodedAddrs[j]]; exists { + passesFilter = true + } } } - vinEntry.PrevOut = &btcjson.PrevOut{ - Addresses: strAddrs, - Value: btcutil.Amount(originTxOut.Value).ToBTC(), + if !passesFilter { + continue } + + var vinEntry btcjson.VinPrevOut + vinEntry.Txid = txIn.PreviousOutPoint.Hash.String() + vinEntry.Vout = txIn.PreviousOutPoint.Index + vinEntry.Sequence = txIn.Sequence + vinEntry.ScriptSig = &btcjson.ScriptSig{ + Asm: disbuf, + Hex: hex.EncodeToString(txIn.SignatureScript), + } + + // Only populate previous output information if requested + if vinExtra != 0 { + vinEntry.PrevOut = &btcjson.PrevOut{ + Addresses: encodedAddrs, + Value: btcutil.Amount(originTxOut.Value).ToBTC(), + } + } + + vinList = append(vinList, vinEntry) } return vinList @@ -738,34 +768,47 @@ func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.P // createVoutList returns a slice of JSON objects for the outputs of the passed // transaction. -func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params) []btcjson.Vout { - voutList := make([]btcjson.Vout, len(mtx.TxOut)) +func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params, filterAddrMap map[string]struct{}) []btcjson.Vout { + voutList := make([]btcjson.Vout, 0, len(mtx.TxOut)) for i, v := range mtx.TxOut { - voutList[i].N = uint32(i) - voutList[i].Value = btcutil.Amount(v.Value).ToBTC() + // reset filter flag for each. + passesFilter := len(filterAddrMap) == 0 // The disassembled string will contain [error] inline if the // script doesn't fully parse, so ignore the error here. disbuf, _ := txscript.DisasmString(v.PkScript) - voutList[i].ScriptPubKey.Asm = disbuf - voutList[i].ScriptPubKey.Hex = hex.EncodeToString(v.PkScript) // Ignore the error here since an error means the script // couldn't parse and there is no additional information about // it anyways. scriptClass, addrs, reqSigs, _ := txscript.ExtractPkScriptAddrs( v.PkScript, chainParams) - voutList[i].ScriptPubKey.Type = scriptClass.String() - voutList[i].ScriptPubKey.ReqSigs = int32(reqSigs) - if addrs == nil { - voutList[i].ScriptPubKey.Addresses = nil - } else { - voutList[i].ScriptPubKey.Addresses = make([]string, len(addrs)) - for j, addr := range addrs { - voutList[i].ScriptPubKey.Addresses[j] = addr.EncodeAddress() + encodedAddrs := make([]string, len(addrs)) + for j, addr := range addrs { + encodedAddrs[j] = addr.EncodeAddress() + + if len(filterAddrMap) > 0 { + if _, exists := filterAddrMap[encodedAddrs[j]]; exists { + passesFilter = true + } } } + + if !passesFilter { + continue + } + + var vout btcjson.Vout + vout.N = uint32(i) + vout.Value = btcutil.Amount(v.Value).ToBTC() + vout.ScriptPubKey.Addresses = encodedAddrs + vout.ScriptPubKey.Asm = disbuf + vout.ScriptPubKey.Hex = hex.EncodeToString(v.PkScript) + vout.ScriptPubKey.Type = scriptClass.String() + vout.ScriptPubKey.ReqSigs = int32(reqSigs) + + voutList = append(voutList, vout) } return voutList @@ -775,18 +818,24 @@ func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params) []btcjson.Vou // to a raw transaction JSON object, possibly with vin.PrevOut section. func createSearchRawTransactionsResult(s *rpcServer, chainParams *chaincfg.Params, mtx *wire.MsgTx, txHash string, blkHeader *wire.BlockHeader, blkHash string, - blkHeight int32, chainHeight int32, vinExtra int) (*btcjson.SearchRawTransactionsResult, error) { + blkHeight int32, chainHeight int32, vinExtra int, filterAddrMap map[string]struct{}) (*btcjson.SearchRawTransactionsResult, error) { - mtxHex, err := messageToHex(mtx) - if err != nil { - return nil, err + // omit hex if filterAddrMap are present. When filtering, typically the + // goal is to reduce unnecessary bloat in the result. + var mtxHex string + if len(filterAddrMap) == 0 { + mtxHexTmp, err := messageToHex(mtx) + if err != nil { + return nil, err + } + mtxHex = mtxHexTmp } txReply := &btcjson.SearchRawTransactionsResult{ Hex: mtxHex, Txid: txHash, - Vout: createVoutList(mtx, chainParams), - Vin: createVinListPrevOut(s, mtx, chainParams, vinExtra), + Vout: createVoutList(mtx, chainParams, filterAddrMap), + Vin: createVinListPrevOut(s, mtx, chainParams, vinExtra, filterAddrMap), Version: mtx.Version, LockTime: mtx.LockTime, } @@ -816,7 +865,7 @@ func createTxRawResult(chainParams *chaincfg.Params, mtx *wire.MsgTx, txReply := &btcjson.TxRawResult{ Hex: mtxHex, Txid: txHash, - Vout: createVoutList(mtx, chainParams), + Vout: createVoutList(mtx, chainParams, nil), Vin: createVinList(mtx), Version: mtx.Version, LockTime: mtx.LockTime, @@ -861,7 +910,7 @@ func handleDecodeRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan Version: mtx.Version, Locktime: mtx.LockTime, Vin: createVinList(&mtx), - Vout: createVoutList(&mtx, s.server.chainParams), + Vout: createVoutList(&mtx, s.server.chainParams, nil), } return txReply, nil } @@ -3175,8 +3224,17 @@ func handleSearchRawTransactions(s *rpcServer, cmd interface{}, closeChan <-chan blkHeight = txReply.Height } + // c.FilterAddrs can be nil, empty or non-empty. Here we normalize that + // to a non-nil array (empty or non-empty) to avoid future nil checks. + filterAddrMap := make(map[string]struct{}) + if c.FilterAddrs != nil && len(*c.FilterAddrs) > 0 { + for _, addr := range *c.FilterAddrs { + filterAddrMap[addr] = struct{}{} + } + } + rawTxn, err := createSearchRawTransactionsResult(s, s.server.chainParams, mtx, - txHash, blkHeader, blkHashStr, blkHeight, maxIdx, *c.VinExtra) + txHash, blkHeader, blkHashStr, blkHeight, maxIdx, *c.VinExtra, filterAddrMap) if err != nil { return nil, err } diff --git a/rpcserverhelp.go b/rpcserverhelp.go index d9b0b305..1973b527 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -472,6 +472,7 @@ var helpDescsEnUS = map[string]string{ "searchrawtransactions-count": "The maximum number of transactions to return", "searchrawtransactions-vinextra": "Specify that extra data from previous output will be returned in vin", "searchrawtransactions-reverse": "Specifies that the transactions should be returned in reverse chronological order", + "searchrawtransactions-filteraddrs": "Address list. Only inputs or outputs with matching address will be returned", "searchrawtransactions--result0": "Hex-encoded serialized transaction", // SendRawTransactionCmd help.