github.com/aychain/blockbook@v0.1.1-0.20181121092459-6d1fc7e07c5b/server/socketio.go (about) 1 package server 2 3 import ( 4 "blockbook/api" 5 "blockbook/bchain" 6 "blockbook/common" 7 "blockbook/db" 8 "encoding/json" 9 "math/big" 10 "net/http" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/golang/glog" 16 "github.com/juju/errors" 17 "github.com/martinboehm/golang-socketio" 18 "github.com/martinboehm/golang-socketio/transport" 19 ) 20 21 // SocketIoServer is handle to SocketIoServer 22 type SocketIoServer struct { 23 server *gosocketio.Server 24 db *db.RocksDB 25 txCache *db.TxCache 26 chain bchain.BlockChain 27 chainParser bchain.BlockChainParser 28 metrics *common.Metrics 29 is *common.InternalState 30 api *api.Worker 31 } 32 33 // NewSocketIoServer creates new SocketIo interface to blockbook and returns its handle 34 func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*SocketIoServer, error) { 35 api, err := api.NewWorker(db, chain, txCache, is) 36 if err != nil { 37 return nil, err 38 } 39 40 server := gosocketio.NewServer(transport.GetDefaultWebsocketTransport()) 41 42 server.On(gosocketio.OnConnection, func(c *gosocketio.Channel) { 43 glog.Info("Client connected ", c.Id()) 44 metrics.SocketIOClients.Inc() 45 }) 46 47 server.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel) { 48 glog.Info("Client disconnected ", c.Id()) 49 metrics.SocketIOClients.Dec() 50 }) 51 52 server.On(gosocketio.OnError, func(c *gosocketio.Channel) { 53 glog.Error("Client error ", c.Id()) 54 }) 55 56 type Message struct { 57 Name string `json:"name"` 58 Message string `json:"message"` 59 } 60 s := &SocketIoServer{ 61 server: server, 62 db: db, 63 txCache: txCache, 64 chain: chain, 65 chainParser: chain.GetChainParser(), 66 metrics: metrics, 67 is: is, 68 api: api, 69 } 70 71 server.On("message", s.onMessage) 72 server.On("subscribe", s.onSubscribe) 73 74 return s, nil 75 } 76 77 // GetHandler returns socket.io http handler 78 func (s *SocketIoServer) GetHandler() http.Handler { 79 return s.server 80 } 81 82 type addrOpts struct { 83 Start int `json:"start"` 84 End int `json:"end"` 85 QueryMempoolOnly bool `json:"queryMempoolOnly"` 86 From int `json:"from"` 87 To int `json:"to"` 88 } 89 90 var onMessageHandlers = map[string]func(*SocketIoServer, json.RawMessage) (interface{}, error){ 91 "getAddressTxids": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 92 addr, opts, err := unmarshalGetAddressRequest(params) 93 if err == nil { 94 rv, err = s.getAddressTxids(addr, &opts) 95 } 96 return 97 }, 98 "getAddressHistory": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 99 addr, opts, err := unmarshalGetAddressRequest(params) 100 if err == nil { 101 rv, err = s.getAddressHistory(addr, &opts) 102 } 103 return 104 }, 105 "getBlockHeader": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 106 height, hash, err := unmarshalGetBlockHeader(params) 107 if err == nil { 108 rv, err = s.getBlockHeader(height, hash) 109 } 110 return 111 }, 112 "estimateSmartFee": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 113 blocks, conservative, err := unmarshalEstimateSmartFee(params) 114 if err == nil { 115 rv, err = s.estimateSmartFee(blocks, conservative) 116 } 117 return 118 }, 119 "estimateFee": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 120 blocks, err := unmarshalEstimateFee(params) 121 if err == nil { 122 rv, err = s.estimateFee(blocks) 123 } 124 return 125 }, 126 "getInfo": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 127 return s.getInfo() 128 }, 129 "getDetailedTransaction": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 130 txid, err := unmarshalGetDetailedTransaction(params) 131 if err == nil { 132 rv, err = s.getDetailedTransaction(txid) 133 } 134 return 135 }, 136 "sendTransaction": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 137 tx, err := unmarshalStringParameter(params) 138 if err == nil { 139 rv, err = s.sendTransaction(tx) 140 } 141 return 142 }, 143 "getMempoolEntry": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { 144 txid, err := unmarshalStringParameter(params) 145 if err == nil { 146 rv, err = s.getMempoolEntry(txid) 147 } 148 return 149 }, 150 } 151 152 type resultError struct { 153 Error struct { 154 Message string `json:"message"` 155 } `json:"error"` 156 } 157 158 func (s *SocketIoServer) onMessage(c *gosocketio.Channel, req map[string]json.RawMessage) (rv interface{}) { 159 var err error 160 method := strings.Trim(string(req["method"]), "\"") 161 defer func() { 162 if r := recover(); r != nil { 163 glog.Error(c.Id(), " onMessage ", method, " recovered from panic: ", r) 164 e := resultError{} 165 e.Error.Message = "Internal error" 166 rv = e 167 } 168 }() 169 t := time.Now() 170 params := req["params"] 171 defer s.metrics.SocketIOReqDuration.With(common.Labels{"method": method}).Observe(float64(time.Since(t)) / 1e3) // in microseconds 172 f, ok := onMessageHandlers[method] 173 if ok { 174 rv, err = f(s, params) 175 } else { 176 err = errors.New("unknown method") 177 } 178 if err == nil { 179 glog.V(1).Info(c.Id(), " onMessage ", method, " success") 180 s.metrics.SocketIORequests.With(common.Labels{"method": method, "status": "success"}).Inc() 181 return rv 182 } 183 glog.Error(c.Id(), " onMessage ", method, ": ", errors.ErrorStack(err)) 184 s.metrics.SocketIORequests.With(common.Labels{"method": method, "status": err.Error()}).Inc() 185 e := resultError{} 186 e.Error.Message = err.Error() 187 return e 188 } 189 190 func unmarshalGetAddressRequest(params []byte) (addr []string, opts addrOpts, err error) { 191 var p []json.RawMessage 192 err = json.Unmarshal(params, &p) 193 if err != nil { 194 return 195 } 196 if len(p) != 2 { 197 err = errors.New("incorrect number of parameters") 198 return 199 } 200 err = json.Unmarshal(p[0], &addr) 201 if err != nil { 202 return 203 } 204 err = json.Unmarshal(p[1], &opts) 205 return 206 } 207 208 type resultAddressTxids struct { 209 Result []string `json:"result"` 210 } 211 212 func (s *SocketIoServer) getAddressTxids(addr []string, opts *addrOpts) (res resultAddressTxids, err error) { 213 txids := make([]string, 0, 8) 214 lower, higher := uint32(opts.End), uint32(opts.Start) 215 for _, address := range addr { 216 if !opts.QueryMempoolOnly { 217 err = s.db.GetTransactions(address, lower, higher, func(txid string, vout uint32, isOutput bool) error { 218 txids = append(txids, txid) 219 return nil 220 }) 221 if err != nil { 222 return res, err 223 } 224 } else { 225 m, err := s.chain.GetMempoolTransactions(address) 226 if err != nil { 227 return res, err 228 } 229 txids = append(txids, m...) 230 } 231 } 232 res.Result = api.UniqueTxidsInReverse(txids) 233 return res, nil 234 } 235 236 type addressHistoryIndexes struct { 237 InputIndexes []int `json:"inputIndexes"` 238 OutputIndexes []int `json:"outputIndexes"` 239 } 240 241 type txInputs struct { 242 Txid *string `json:"txid"` 243 OutputIndex int `json:"outputIndex"` 244 Script *string `json:"script"` 245 // ScriptAsm *string `json:"scriptAsm"` 246 Sequence int64 `json:"sequence"` 247 Address *string `json:"address"` 248 Satoshis int64 `json:"satoshis"` 249 } 250 251 type txOutputs struct { 252 Satoshis int64 `json:"satoshis"` 253 Script *string `json:"script"` 254 // ScriptAsm *string `json:"scriptAsm"` 255 // SpentTxID *string `json:"spentTxId,omitempty"` 256 // SpentIndex int `json:"spentIndex,omitempty"` 257 // SpentHeight int `json:"spentHeight,omitempty"` 258 Address *string `json:"address"` 259 } 260 261 type resTx struct { 262 Hex string `json:"hex"` 263 // BlockHash string `json:"blockHash,omitempty"` 264 Height int `json:"height"` 265 BlockTimestamp int64 `json:"blockTimestamp,omitempty"` 266 Version int `json:"version"` 267 Hash string `json:"hash"` 268 Locktime int `json:"locktime,omitempty"` 269 // Size int `json:"size,omitempty"` 270 Inputs []txInputs `json:"inputs"` 271 InputSatoshis int64 `json:"inputSatoshis,omitempty"` 272 Outputs []txOutputs `json:"outputs"` 273 OutputSatoshis int64 `json:"outputSatoshis,omitempty"` 274 FeeSatoshis int64 `json:"feeSatoshis,omitempty"` 275 } 276 277 type addressHistoryItem struct { 278 Addresses map[string]*addressHistoryIndexes `json:"addresses"` 279 Satoshis int64 `json:"satoshis"` 280 Confirmations int `json:"confirmations"` 281 Tx resTx `json:"tx"` 282 } 283 284 type resultGetAddressHistory struct { 285 Result struct { 286 TotalCount int `json:"totalCount"` 287 Items []addressHistoryItem `json:"items"` 288 } `json:"result"` 289 } 290 291 func stringInSlice(a string, list []string) bool { 292 for _, b := range list { 293 if b == a { 294 return true 295 } 296 } 297 return false 298 } 299 300 func txToResTx(tx *api.Tx) resTx { 301 inputs := make([]txInputs, len(tx.Vin)) 302 for i := range tx.Vin { 303 vin := &tx.Vin[i] 304 txid := vin.Txid 305 script := vin.ScriptSig.Hex 306 input := txInputs{ 307 Txid: &txid, 308 Script: &script, 309 Sequence: int64(vin.Sequence), 310 OutputIndex: int(vin.Vout), 311 Satoshis: vin.ValueSat.Int64(), 312 } 313 if len(vin.Addresses) > 0 { 314 a := vin.Addresses[0] 315 input.Address = &a 316 } 317 inputs[i] = input 318 } 319 outputs := make([]txOutputs, len(tx.Vout)) 320 for i := range tx.Vout { 321 vout := &tx.Vout[i] 322 script := vout.ScriptPubKey.Hex 323 output := txOutputs{ 324 Satoshis: vout.ValueSat.Int64(), 325 Script: &script, 326 } 327 if len(vout.ScriptPubKey.Addresses) > 0 { 328 a := vout.ScriptPubKey.Addresses[0] 329 output.Address = &a 330 } 331 outputs[i] = output 332 } 333 var h int 334 if tx.Confirmations == 0 { 335 h = -1 336 } else { 337 h = int(tx.Blockheight) 338 } 339 return resTx{ 340 BlockTimestamp: tx.Blocktime, 341 FeeSatoshis: tx.FeesSat.Int64(), 342 Hash: tx.Txid, 343 Height: h, 344 Hex: tx.Hex, 345 Inputs: inputs, 346 InputSatoshis: tx.ValueInSat.Int64(), 347 Locktime: int(tx.Locktime), 348 Outputs: outputs, 349 OutputSatoshis: tx.ValueOutSat.Int64(), 350 Version: int(tx.Version), 351 } 352 } 353 354 func addressInSlice(s, t []string) string { 355 for _, sa := range s { 356 for _, ta := range t { 357 if ta == sa { 358 return sa 359 } 360 } 361 } 362 return "" 363 } 364 365 func (s *SocketIoServer) getAddressesFromVout(vout *bchain.Vout) ([]string, error) { 366 addrDesc, err := s.chainParser.GetAddrDescFromVout(vout) 367 if err != nil { 368 return nil, err 369 } 370 voutAddr, _, err := s.chainParser.GetAddressesFromAddrDesc(addrDesc) 371 if err != nil { 372 return nil, err 373 } 374 return voutAddr, nil 375 } 376 377 func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res resultGetAddressHistory, err error) { 378 txr, err := s.getAddressTxids(addr, opts) 379 if err != nil { 380 return 381 } 382 txids := txr.Result 383 res.Result.TotalCount = len(txids) 384 res.Result.Items = make([]addressHistoryItem, 0, 8) 385 to := len(txids) 386 if to > opts.To { 387 to = opts.To 388 } 389 for txi := opts.From; txi < to; txi++ { 390 tx, err := s.api.GetTransaction(txids[txi], false) 391 if err != nil { 392 return res, err 393 } 394 ads := make(map[string]*addressHistoryIndexes) 395 var totalSat big.Int 396 for i := range tx.Vin { 397 vin := &tx.Vin[i] 398 a := addressInSlice(vin.Addresses, addr) 399 if a != "" { 400 hi := ads[a] 401 if hi == nil { 402 hi = &addressHistoryIndexes{OutputIndexes: []int{}} 403 ads[a] = hi 404 } 405 hi.InputIndexes = append(hi.InputIndexes, int(vin.N)) 406 totalSat.Sub(&totalSat, &vin.ValueSat) 407 } 408 } 409 for i := range tx.Vout { 410 vout := &tx.Vout[i] 411 a := addressInSlice(vout.ScriptPubKey.Addresses, addr) 412 if a != "" { 413 hi := ads[a] 414 if hi == nil { 415 hi = &addressHistoryIndexes{InputIndexes: []int{}} 416 ads[a] = hi 417 } 418 hi.OutputIndexes = append(hi.OutputIndexes, int(vout.N)) 419 totalSat.Add(&totalSat, &vout.ValueSat) 420 } 421 } 422 ahi := addressHistoryItem{} 423 ahi.Addresses = ads 424 ahi.Confirmations = int(tx.Confirmations) 425 ahi.Satoshis = totalSat.Int64() 426 ahi.Tx = txToResTx(tx) 427 res.Result.Items = append(res.Result.Items, ahi) 428 // } 429 } 430 return 431 } 432 433 func unmarshalArray(params []byte, np int) (p []interface{}, err error) { 434 err = json.Unmarshal(params, &p) 435 if err != nil { 436 return 437 } 438 if len(p) != np { 439 err = errors.New("incorrect number of parameters") 440 return 441 } 442 return 443 } 444 445 func unmarshalGetBlockHeader(params []byte) (height uint32, hash string, err error) { 446 p, err := unmarshalArray(params, 1) 447 if err != nil { 448 return 449 } 450 fheight, ok := p[0].(float64) 451 if ok { 452 return uint32(fheight), "", nil 453 } 454 hash, ok = p[0].(string) 455 if ok { 456 return 457 } 458 err = errors.New("incorrect parameter") 459 return 460 } 461 462 type resultGetBlockHeader struct { 463 Result struct { 464 Hash string `json:"hash"` 465 Version int `json:"version"` 466 Confirmations int `json:"confirmations"` 467 Height int `json:"height"` 468 ChainWork string `json:"chainWork"` 469 NextHash string `json:"nextHash"` 470 MerkleRoot string `json:"merkleRoot"` 471 Time int `json:"time"` 472 MedianTime int `json:"medianTime"` 473 Nonce int `json:"nonce"` 474 Bits string `json:"bits"` 475 Difficulty float64 `json:"difficulty"` 476 } `json:"result"` 477 } 478 479 func (s *SocketIoServer) getBlockHeader(height uint32, hash string) (res resultGetBlockHeader, err error) { 480 if hash == "" { 481 // trezor is interested only in hash 482 hash, err = s.db.GetBlockHash(height) 483 if err != nil { 484 return 485 } 486 res.Result.Hash = hash 487 return 488 } 489 bh, err := s.chain.GetBlockHeader(hash) 490 if err != nil { 491 return 492 } 493 res.Result.Hash = bh.Hash 494 res.Result.Confirmations = bh.Confirmations 495 res.Result.Height = int(bh.Height) 496 res.Result.NextHash = bh.Next 497 return 498 } 499 500 func unmarshalEstimateSmartFee(params []byte) (blocks int, conservative bool, err error) { 501 p, err := unmarshalArray(params, 2) 502 if err != nil { 503 return 504 } 505 fblocks, ok := p[0].(float64) 506 if !ok { 507 err = errors.New("Invalid parameter blocks") 508 return 509 } 510 blocks = int(fblocks) 511 conservative, ok = p[1].(bool) 512 if !ok { 513 err = errors.New("Invalid parameter conservative") 514 return 515 } 516 return 517 } 518 519 type resultEstimateSmartFee struct { 520 // for compatibility reasons use float64 521 Result float64 `json:"result"` 522 } 523 524 func (s *SocketIoServer) estimateSmartFee(blocks int, conservative bool) (res resultEstimateSmartFee, err error) { 525 fee, err := s.chain.EstimateSmartFee(blocks, conservative) 526 if err != nil { 527 return 528 } 529 res.Result, err = strconv.ParseFloat(s.chainParser.AmountToDecimalString(&fee), 64) 530 return 531 } 532 533 func unmarshalEstimateFee(params []byte) (blocks int, err error) { 534 p, err := unmarshalArray(params, 1) 535 if err != nil { 536 return 537 } 538 fblocks, ok := p[0].(float64) 539 if !ok { 540 err = errors.New("Invalid parameter nblocks") 541 return 542 } 543 blocks = int(fblocks) 544 return 545 } 546 547 type resultEstimateFee struct { 548 // for compatibility reasons use float64 549 Result float64 `json:"result"` 550 } 551 552 func (s *SocketIoServer) estimateFee(blocks int) (res resultEstimateFee, err error) { 553 fee, err := s.chain.EstimateFee(blocks) 554 if err != nil { 555 return 556 } 557 res.Result, err = strconv.ParseFloat(s.chainParser.AmountToDecimalString(&fee), 64) 558 return 559 } 560 561 type resultGetInfo struct { 562 Result struct { 563 Version int `json:"version,omitempty"` 564 ProtocolVersion int `json:"protocolVersion,omitempty"` 565 Blocks int `json:"blocks"` 566 TimeOffset int `json:"timeOffset,omitempty"` 567 Connections int `json:"connections,omitempty"` 568 Proxy string `json:"proxy,omitempty"` 569 Difficulty float64 `json:"difficulty,omitempty"` 570 Testnet bool `json:"testnet"` 571 RelayFee float64 `json:"relayFee,omitempty"` 572 Errors string `json:"errors,omitempty"` 573 Network string `json:"network,omitempty"` 574 Subversion string `json:"subversion,omitempty"` 575 LocalServices string `json:"localServices,omitempty"` 576 CoinName string `json:"coin_name,omitempty"` 577 About string `json:"about,omitempty"` 578 } `json:"result"` 579 } 580 581 func (s *SocketIoServer) getInfo() (res resultGetInfo, err error) { 582 height, _, err := s.db.GetBestBlock() 583 if err != nil { 584 return 585 } 586 res.Result.Blocks = int(height) 587 res.Result.Testnet = s.chain.IsTestnet() 588 res.Result.Network = s.chain.GetNetworkName() 589 res.Result.Subversion = s.chain.GetSubversion() 590 res.Result.CoinName = s.chain.GetCoinName() 591 res.Result.About = api.Text.BlockbookAbout 592 return 593 } 594 595 func unmarshalStringParameter(params []byte) (s string, err error) { 596 p, err := unmarshalArray(params, 1) 597 if err != nil { 598 return 599 } 600 s, ok := p[0].(string) 601 if ok { 602 return 603 } 604 err = errors.New("incorrect parameter") 605 return 606 } 607 608 func unmarshalGetDetailedTransaction(params []byte) (txid string, err error) { 609 var p []json.RawMessage 610 err = json.Unmarshal(params, &p) 611 if err != nil { 612 return 613 } 614 if len(p) != 1 { 615 err = errors.New("incorrect number of parameters") 616 return 617 } 618 err = json.Unmarshal(p[0], &txid) 619 if err != nil { 620 return 621 } 622 return 623 } 624 625 type resultGetDetailedTransaction struct { 626 Result resTx `json:"result"` 627 } 628 629 func (s *SocketIoServer) getDetailedTransaction(txid string) (res resultGetDetailedTransaction, err error) { 630 tx, err := s.api.GetTransaction(txid, false) 631 if err != nil { 632 return res, err 633 } 634 res.Result = txToResTx(tx) 635 return 636 } 637 638 func (s *SocketIoServer) sendTransaction(tx string) (res resultSendTransaction, err error) { 639 txid, err := s.chain.SendRawTransaction(tx) 640 if err != nil { 641 return res, err 642 } 643 res.Result = txid 644 return 645 } 646 647 type resultGetMempoolEntry struct { 648 Result *bchain.MempoolEntry `json:"result"` 649 } 650 651 func (s *SocketIoServer) getMempoolEntry(txid string) (res resultGetMempoolEntry, err error) { 652 entry, err := s.chain.GetMempoolEntry(txid) 653 if err != nil { 654 return res, err 655 } 656 res.Result = entry 657 return 658 } 659 660 // onSubscribe expects two event subscriptions based on the req parameter (including the doublequotes): 661 // "bitcoind/hashblock" 662 // "bitcoind/addresstxid",["2MzTmvPJLZaLzD9XdN3jMtQA5NexC3rAPww","2NAZRJKr63tSdcTxTN3WaE9ZNDyXy6PgGuv"] 663 func (s *SocketIoServer) onSubscribe(c *gosocketio.Channel, req []byte) interface{} { 664 defer func() { 665 if r := recover(); r != nil { 666 glog.Error(c.Id(), " onSubscribe recovered from panic: ", r) 667 } 668 }() 669 670 onError := func(id, sc, err, detail string) { 671 glog.Error(id, " onSubscribe ", err, ": ", detail) 672 s.metrics.SocketIOSubscribes.With(common.Labels{"channel": sc, "status": err}).Inc() 673 } 674 675 r := string(req) 676 glog.V(1).Info(c.Id(), " onSubscribe ", r) 677 var sc string 678 i := strings.Index(r, "\",[") 679 if i > 0 { 680 var addrs []string 681 sc = r[1:i] 682 if sc != "bitcoind/addresstxid" { 683 onError(c.Id(), sc, "invalid data", "expecting bitcoind/addresstxid, req: "+r) 684 return nil 685 } 686 err := json.Unmarshal([]byte(r[i+2:]), &addrs) 687 if err != nil { 688 onError(c.Id(), sc, "invalid data", err.Error()+", req: "+r) 689 return nil 690 } 691 // normalize the addresses to AddressDescriptor 692 descs := make([]bchain.AddressDescriptor, len(addrs)) 693 for i, a := range addrs { 694 d, err := s.chainParser.GetAddrDescFromAddress(a) 695 if err != nil { 696 onError(c.Id(), sc, "invalid address "+a, err.Error()+", req: "+r) 697 return nil 698 } 699 descs[i] = d 700 } 701 for _, d := range descs { 702 c.Join("bitcoind/addresstxid-" + string(d)) 703 } 704 } else { 705 sc = r[1 : len(r)-1] 706 if sc != "bitcoind/hashblock" { 707 onError(c.Id(), sc, "invalid data", "expecting bitcoind/hashblock, req: "+r) 708 return nil 709 } 710 c.Join(sc) 711 } 712 s.metrics.SocketIOSubscribes.With(common.Labels{"channel": sc, "status": "success"}).Inc() 713 return nil 714 } 715 716 // OnNewBlockHash notifies users subscribed to bitcoind/hashblock about new block 717 func (s *SocketIoServer) OnNewBlockHash(hash string) { 718 c := s.server.BroadcastTo("bitcoind/hashblock", "bitcoind/hashblock", hash) 719 glog.Info("broadcasting new block hash ", hash, " to ", c, " channels") 720 } 721 722 // OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block 723 func (s *SocketIoServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) { 724 addr, searchable, err := s.chainParser.GetAddressesFromAddrDesc(desc) 725 if err != nil { 726 glog.Error("GetAddressesFromAddrDesc error ", err, " for descriptor ", desc) 727 } else if searchable && len(addr) == 1 { 728 data := map[string]interface{}{"address": addr[0], "txid": txid} 729 if !isOutput { 730 data["input"] = true 731 } 732 c := s.server.BroadcastTo("bitcoind/addresstxid-"+string(desc), "bitcoind/addresstxid", data) 733 if c > 0 { 734 glog.Info("broadcasting new txid ", txid, " for addr ", addr[0], " to ", c, " channels") 735 } 736 } 737 }