github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/dcrdata/cmds.go (about) 1 // Copyright (c) 2020-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package dcrdata 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "strconv" 14 "strings" 15 16 jsonrpc "github.com/decred/dcrd/rpc/jsonrpc/types/v2" 17 types "github.com/decred/dcrdata/v6/api/types" 18 "github.com/decred/politeia/politeiad/plugins/dcrdata" 19 "github.com/decred/politeia/util" 20 ) 21 22 // cmdBestBlock returns the best block. If the dcrdata websocket has been 23 // disconnected the best block will be fetched from the dcrdata HTTP API. If 24 // dcrdata cannot be reached then the most recent cached best block will be 25 // returned along with a status of StatusDisconnected. It is the callers 26 // responsibility to determine if the stale best block should be used. 27 func (p *dcrdataPlugin) cmdBestBlock(payload string) (string, error) { 28 // Payload is empty. Nothing to decode. 29 30 // Get the cached best block 31 bb := p.bestBlockGet() 32 var ( 33 fetch bool 34 stale uint32 35 status = dcrdata.StatusConnected 36 ) 37 switch { 38 case bb == 0: 39 // No cached best block means that the best block has not been 40 // populated by the websocket yet. Fetch is manually. 41 fetch = true 42 case p.bestBlockIsStale(): 43 // The cached best block has been populated by the websocket, but 44 // the websocket is currently disconnected and the cached value 45 // is stale. Try to fetch the best block manually and only use 46 // the stale value if manually fetching it fails. 47 fetch = true 48 stale = bb 49 } 50 51 // Fetch the best block manually if required 52 if fetch { 53 block, err := p.bestBlockHTTP() 54 switch { 55 case err == nil: 56 // We got the best block. Use it. 57 bb = block.Height 58 case stale != 0: 59 // Unable to fetch the best block manually. Use the stale 60 // value and mark the connection status as disconnected. 61 bb = stale 62 status = dcrdata.StatusDisconnected 63 default: 64 // Unable to fetch the best block manually and there is no 65 // stale cached value to return. 66 return "", fmt.Errorf("bestBlockHTTP: %v", err) 67 } 68 } 69 70 // Prepare reply 71 bbr := dcrdata.BestBlockReply{ 72 Status: status, 73 Height: bb, 74 } 75 reply, err := json.Marshal(bbr) 76 if err != nil { 77 return "", err 78 } 79 80 return string(reply), nil 81 } 82 83 // cmdBlockDetails retrieves the block details for the provided block height. 84 func (p *dcrdataPlugin) cmdBlockDetails(payload string) (string, error) { 85 // Decode payload 86 var bd dcrdata.BlockDetails 87 err := json.Unmarshal([]byte(payload), &bd) 88 if err != nil { 89 return "", err 90 } 91 92 // Fetch block details 93 bdb, err := p.blockDetails(bd.Height) 94 if err != nil { 95 return "", fmt.Errorf("blockDetails: %v", err) 96 } 97 98 // Prepare reply 99 bdr := dcrdata.BlockDetailsReply{ 100 Block: convertBlockDataBasicFromV5(*bdb), 101 } 102 reply, err := json.Marshal(bdr) 103 if err != nil { 104 return "", err 105 } 106 107 return string(reply), nil 108 } 109 110 // cmdTicketPool requests the lists of tickets in the ticket pool at a 111 // specified block hash. 112 func (p *dcrdataPlugin) cmdTicketPool(payload string) (string, error) { 113 // Decode payload 114 var tp dcrdata.TicketPool 115 err := json.Unmarshal([]byte(payload), &tp) 116 if err != nil { 117 return "", err 118 } 119 120 // Get the ticket pool 121 tickets, err := p.ticketPool(tp.BlockHash) 122 if err != nil { 123 return "", fmt.Errorf("ticketPool: %v", err) 124 } 125 126 // Prepare reply 127 tpr := dcrdata.TicketPoolReply{ 128 Tickets: tickets, 129 } 130 reply, err := json.Marshal(tpr) 131 if err != nil { 132 return "", err 133 } 134 135 return string(reply), nil 136 } 137 138 // TxsTrimmed requests the trimmed transaction information for the provided 139 // transaction IDs. 140 func (p *dcrdataPlugin) cmdTxsTrimmed(payload string) (string, error) { 141 // Decode payload 142 var tt dcrdata.TxsTrimmed 143 err := json.Unmarshal([]byte(payload), &tt) 144 if err != nil { 145 return "", err 146 } 147 148 // Get trimmed txs 149 txs, err := p.txsTrimmed(tt.TxIDs) 150 if err != nil { 151 return "", fmt.Errorf("txsTrimmed: %v", err) 152 } 153 154 // Prepare reply 155 ttr := dcrdata.TxsTrimmedReply{ 156 Txs: convertTrimmedTxsFromV5(txs), 157 } 158 reply, err := json.Marshal(ttr) 159 if err != nil { 160 return "", err 161 } 162 163 return string(reply), nil 164 } 165 166 // makeReq makes a dcrdata http request to the method and route provided, 167 // serializing the provided object as the request body, and returning a byte 168 // slice of the response body. An error is returned if dcrdata responds with 169 // anything other than a 200 http status code. 170 func (p *dcrdataPlugin) makeReq(method string, route string, headers map[string]string, v interface{}) ([]byte, error) { 171 var ( 172 url = p.hostHTTP + route 173 reqBody []byte 174 err error 175 ) 176 177 log.Tracef("%v %v", method, url) 178 179 // Setup request 180 if v != nil { 181 reqBody, err = json.Marshal(v) 182 if err != nil { 183 return nil, err 184 } 185 } 186 req, err := http.NewRequest(method, url, bytes.NewReader(reqBody)) 187 if err != nil { 188 return nil, err 189 } 190 for k, v := range headers { 191 req.Header.Add(k, v) 192 } 193 194 // Send request 195 r, err := p.client.Do(req) 196 if err != nil { 197 return nil, err 198 } 199 defer r.Body.Close() 200 201 // Handle response 202 if r.StatusCode != http.StatusOK { 203 body, err := io.ReadAll(r.Body) 204 if err != nil { 205 return nil, fmt.Errorf("%v %v %v %v", 206 r.StatusCode, method, url, err) 207 } 208 return nil, fmt.Errorf("%v %v %v %s", 209 r.StatusCode, method, url, body) 210 } 211 212 return util.RespBody(r), nil 213 } 214 215 // bestBlockHTTP fetches and returns the best block from the dcrdata http API. 216 func (p *dcrdataPlugin) bestBlockHTTP() (*types.BlockDataBasic, error) { 217 resBody, err := p.makeReq(http.MethodGet, routeBestBlock, nil, nil) 218 if err != nil { 219 return nil, err 220 } 221 222 var bdb types.BlockDataBasic 223 err = json.Unmarshal(resBody, &bdb) 224 if err != nil { 225 return nil, err 226 } 227 228 return &bdb, nil 229 } 230 231 // blockDetails returns the block details for the block at the specified block 232 // height. 233 func (p *dcrdataPlugin) blockDetails(height uint32) (*types.BlockDataBasic, error) { 234 h := strconv.FormatUint(uint64(height), 10) 235 236 route := strings.Replace(routeBlockDetails, "{height}", h, 1) 237 resBody, err := p.makeReq(http.MethodGet, route, nil, nil) 238 if err != nil { 239 return nil, err 240 } 241 242 var bdb types.BlockDataBasic 243 err = json.Unmarshal(resBody, &bdb) 244 if err != nil { 245 return nil, err 246 } 247 248 return &bdb, nil 249 } 250 251 // ticketPool returns the list of tickets in the ticket pool at the specified 252 // block hash. 253 func (p *dcrdataPlugin) ticketPool(blockHash string) ([]string, error) { 254 route := strings.Replace(routeTicketPool, "{hash}", blockHash, 1) 255 route += "?sort=true" 256 resBody, err := p.makeReq(http.MethodGet, route, nil, nil) 257 if err != nil { 258 return nil, err 259 } 260 261 var tickets []string 262 err = json.Unmarshal(resBody, &tickets) 263 if err != nil { 264 return nil, err 265 } 266 267 return tickets, nil 268 } 269 270 // txsTrimmed returns the TrimmedTx for the specified tx IDs. 271 func (p *dcrdataPlugin) txsTrimmed(txIDs []string) ([]types.TrimmedTx, error) { 272 t := types.Txns{ 273 Transactions: txIDs, 274 } 275 headers := map[string]string{ 276 headerContentType: contentTypeJSON, 277 } 278 resBody, err := p.makeReq(http.MethodPost, routeTxsTrimmed, headers, t) 279 if err != nil { 280 return nil, err 281 } 282 283 var txs []types.TrimmedTx 284 err = json.Unmarshal(resBody, &txs) 285 if err != nil { 286 return nil, err 287 } 288 289 return txs, nil 290 } 291 292 func convertTicketPoolInfoFromV5(t types.TicketPoolInfo) dcrdata.TicketPoolInfo { 293 return dcrdata.TicketPoolInfo{ 294 Height: t.Height, 295 Size: t.Size, 296 Value: t.Value, 297 ValAvg: t.ValAvg, 298 Winners: t.Winners, 299 } 300 } 301 302 func convertBlockDataBasicFromV5(b types.BlockDataBasic) dcrdata.BlockDataBasic { 303 var poolInfo *dcrdata.TicketPoolInfo 304 if b.PoolInfo != nil { 305 p := convertTicketPoolInfoFromV5(*b.PoolInfo) 306 poolInfo = &p 307 } 308 return dcrdata.BlockDataBasic{ 309 Height: b.Height, 310 Size: b.Size, 311 Hash: b.Hash, 312 Difficulty: b.Difficulty, 313 StakeDiff: b.StakeDiff, 314 Time: b.Time.UNIX(), 315 NumTx: b.NumTx, 316 MiningFee: b.MiningFee, 317 TotalSent: b.TotalSent, 318 PoolInfo: poolInfo, 319 } 320 } 321 322 func convertScriptSigFromJSONRPC(s jsonrpc.ScriptSig) dcrdata.ScriptSig { 323 return dcrdata.ScriptSig{ 324 Asm: s.Asm, 325 Hex: s.Hex, 326 } 327 } 328 329 func convertVinFromJSONRPC(v jsonrpc.Vin) dcrdata.Vin { 330 var scriptSig *dcrdata.ScriptSig 331 if v.ScriptSig != nil { 332 s := convertScriptSigFromJSONRPC(*v.ScriptSig) 333 scriptSig = &s 334 } 335 return dcrdata.Vin{ 336 Coinbase: v.Coinbase, 337 Stakebase: v.Stakebase, 338 Txid: v.Txid, 339 Vout: v.Vout, 340 Tree: v.Tree, 341 Sequence: v.Sequence, 342 AmountIn: v.AmountIn, 343 BlockHeight: v.BlockHeight, 344 BlockIndex: v.BlockIndex, 345 ScriptSig: scriptSig, 346 } 347 } 348 349 func convertVinsFromV5(ins []jsonrpc.Vin) []dcrdata.Vin { 350 i := make([]dcrdata.Vin, 0, len(ins)) 351 for _, v := range ins { 352 i = append(i, convertVinFromJSONRPC(v)) 353 } 354 return i 355 } 356 357 func convertScriptPubKeyFromV5(s types.ScriptPubKey) dcrdata.ScriptPubKey { 358 return dcrdata.ScriptPubKey{ 359 Asm: s.Asm, 360 Hex: s.Hex, 361 ReqSigs: s.ReqSigs, 362 Type: s.Type, 363 Addresses: s.Addresses, 364 CommitAmt: s.CommitAmt, 365 } 366 } 367 368 func convertTxInputIDFromV5(t types.TxInputID) dcrdata.TxInputID { 369 return dcrdata.TxInputID{ 370 Hash: t.Hash, 371 Index: t.Index, 372 } 373 } 374 375 func convertVoutFromV5(v types.Vout) dcrdata.Vout { 376 var spend *dcrdata.TxInputID 377 if v.Spend != nil { 378 s := convertTxInputIDFromV5(*v.Spend) 379 spend = &s 380 } 381 return dcrdata.Vout{ 382 Value: v.Value, 383 N: v.N, 384 Version: v.Version, 385 ScriptPubKeyDecoded: convertScriptPubKeyFromV5(v.ScriptPubKeyDecoded), 386 Spend: spend, 387 } 388 } 389 390 func convertVoutsFromV5(outs []types.Vout) []dcrdata.Vout { 391 o := make([]dcrdata.Vout, 0, len(outs)) 392 for _, v := range outs { 393 o = append(o, convertVoutFromV5(v)) 394 } 395 return o 396 } 397 398 func convertTrimmedTxFromV5(t types.TrimmedTx) dcrdata.TrimmedTx { 399 return dcrdata.TrimmedTx{ 400 TxID: t.TxID, 401 Version: t.Version, 402 Locktime: t.Locktime, 403 Expiry: t.Expiry, 404 Vin: convertVinsFromV5(t.Vin), 405 Vout: convertVoutsFromV5(t.Vout), 406 } 407 } 408 409 func convertTrimmedTxsFromV5(txs []types.TrimmedTx) []dcrdata.TrimmedTx { 410 t := make([]dcrdata.TrimmedTx, 0, len(txs)) 411 for _, v := range txs { 412 t = append(t, convertTrimmedTxFromV5(v)) 413 } 414 return t 415 }