diff --git a/dcrjson/chainsvrcmds.go b/dcrjson/chainsvrcmds.go index 921bed53..7cc2e462 100644 --- a/dcrjson/chainsvrcmds.go +++ b/dcrjson/chainsvrcmds.go @@ -587,12 +587,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 @@ -600,14 +601,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/dcrjson/chainsvrcmds_test.go b/dcrjson/chainsvrcmds_test.go index 8cfb0943..972654d8 100644 --- a/dcrjson/chainsvrcmds_test.go +++ b/dcrjson/chainsvrcmds_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2014 The btcsuite developers -// Copyright (c) 2015 The Decred developers +// Copyright (c) 2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -714,16 +714,17 @@ func TestChainSvrCmds(t *testing.T) { return dcrjson.NewCmd("searchrawtransactions", "1Address") }, staticCmd: func() interface{} { - return dcrjson.NewSearchRawTransactionsCmd("1Address", nil, nil, nil, nil, nil) + return dcrjson.NewSearchRawTransactionsCmd("1Address", nil, nil, nil, nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address"],"id":1}`, unmarshalled: &dcrjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: dcrjson.Int(1), - Skip: dcrjson.Int(0), - Count: dcrjson.Int(100), - VinExtra: dcrjson.Int(0), - Reverse: dcrjson.Bool(false), + Address: "1Address", + Verbose: dcrjson.Int(1), + Skip: dcrjson.Int(0), + Count: dcrjson.Int(100), + VinExtra: dcrjson.Int(0), + Reverse: dcrjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -733,16 +734,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return dcrjson.NewSearchRawTransactionsCmd("1Address", - dcrjson.Int(0), nil, nil, nil, nil) + dcrjson.Int(0), nil, nil, nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0],"id":1}`, unmarshalled: &dcrjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: dcrjson.Int(0), - Skip: dcrjson.Int(0), - Count: dcrjson.Int(100), - VinExtra: dcrjson.Int(0), - Reverse: dcrjson.Bool(false), + Address: "1Address", + Verbose: dcrjson.Int(0), + Skip: dcrjson.Int(0), + Count: dcrjson.Int(100), + VinExtra: dcrjson.Int(0), + Reverse: dcrjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -752,16 +754,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return dcrjson.NewSearchRawTransactionsCmd("1Address", - dcrjson.Int(0), dcrjson.Int(5), nil, nil, nil) + dcrjson.Int(0), dcrjson.Int(5), nil, nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5],"id":1}`, unmarshalled: &dcrjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: dcrjson.Int(0), - Skip: dcrjson.Int(5), - Count: dcrjson.Int(100), - VinExtra: dcrjson.Int(0), - Reverse: dcrjson.Bool(false), + Address: "1Address", + Verbose: dcrjson.Int(0), + Skip: dcrjson.Int(5), + Count: dcrjson.Int(100), + VinExtra: dcrjson.Int(0), + Reverse: dcrjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -771,16 +774,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return dcrjson.NewSearchRawTransactionsCmd("1Address", - dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), nil, nil) + dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), nil, nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10],"id":1}`, unmarshalled: &dcrjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: dcrjson.Int(0), - Skip: dcrjson.Int(5), - Count: dcrjson.Int(10), - VinExtra: dcrjson.Int(0), - Reverse: dcrjson.Bool(false), + Address: "1Address", + Verbose: dcrjson.Int(0), + Skip: dcrjson.Int(5), + Count: dcrjson.Int(10), + VinExtra: dcrjson.Int(0), + Reverse: dcrjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -790,16 +794,17 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return dcrjson.NewSearchRawTransactionsCmd("1Address", - dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), dcrjson.Int(1), nil) + dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), dcrjson.Int(1), nil, nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10,1],"id":1}`, unmarshalled: &dcrjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: dcrjson.Int(0), - Skip: dcrjson.Int(5), - Count: dcrjson.Int(10), - VinExtra: dcrjson.Int(1), - Reverse: dcrjson.Bool(false), + Address: "1Address", + Verbose: dcrjson.Int(0), + Skip: dcrjson.Int(5), + Count: dcrjson.Int(10), + VinExtra: dcrjson.Int(1), + Reverse: dcrjson.Bool(false), + FilterAddrs: nil, }, }, { @@ -809,16 +814,39 @@ func TestChainSvrCmds(t *testing.T) { }, staticCmd: func() interface{} { return dcrjson.NewSearchRawTransactionsCmd("1Address", - dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), dcrjson.Int(1), dcrjson.Bool(true)) + dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), + dcrjson.Int(1), dcrjson.Bool(true), nil) }, marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10,1,true],"id":1}`, unmarshalled: &dcrjson.SearchRawTransactionsCmd{ - Address: "1Address", - Verbose: dcrjson.Int(0), - Skip: dcrjson.Int(5), - Count: dcrjson.Int(10), - VinExtra: dcrjson.Int(1), - Reverse: dcrjson.Bool(true), + Address: "1Address", + Verbose: dcrjson.Int(0), + Skip: dcrjson.Int(5), + Count: dcrjson.Int(10), + VinExtra: dcrjson.Int(1), + Reverse: dcrjson.Bool(true), + FilterAddrs: nil, + }, + }, + { + name: "searchrawtransactions", + newCmd: func() (interface{}, error) { + return dcrjson.NewCmd("searchrawtransactions", "1Address", 0, 5, 10, 1, true, []string{"1Address"}) + }, + staticCmd: func() interface{} { + return dcrjson.NewSearchRawTransactionsCmd("1Address", + dcrjson.Int(0), dcrjson.Int(5), dcrjson.Int(10), + dcrjson.Int(1), dcrjson.Bool(true), &[]string{"1Address"}) + }, + marshalled: `{"jsonrpc":"1.0","method":"searchrawtransactions","params":["1Address",0,5,10,1,true,["1Address"]],"id":1}`, + unmarshalled: &dcrjson.SearchRawTransactionsCmd{ + Address: "1Address", + Verbose: dcrjson.Int(0), + Skip: dcrjson.Int(5), + Count: dcrjson.Int(10), + VinExtra: dcrjson.Int(1), + Reverse: dcrjson.Bool(true), + FilterAddrs: &[]string{"1Address"}, }, }, { diff --git a/dcrjson/chainsvrresults.go b/dcrjson/chainsvrresults.go index 31038f7a..7eb3ed29 100644 --- a/dcrjson/chainsvrresults.go +++ b/dcrjson/chainsvrresults.go @@ -457,7 +457,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 33703a1c..233c5b0c 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1355,22 +1355,43 @@ func createVinList(mtx *wire.MsgTx) []dcrjson.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) []dcrjson.VinPrevOut { +func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.Params, + vinExtra int, filterAddrMap map[string]struct{}) []dcrjson.VinPrevOut { + + // We use a dynamically sized list to accomodate address filter. + vinList := make([]dcrjson.VinPrevOut, 0, len(mtx.TxIn)) + // Coinbase transactions only have a single txin by definition. - vinList := make([]dcrjson.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 dcrjson.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 := dcrutil.NewTx(mtx) txStoreNew, err := s.server.txMemPool.fetchInputTransactions(tx, true) if err == nil { @@ -1378,13 +1399,43 @@ 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] + txData := txStore[txIn.PreviousOutPoint.Hash] + if txData == nil { + continue + } + + originTxOut := txData.Tx.MsgTx().TxOut[txIn.PreviousOutPoint.Index] + + // Ignore the error here since an error means the script + // couldn't parse and there is no additional information about + // it anyways. + _, addrs, _, _ := txscript.ExtractPkScriptAddrs( + txscript.DefaultScriptVersion, originTxOut.PkScript, chainParams) + 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 vinEntry dcrjson.VinPrevOut vinEntry.Txid = txIn.PreviousOutPoint.Hash.String() vinEntry.Vout = txIn.PreviousOutPoint.Index vinEntry.Tree = txIn.PreviousOutPoint.Tree @@ -1405,35 +1456,15 @@ func createVinListPrevOut(s *rpcServer, mtx *wire.MsgTx, chainParams *chaincfg.P 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 - } - - originTxOut := txData.Tx.MsgTx().TxOut[txIn.PreviousOutPoint.Index] - - // 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( - txscript.DefaultScriptVersion, originTxOut.PkScript, chainParams) - if addrs != nil { - strAddrs = make([]string, len(addrs)) - for j, addr := range addrs { - strAddrs[j] = addr.EncodeAddress() + // Only populate previous output information if requested + if vinExtra != 0 { + vinEntry.PrevOut = &dcrjson.PrevOut{ + Addresses: encodedAddrs, + Value: dcrutil.Amount(originTxOut.Value).ToCoin(), } } - vinEntry.PrevOut = &dcrjson.PrevOut{ - Addresses: strAddrs, - Value: dcrutil.Amount(originTxOut.Value).ToCoin(), - } + vinList = append(vinList, vinEntry) } return vinList @@ -1441,62 +1472,86 @@ 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) []dcrjson.Vout { +func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params, + filterAddrMap map[string]struct{}) []dcrjson.Vout { + txType := stake.DetermineTxType(dcrutil.NewTx(mtx)) - voutList := make([]dcrjson.Vout, len(mtx.TxOut)) + voutList := make([]dcrjson.Vout, 0, len(mtx.TxOut)) for i, v := range mtx.TxOut { - voutList[i].N = uint32(i) - voutList[i].Value = dcrutil.Amount(v.Value).ToCoin() - voutList[i].Version = v.Version + // 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.Version, v.PkScript, chainParams) - voutList[i].ScriptPubKey.Type = scriptClass.String() - voutList[i].ScriptPubKey.ReqSigs = int32(reqSigs) - - if addrs == nil { - voutList[i].ScriptPubKey.Addresses = nil - - // Decode commitment outputs for tickets and set the - // type correctly. - if txType == stake.TxTypeSStx && (i > 0) && (i%2 != 0) { - addr, err := stake.AddrFromSStxPkScrCommitment(v.PkScript, - chainParams) - if err != nil { - rpcsLog.Warnf("failed to decode ticket commitment addr "+ - "output for tx hash %v, output idx %v", mtx.TxSha(), - i) - continue - } - amt, err := stake.AmountFromSStxPkScrCommitment(v.PkScript) - if err != nil { - rpcsLog.Warnf("failed to decode ticket commitment amt "+ - "output for tx hash %v, output idx %v", mtx.TxSha(), - i) - continue - } - amtCoin := amt.ToCoin() - - voutList[i].ScriptPubKey.Type = sstxCommitmentString - voutList[i].ScriptPubKey.Addresses = make([]string, 1) - voutList[i].ScriptPubKey.Addresses[0] = addr.EncodeAddress() - voutList[i].ScriptPubKey.CommitAmt = &amtCoin + // Attempt to extract addresses from the public key script. In + // the case of stake submission transactions, the odd outputs + // contain a commitment address, so detect that case + // accordingly. + var addrs []dcrutil.Address + var scriptClass string + var reqSigs int + var commitAmt *dcrutil.Amount + if txType == stake.TxTypeSStx && (i%2 != 0) { + scriptClass = sstxCommitmentString + addr, err := stake.AddrFromSStxPkScrCommitment(v.PkScript, + chainParams) + if err != nil { + rpcsLog.Warnf("failed to decode ticket "+ + "commitment addr output for tx hash "+ + "%v, output idx %v", mtx.TxSha(), i) + } else { + addrs = []dcrutil.Address{addr} + } + amt, err := stake.AmountFromSStxPkScrCommitment(v.PkScript) + if err != nil { + rpcsLog.Warnf("failed to decode ticket "+ + "commitment amt output for tx hash %v"+ + ", output idx %v", mtx.TxSha(), i) + } else { + commitAmt = &amt } } else { - voutList[i].ScriptPubKey.Addresses = make([]string, len(addrs)) - for j, addr := range addrs { - voutList[i].ScriptPubKey.Addresses[j] = addr.EncodeAddress() + // Ignore the error here since an error means the script + // couldn't parse and there is no additional information + // about it anyways. + var sc txscript.ScriptClass + sc, addrs, reqSigs, _ = txscript.ExtractPkScriptAddrs( + v.Version, v.PkScript, chainParams) + scriptClass = sc.String() + } + + 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 dcrjson.Vout + voutSPK := &vout.ScriptPubKey + vout.N = uint32(i) + vout.Value = dcrutil.Amount(v.Value).ToCoin() + vout.Version = v.Version + voutSPK.Addresses = encodedAddrs + voutSPK.Asm = disbuf + voutSPK.Hex = hex.EncodeToString(v.PkScript) + voutSPK.Type = scriptClass + voutSPK.ReqSigs = int32(reqSigs) + if commitAmt != nil { + voutSPK.CommitAmt = dcrjson.Float64(commitAmt.ToCoin()) + } + + voutList = append(voutList, vout) } return voutList @@ -1506,19 +1561,25 @@ func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params) []dcrjson.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 int64, chainHeight int64, - vinExtra int) (*dcrjson.SearchRawTransactionsResult, error) { + blkHeight int64, chainHeight int64, vinExtra int, + filterAddrMap map[string]struct{}) (*dcrjson.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 := &dcrjson.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, } @@ -1549,7 +1610,7 @@ func createTxRawResult(chainParams *chaincfg.Params, mtx *wire.MsgTx, txReply := &dcrjson.TxRawResult{ Hex: mtxHex, Txid: txHash, - Vout: createVoutList(mtx, chainParams), + Vout: createVoutList(mtx, chainParams, nil), Vin: createVinList(mtx), Version: mtx.Version, LockTime: mtx.LockTime, @@ -1598,7 +1659,7 @@ func handleDecodeRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan Locktime: mtx.LockTime, Expiry: mtx.Expiry, Vin: createVinList(&mtx), - Vout: createVoutList(&mtx, s.server.chainParams), + Vout: createVoutList(&mtx, s.server.chainParams, nil), } return txReply, nil } @@ -4835,8 +4896,18 @@ func handleSearchRawTransactions(s *rpcServer, cmd interface{}, 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) + mtx, txHash, blkHeader, blkHashStr, blkHeight, maxIdx, + *c.VinExtra, filterAddrMap) if err != nil { return nil, err } diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 1d16839d..448d05d0 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -592,6 +592,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.