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  }